feat: 高级编辑共享跳过审核 + 去重 + 通知 + 已分享状态
Some checks failed
PR Preview / teardown-preview (pull_request) Has been skipped
Test / unit-test (push) Successful in 4s
Test / build-check (push) Successful in 4s
PR Preview / test (pull_request) Successful in 5s
PR Preview / deploy-preview (pull_request) Successful in 15s
Test / e2e-test (push) Failing after 53s

共享流程:
- 高级编辑/管理员共享直接进公共库(跳过审核)
- 普通用户共享仍需管理员审核
- 高级编辑共享后通知管理员"已添加"(非待审核)

去重检测:
- 同名同内容:提示"已有一模一样的"
- 同名不同内容:提示改名后共享

状态显示:
- 共享后对比公共库内容,相同则显示"已共享"
- 修改内容后"已共享"消失,可重新共享

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 20:03:46 +00:00
parent 650c04a972
commit 480e843316
2 changed files with 45 additions and 18 deletions

View File

@@ -799,9 +799,16 @@ def create_recipe(recipe: RecipeIn, user=Depends(get_current_user)):
c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,)) c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,))
c.execute("INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", (rid, tag)) c.execute("INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", (rid, tag))
log_audit(conn, user["id"], "create_recipe", "recipe", rid, recipe.name) log_audit(conn, user["id"], "create_recipe", "recipe", rid, recipe.name)
# Notify admin when non-admin/non-senior_editor creates a recipe (needs review)
if user["role"] not in ("admin", "senior_editor"):
who = user.get("display_name") or user["username"] who = user.get("display_name") or user["username"]
if user["role"] == "senior_editor":
# Senior editor adds directly — just inform admin
conn.execute(
"INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)",
("admin", "📋 新配方已添加",
f"{who} 将配方「{recipe.name}」添加到了公共配方库。\n[recipe_id:{rid}]")
)
elif user["role"] not in ("admin",):
# Other users need review
conn.execute( conn.execute(
"INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)",
("admin", "📝 新配方待审核", ("admin", "📝 新配方待审核",

View File

@@ -1199,22 +1199,21 @@ function openRecipeDetail(recipe) {
if (idx >= 0) previewRecipeIndex.value = idx if (idx >= 0) previewRecipeIndex.value = idx
} }
function getDiaryShareStatus(d) { function diaryMatchesPublic(d) {
// Check pending first (exact name match in public library owned by user)
if (sharedCount.value.pendingNames.includes(d.name)) return 'pending'
// Check adopted — but if diary content was modified, allow re-sharing
if (sharedCount.value.adoptedNames.includes(d.name)) {
// Compare with the public recipe to see if content changed
const pub = recipeStore.recipes.find(r => r.name === d.name) const pub = recipeStore.recipes.find(r => r.name === d.name)
if (pub) { if (!pub) return false
const dIngs = (d.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil)) const dIngs = (d.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
const pIngs = (pub.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil)) const pIngs = (pub.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
const same = dIngs.length === pIngs.length && dIngs.every((ing, i) => ing.oil === pIngs[i].oil && ing.drops === pIngs[i].drops) return dIngs.length === pIngs.length && dIngs.every((ing, i) => ing.oil === pIngs[i].oil && ing.drops === pIngs[i].drops)
if (same) return 'shared' }
}
// Content changed or public recipe not found — can re-share function getDiaryShareStatus(d) {
return null // Check pending (owned by user in public library, not yet adopted)
} if (sharedCount.value.pendingNames.includes(d.name)) return 'pending'
// Check if public library has same recipe with same content
if (diaryMatchesPublic(d)) return 'shared'
// Check adopted names from audit log
if (sharedCount.value.adoptedNames.includes(d.name) && diaryMatchesPublic(d)) return 'shared'
return null return null
} }
@@ -1244,6 +1243,27 @@ async function recommendApprove(recipe) {
} }
async function shareDiaryToPublic(diary) { async function shareDiaryToPublic(diary) {
// Check for duplicates in public library
const dup = recipeStore.recipes.find(r => r.name === diary.name)
if (dup) {
const dIngs = (diary.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
const pIngs = (dup.ingredients || []).filter(i => i.oil).sort((a, b) => a.oil.localeCompare(b.oil))
const identical = dIngs.length === pIngs.length && dIngs.every((ing, i) => ing.oil === pIngs[i].oil && ing.drops === pIngs[i].drops)
if (identical) {
ui.showToast('公共配方库中已有一模一样的配方「' + diary.name + '」')
return
}
// Same name, different content
const action = await showConfirm(
`公共配方库中已有同名配方「${diary.name}」但内容不同,是否改名后共享?`,
{ okText: '改名', cancelText: '取消' }
)
if (!action) return
const newName = await showPrompt('请输入新名称:', diary.name)
if (!newName || !newName.trim()) return
diary = { ...diary, name: newName.trim() }
}
const ok = await showConfirm(`将「${diary.name}」共享到公共配方库?`) const ok = await showConfirm(`将「${diary.name}」共享到公共配方库?`)
if (!ok) return if (!ok) return
try { try {
@@ -1257,7 +1277,7 @@ async function shareDiaryToPublic(diary) {
}), }),
}) })
if (res.ok) { if (res.ok) {
if (auth.isAdmin) { if (auth.isAdmin || auth.canManage) {
ui.showToast('已共享到公共配方库') ui.showToast('已共享到公共配方库')
} else { } else {
ui.showToast('已提交,等待管理员审核') ui.showToast('已提交,等待管理员审核')