From 3c3ce30b484dede400717a79e32369f8ceb33984 Mon Sep 17 00:00:00 2001 From: Hera Zhao Date: Thu, 9 Apr 2026 21:58:07 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=B1=E4=BA=AB=E9=85=8D=E6=96=B9?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E5=AE=8C=E6=95=B4=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /api/recipes/{id}/reject 端点:拒绝配方并通知提交者(含原因) - 采纳配方时通知提交者"配方已采纳" - 管理员拒绝配方时输入原因 - 贡献统计改为统计被采纳的配方数(含 audit_log 记录) - 完整流程测试:共享→通知→拒绝(带原因)→通知→重新共享→采纳→通知 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/main.py | 47 ++++++++++++++++++++++++++-- frontend/src/views/RecipeManager.vue | 22 ++++++++----- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/backend/main.py b/backend/main.py index 9ac83ce..9bfa9fe 100644 --- a/backend/main.py +++ b/backend/main.py @@ -863,11 +863,48 @@ def adopt_recipe(recipe_id: int, user=Depends(require_role("admin"))): if row["owner_id"] == user["id"]: conn.close() return {"ok": True, "msg": "already owned"} - old_owner = conn.execute("SELECT display_name, username FROM users WHERE id = ?", (row["owner_id"],)).fetchone() + old_owner = conn.execute("SELECT id, role, display_name, username FROM users WHERE id = ?", (row["owner_id"],)).fetchone() old_name = (old_owner["display_name"] or old_owner["username"]) if old_owner else "unknown" conn.execute("UPDATE recipes SET owner_id = ?, updated_by = ? WHERE id = ?", (user["id"], user["id"], recipe_id)) log_audit(conn, user["id"], "adopt_recipe", "recipe", recipe_id, row["name"], json.dumps({"from_user": old_name})) + # Notify submitter that recipe was approved + if old_owner and old_owner["id"] != user["id"]: + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (old_owner["role"], "🎉 配方已采纳", + f"你共享的配方「{row['name']}」已被采纳到公共配方库!", old_owner["id"]) + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/recipes/{recipe_id}/reject") +def reject_recipe(recipe_id: int, body: dict = None, user=Depends(require_role("admin"))): + conn = get_db() + row = conn.execute("SELECT id, name, owner_id FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + if not row: + conn.close() + raise HTTPException(404, "Recipe not found") + reason = (body or {}).get("reason", "").strip() + # Notify submitter + old_owner = conn.execute("SELECT id, role, display_name, username FROM users WHERE id = ?", (row["owner_id"],)).fetchone() + if old_owner and old_owner["id"] != user["id"]: + msg = f"你共享的配方「{row['name']}」未被采纳。" + if reason: + msg += f"\n原因:{reason}" + msg += "\n你可以修改后重新共享。" + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (old_owner["role"], "配方未被采纳", msg, old_owner["id"]) + ) + # Delete the recipe + conn.execute("DELETE FROM recipe_ingredients WHERE recipe_id = ?", (recipe_id,)) + conn.execute("DELETE FROM recipe_tags WHERE recipe_id = ?", (recipe_id,)) + conn.execute("DELETE FROM recipes WHERE id = ?", (recipe_id,)) + log_audit(conn, user["id"], "reject_recipe", "recipe", recipe_id, row["name"], + json.dumps({"reason": reason})) conn.commit() conn.close() return {"ok": True} @@ -1407,11 +1444,17 @@ def my_contribution(user=Depends(get_current_user)): if not user.get("id"): return {"shared_count": 0} conn = get_db() + # Count recipes adopted from this user (tracked in audit_log) count = conn.execute( + "SELECT COUNT(*) FROM audit_log WHERE action = 'adopt_recipe' AND detail LIKE ?", + (f'%"from_user": "{user.get("display_name") or user.get("username")}"%',) + ).fetchone()[0] + # Also count recipes still owned by user in public library + own_count = conn.execute( "SELECT COUNT(*) FROM recipes WHERE owner_id = ?", (user["id"],) ).fetchone()[0] conn.close() - return {"shared_count": count} + return {"shared_count": count + own_count} # ── Notifications ────────────────────────────────────── diff --git a/frontend/src/views/RecipeManager.vue b/frontend/src/views/RecipeManager.vue index 7befc91..4ecb529 100644 --- a/frontend/src/views/RecipeManager.vue +++ b/frontend/src/views/RecipeManager.vue @@ -534,20 +534,28 @@ async function removeRecipe(recipe) { async function approveRecipe(recipe) { try { - await api('/api/recipes/' + recipe._id + '/adopt', { method: 'POST' }) - ui.showToast('已采纳') - await recipeStore.loadRecipes() + const res = await api('/api/recipes/' + recipe._id + '/adopt', { method: 'POST' }) + if (res.ok) { + ui.showToast('已采纳并通知提交者') + await recipeStore.loadRecipes() + } } catch { ui.showToast('操作失败') } } async function rejectRecipe(recipe) { - const ok = await showConfirm(`确定删除「${recipe.name}」?`) - if (!ok) return + const reason = await showPrompt(`拒绝「${recipe.name}」的原因(选填):`) + if (reason === null) return try { - await recipeStore.deleteRecipe(recipe._id) - ui.showToast('已删除') + const res = await api(`/api/recipes/${recipe._id}/reject`, { + method: 'POST', + body: JSON.stringify({ reason: reason || '' }), + }) + if (res.ok) { + await recipeStore.loadRecipes() + ui.showToast('已拒绝并通知提交者') + } } catch { ui.showToast('操作失败') }