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 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 53s
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 3s
PR Preview / test (pull_request) Successful in 4s
PR Preview / deploy-preview (pull_request) Successful in 14s
Test / e2e-test (push) Failing after 53s
- 去审核按钮仅管理员可见,其他用户显示已读
- 共享配方通知只发管理员
- 管理员待审核栏加"指派"按钮,选择高级编辑者审核
- 指派后发送通知给被指派人
- 新增 /api/recipes/{id}/assign-review 端点
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -781,15 +781,14 @@ 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 recipe_tags (recipe_id, tag_name) VALUES (?, ?)", (rid, tag))
|
||||
log_audit(conn, user["id"], "create_recipe", "recipe", rid, recipe.name)
|
||||
# Notify admin and senior editors when non-admin creates a recipe
|
||||
if user["role"] not in ("admin", "senior_editor"):
|
||||
# Notify admin only when non-admin creates a recipe
|
||||
if user["role"] != "admin":
|
||||
who = user.get("display_name") or user["username"]
|
||||
for role in ("admin", "senior_editor"):
|
||||
conn.execute(
|
||||
"INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)",
|
||||
(role, "📝 新配方待审核",
|
||||
f"{who} 共享了配方「{recipe.name}」,请到管理配方查看。\n[recipe_id:{rid}]")
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)",
|
||||
("admin", "📝 新配方待审核",
|
||||
f"{who} 共享了配方「{recipe.name}」,请到管理配方查看。\n[recipe_id:{rid}]")
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"id": rid}
|
||||
@@ -925,6 +924,31 @@ def reject_recipe(recipe_id: int, body: dict = None, user=Depends(require_role("
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/recipes/{recipe_id}/assign-review")
|
||||
def assign_recipe_review(recipe_id: int, body: dict, user=Depends(require_role("admin"))):
|
||||
reviewer_id = body.get("user_id")
|
||||
if not reviewer_id:
|
||||
raise HTTPException(400, "请选择审核人")
|
||||
conn = get_db()
|
||||
recipe = conn.execute("SELECT name FROM recipes WHERE id = ?", (recipe_id,)).fetchone()
|
||||
if not recipe:
|
||||
conn.close()
|
||||
raise HTTPException(404, "配方不存在")
|
||||
reviewer = conn.execute("SELECT role, display_name, username FROM users WHERE id = ?", (reviewer_id,)).fetchone()
|
||||
if not reviewer:
|
||||
conn.close()
|
||||
raise HTTPException(404, "用户不存在")
|
||||
conn.execute(
|
||||
"INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)",
|
||||
(reviewer["role"], "📋 请审核配方",
|
||||
f"管理员指派你审核配方「{recipe['name']}」,请到管理配方页面查看并反馈意见。\n[recipe_id:{recipe_id}]",
|
||||
reviewer_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/recipes/adopt-batch")
|
||||
def adopt_batch(body: dict, user=Depends(require_role("admin"))):
|
||||
ids = body.get("ids", [])
|
||||
|
||||
@@ -130,7 +130,7 @@ function isSearchMissing(n) {
|
||||
}
|
||||
|
||||
function isReviewable(n) {
|
||||
if (!n.title) return false
|
||||
if (!auth.isAdmin || !n.title) return false
|
||||
return n.title.includes('待审核') || n.title.includes('商业认证') || n.title.includes('申请')
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
<span class="pending-owner">{{ r._owner_name }}</span>
|
||||
<button class="btn-sm btn-approve" @click="approveRecipe(r)">通过</button>
|
||||
<button class="btn-sm btn-reject" @click="rejectRecipe(r)">拒绝</button>
|
||||
<button class="btn-sm btn-outline" @click="r._showAssign = !r._showAssign">指派</button>
|
||||
<div v-if="r._showAssign" class="assign-row">
|
||||
<select v-model="r._assignTo" class="assign-select">
|
||||
<option value="">选择审核人...</option>
|
||||
<option v-for="u in seniorEditors" :key="u.id" :value="u.id">{{ u.display_name || u.username }}</option>
|
||||
</select>
|
||||
<button class="btn-sm btn-primary" @click="assignReview(r)" :disabled="!r._assignTo">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -416,6 +424,7 @@ const editingRecipe = ref(null)
|
||||
const showPending = ref(false)
|
||||
const pendingRecipes = ref([])
|
||||
const pendingCount = ref(0)
|
||||
const seniorEditors = ref([])
|
||||
|
||||
// Form state
|
||||
const formName = ref('')
|
||||
@@ -1069,6 +1078,13 @@ onMounted(async () => {
|
||||
const res = await api('/api/recipe-reviews')
|
||||
if (res.ok) reviewHistory.value = await res.json()
|
||||
} catch {}
|
||||
try {
|
||||
const res = await api('/api/users')
|
||||
if (res.ok) {
|
||||
const users = await res.json()
|
||||
seniorEditors.value = users.filter(u => u.role === 'senior_editor')
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
// Open recipe editor if redirected from card view
|
||||
const editId = localStorage.getItem('oil_edit_recipe_id')
|
||||
@@ -1092,6 +1108,24 @@ function editDiaryRecipe(diary) {
|
||||
showAddOverlay.value = true
|
||||
}
|
||||
|
||||
async function assignReview(recipe) {
|
||||
const userId = recipe._assignTo
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await api('/api/recipes/' + recipe._id + '/assign-review', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ user_id: userId }),
|
||||
})
|
||||
if (res.ok) {
|
||||
recipe._showAssign = false
|
||||
recipe._assignTo = ''
|
||||
ui.showToast('已指派审核')
|
||||
}
|
||||
} catch {
|
||||
ui.showToast('指派失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openRecipeDetail(recipe) {
|
||||
const idx = recipeStore.recipes.findIndex(r => r._id === recipe._id)
|
||||
if (idx >= 0) previewRecipeIndex.value = idx
|
||||
@@ -1700,6 +1734,9 @@ watch(() => recipeStore.recipes, () => {
|
||||
padding: 0; margin-right: 4px; line-height: 1;
|
||||
}
|
||||
.mini-select.active { background: #4a9d7e; border-color: #4a9d7e; color: #fff; }
|
||||
.assign-row { display: flex; gap: 6px; align-items: center; margin-top: 4px; }
|
||||
.assign-select { padding: 4px 8px; border: 1px solid #d4cfc7; border-radius: 6px; font-size: 12px; font-family: inherit; }
|
||||
.btn-primary { background: linear-gradient(135deg, #7ec6a4, #4a9d7e); color: #fff; border: none; border-radius: 8px; cursor: pointer; font-family: inherit; }
|
||||
.tag-list-bar {
|
||||
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; padding: 8px 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user