feat: 共享配方审核完整流程
- 新增 /api/recipes/{id}/reject 端点:拒绝配方并通知提交者(含原因)
- 采纳配方时通知提交者"配方已采纳"
- 管理员拒绝配方时输入原因
- 贡献统计改为统计被采纳的配方数(含 audit_log 记录)
- 完整流程测试:共享→通知→拒绝(带原因)→通知→重新共享→采纳→通知
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -863,11 +863,48 @@ def adopt_recipe(recipe_id: int, user=Depends(require_role("admin"))):
|
|||||||
if row["owner_id"] == user["id"]:
|
if row["owner_id"] == user["id"]:
|
||||||
conn.close()
|
conn.close()
|
||||||
return {"ok": True, "msg": "already owned"}
|
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"
|
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))
|
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"],
|
log_audit(conn, user["id"], "adopt_recipe", "recipe", recipe_id, row["name"],
|
||||||
json.dumps({"from_user": old_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.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -1407,11 +1444,17 @@ def my_contribution(user=Depends(get_current_user)):
|
|||||||
if not user.get("id"):
|
if not user.get("id"):
|
||||||
return {"shared_count": 0}
|
return {"shared_count": 0}
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
# Count recipes adopted from this user (tracked in audit_log)
|
||||||
count = conn.execute(
|
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"],)
|
"SELECT COUNT(*) FROM recipes WHERE owner_id = ?", (user["id"],)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
conn.close()
|
conn.close()
|
||||||
return {"shared_count": count}
|
return {"shared_count": count + own_count}
|
||||||
|
|
||||||
|
|
||||||
# ── Notifications ──────────────────────────────────────
|
# ── Notifications ──────────────────────────────────────
|
||||||
|
|||||||
@@ -534,20 +534,28 @@ async function removeRecipe(recipe) {
|
|||||||
|
|
||||||
async function approveRecipe(recipe) {
|
async function approveRecipe(recipe) {
|
||||||
try {
|
try {
|
||||||
await api('/api/recipes/' + recipe._id + '/adopt', { method: 'POST' })
|
const res = await api('/api/recipes/' + recipe._id + '/adopt', { method: 'POST' })
|
||||||
ui.showToast('已采纳')
|
if (res.ok) {
|
||||||
await recipeStore.loadRecipes()
|
ui.showToast('已采纳并通知提交者')
|
||||||
|
await recipeStore.loadRecipes()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
ui.showToast('操作失败')
|
ui.showToast('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectRecipe(recipe) {
|
async function rejectRecipe(recipe) {
|
||||||
const ok = await showConfirm(`确定删除「${recipe.name}」?`)
|
const reason = await showPrompt(`拒绝「${recipe.name}」的原因(选填):`)
|
||||||
if (!ok) return
|
if (reason === null) return
|
||||||
try {
|
try {
|
||||||
await recipeStore.deleteRecipe(recipe._id)
|
const res = await api(`/api/recipes/${recipe._id}/reject`, {
|
||||||
ui.showToast('已删除')
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ reason: reason || '' }),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
await recipeStore.loadRecipes()
|
||||||
|
ui.showToast('已拒绝并通知提交者')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
ui.showToast('操作失败')
|
ui.showToast('操作失败')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user