feat: 权限细化、商业认证跳转、UI改进 #19

Merged
hera merged 10 commits from feat/permissions-ui-improvements into main 2026-04-10 09:35:11 +00:00
2 changed files with 60 additions and 9 deletions
Showing only changes of commit 3c3ce30b48 - Show all commits

View File

@@ -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 ──────────────────────────────────────

View File

@@ -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('操作失败')
}