修复问题,通过浏览器插件发布过的笔记后,在web页面的笔记的状态不会更改,还有一个问题,如果上传失败以后,笔记的状态就变成了在未审核页面的改成了失败,应该是在已审核页面中笔记列表的“使用状态”变成失败,可以批量将使用失败的笔记改为待发布的状态,已经发布成功的状态改为已使用。分析一下现在的流程,感觉有点混乱

This commit is contained in:
taiyi 2026-07-22 09:34:55 +08:00
parent 2a3eac2d49
commit 320e130ea5
6 changed files with 131 additions and 23 deletions

View File

@ -58,9 +58,9 @@ app.get('/api/stats', (req, res) => {
const shopCount = db.prepare('SELECT COUNT(*) as cnt FROM shops').get().cnt;
const productCount = db.prepare('SELECT COUNT(*) as cnt FROM products').get().cnt;
const noteCount = db.prepare('SELECT COUNT(*) as cnt FROM notes').get().cnt;
const publishedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'published'").get().cnt;
const publishedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE use_status = 'used'").get().cnt;
const pendingReviewCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'generated'").get().cnt;
const failedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'failed'").get().cnt;
const failedCount = db.prepare("SELECT COUNT(*) as cnt FROM notes WHERE status = 'failed' OR use_status = 'failed'").get().cnt;
res.json({ shops: shopCount, products: productCount, notes: noteCount, published: publishedCount, pendingReview: pendingReviewCount, failed: failedCount });
});
@ -78,7 +78,7 @@ app.use((req, res, next) => {
setInterval(() => {
try {
const stale = db.prepare(
"UPDATE notes SET status = 'approved', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')"
"UPDATE notes SET status = 'approved', use_status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')"
).run();
if (stale.changes > 0) {
console.log(`[定时任务] 释放了 ${stale.changes} 条超时的 publishing 笔记`);

View File

@ -37,6 +37,7 @@ function initDatabase() {
topics TEXT DEFAULT '[]',
image_paths TEXT DEFAULT '[]',
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'generating', 'generated', 'draft', 'approved', 'publishing', 'published', 'failed', 'rejected')),
use_status TEXT DEFAULT 'pending',
error_message TEXT DEFAULT '',
publish_result TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@ -67,12 +68,30 @@ function initDatabase() {
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS generation_jobs (
id TEXT PRIMARY KEY,
shop_id TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
total_products INTEGER DEFAULT 0,
total_notes INTEGER DEFAULT 0,
completed_notes INTEGER DEFAULT 0,
failed_notes INTEGER DEFAULT 0,
current_product_id TEXT DEFAULT NULL,
current_product_title TEXT DEFAULT '',
message TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_products_shop_id ON products(shop_id);
CREATE INDEX IF NOT EXISTS idx_notes_product_id ON notes(product_id);
CREATE INDEX IF NOT EXISTS idx_notes_shop_id ON notes(shop_id);
CREATE INDEX IF NOT EXISTS idx_notes_status ON notes(status);
CREATE INDEX IF NOT EXISTS idx_publish_tasks_status ON publish_tasks(status);
CREATE INDEX IF NOT EXISTS idx_publish_tasks_shop_id ON publish_tasks(shop_id);
CREATE INDEX IF NOT EXISTS idx_generation_jobs_status ON generation_jobs(status);
CREATE INDEX IF NOT EXISTS idx_generation_jobs_shop_id ON generation_jobs(shop_id);
CREATE TABLE IF NOT EXISTS image_hashes (
id TEXT PRIMARY KEY,
@ -90,12 +109,33 @@ function initDatabase() {
"ALTER TABLE notes ADD COLUMN image_prompt TEXT DEFAULT ''",
"ALTER TABLE notes ADD COLUMN worker_id TEXT DEFAULT NULL",
"ALTER TABLE notes ADD COLUMN claimed_at DATETIME DEFAULT NULL",
"ALTER TABLE notes ADD COLUMN use_status TEXT DEFAULT 'pending'",
"ALTER TABLE products ADD COLUMN reference_image TEXT DEFAULT ''",
];
for (const sql of migrate) {
try { db.exec(sql); } catch (e) { /* column already exists */ }
}
db.exec(`
UPDATE notes
SET use_status = 'used', status = 'approved'
WHERE status = 'published';
UPDATE notes
SET use_status = 'failed', status = 'approved'
WHERE status = 'failed'
AND EXISTS (
SELECT 1 FROM publish_tasks
WHERE publish_tasks.note_id = notes.id
AND publish_tasks.status = 'failed'
);
UPDATE notes
SET use_status = 'pending'
WHERE status IN ('approved', 'publishing')
AND (use_status IS NULL OR use_status = '');
`);
// Insert default configs
const insertConfig = db.prepare(`

View File

@ -122,7 +122,7 @@ function createNoteRoutes(db) {
return res.status(400).json({ error: 'ids array required' });
}
const placeholders = ids.map(() => '?').join(',');
db.prepare('UPDATE notes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('approved', ...ids);
db.prepare('UPDATE notes SET status = ?, use_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('approved', 'pending', ...ids);
const batchNotes = db.prepare('SELECT * FROM notes WHERE id IN (' + placeholders + ')').all(...ids);
for (const note of batchNotes) {
createPublishTask(note);
@ -136,21 +136,47 @@ function createNoteRoutes(db) {
return res.status(400).json({ error: 'ids array required' });
}
const placeholders = ids.map(() => '?').join(',');
db.prepare('UPDATE notes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('pending', ...ids);
db.prepare('UPDATE notes SET status = ?, use_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ')').run('pending', 'pending', ...ids);
res.json({ updated: ids.length });
});
router.post('/batch/publish-reset', (req, res) => {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'ids array required' });
}
const placeholders = ids.map(() => '?').join(',');
const resetNotes = db.prepare(
'SELECT * FROM notes WHERE id IN (' + placeholders + ") AND status = 'approved' AND use_status = 'failed'"
).all(...ids);
const reset = db.transaction(() => {
const result = db.prepare(
'UPDATE notes SET use_status = ?, error_message = ?, updated_at = CURRENT_TIMESTAMP WHERE id IN (' + placeholders + ") AND status = 'approved' AND use_status = 'failed'"
).run('pending', '', ...ids);
for (const note of resetNotes) {
createPublishTask({ ...note, use_status: 'pending' });
}
return result.changes;
});
res.json({ updated: reset() });
});
router.put('/:id', (req, res) => {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id);
if (!note) return res.status(404).json({ error: 'Note not found' });
const { title, content, topics, image_paths, status, image_prompt, worker_id } = req.body;
db.prepare(`UPDATE notes SET title=?, content=?, topics=?, image_paths=?, status=?, image_prompt=?, worker_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`)
const nextStatus = status || note.status;
const nextUseStatus = nextStatus === 'approved' && note.status !== 'approved' ? 'pending' : note.use_status;
db.prepare(`UPDATE notes SET title=?, content=?, topics=?, image_paths=?, status=?, use_status=?, image_prompt=?, worker_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?`)
.run(
title || note.title,
content !== undefined ? content : note.content,
topics ? JSON.stringify(topics) : note.topics,
image_paths ? JSON.stringify(image_paths) : note.image_paths,
status || note.status,
nextStatus,
nextUseStatus,
image_prompt !== undefined ? image_prompt : note.image_prompt,
worker_id !== undefined ? worker_id : note.worker_id,
req.params.id
@ -177,7 +203,7 @@ function createNoteRoutes(db) {
const product = db.prepare('SELECT * FROM products WHERE id = ?').get(note.product_id);
const productTitle = product?.title || note.title;
db.prepare("UPDATE notes SET status='generating', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(req.params.id);
db.prepare("UPDATE notes SET status='generating', use_status='pending', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(req.params.id);
// Cancel any pending publish tasks for this note
db.prepare("UPDATE publish_tasks SET status='failed', updated_at=CURRENT_TIMESTAMP WHERE note_id = ? AND status IN ('pending', 'claimed', 'executing')").run(req.params.id);
@ -225,7 +251,7 @@ function createNoteRoutes(db) {
router.post('/:id/complete', (req, res) => {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id);
if (!note) return res.status(404).json({ error: 'Note not found' });
db.prepare("UPDATE notes SET status='published', publish_result=?, published_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?")
db.prepare("UPDATE notes SET status='approved', use_status='used', publish_result=?, error_message='', published_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?")
.run(JSON.stringify(req.body.result || {}), req.params.id);
db.prepare("UPDATE publish_tasks SET status='completed', updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'")
.run(req.params.id);
@ -236,7 +262,7 @@ function createNoteRoutes(db) {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(req.params.id);
if (!note) return res.status(404).json({ error: 'Note not found' });
const { error_message } = req.body;
db.prepare("UPDATE notes SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE id=?")
db.prepare("UPDATE notes SET status='approved', use_status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE id=?")
.run(error_message || 'Unknown error', req.params.id);
db.prepare("UPDATE publish_tasks SET status='failed', error_message=?, updated_at=CURRENT_TIMESTAMP WHERE note_id=? AND status != 'completed'")
.run(error_message || 'Unknown error', req.params.id);

View File

@ -60,7 +60,7 @@ function createTaskRoutes(db) {
"UPDATE publish_tasks SET status = 'pending', claimed_by = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status IN ('claimed', 'executing') AND claimed_at < datetime('now', '-10 minutes')"
).run(shop_id);
db.prepare(
"UPDATE notes SET status = 'approved', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')"
"UPDATE notes SET status = 'approved', use_status = 'pending', worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE shop_id = ? AND status = 'publishing' AND claimed_at < datetime('now', '-10 minutes')"
).run(shop_id);
const claim = db.transaction(() => {
@ -72,7 +72,7 @@ function createTaskRoutes(db) {
"UPDATE publish_tasks SET status = 'claimed', claimed_by = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'"
).run(claimed_by, task.id);
if (result.changes === 0) return null;
db.prepare("UPDATE notes SET status = 'publishing', worker_id = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
db.prepare("UPDATE notes SET status = 'publishing', use_status = 'publishing', worker_id = ?, claimed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?")
.run(claimed_by, task.note_id);
return task;
});
@ -112,7 +112,7 @@ function createTaskRoutes(db) {
).run(req.params.id);
if (result.changes === 0) return false;
db.prepare(
"UPDATE notes SET status = 'published', publish_result = ?, published_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
"UPDATE notes SET status = 'approved', use_status = 'used', publish_result = ?, error_message = '', published_at = CURRENT_TIMESTAMP, worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
).run(JSON.stringify(req.body.result || {}), task.note_id);
return true;
});
@ -142,7 +142,7 @@ function createTaskRoutes(db) {
).run(errorMessage, req.params.id);
if (result.changes === 0) return false;
db.prepare(
"UPDATE notes SET status = 'failed', error_message = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
"UPDATE notes SET status = 'approved', use_status = 'failed', error_message = ?, worker_id = NULL, claimed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
).run(errorMessage, task.note_id);
return true;
});
@ -157,7 +157,7 @@ function createTaskRoutes(db) {
if (!shop_id) return res.status(400).json({ error: 'shop_id required' });
const approvedNotes = db.prepare(
"SELECT id, shop_id FROM notes WHERE shop_id = ? AND status = 'approved'"
"SELECT id, shop_id FROM notes WHERE shop_id = ? AND status = 'approved' AND use_status = 'pending'"
).all(shop_id);
if (approvedNotes.length === 0) {

View File

@ -46,6 +46,7 @@ export const notes = {
batchDelete: (ids) => request("/api/notes/batch-delete", { method: "POST", body: JSON.stringify({ ids }) }),
batchApprove: (ids) => request("/api/notes/batch/approve", { method: "POST", body: JSON.stringify({ ids }) }),
batchReject: (ids) => request("/api/notes/batch/reject", { method: "POST", body: JSON.stringify({ ids }) }),
batchResetPublish: (ids) => request("/api/notes/batch/publish-reset", { method: "POST", body: JSON.stringify({ ids }) }),
rewrite: (id) => request(`/api/notes/${id}/rewrite`, { method: "POST" }),
};
@ -64,6 +65,7 @@ export const generate = {
note: (data) => request("/api/generate/note", { method: "POST", body: JSON.stringify(data) }),
batch: (data) => request("/api/generate/batch", { method: "POST", body: JSON.stringify(data) }),
image: (data) => request("/api/generate/image", { method: "POST", body: JSON.stringify(data) }),
jobs: (shopId) => request(`/api/generate/jobs${shopId ? "?shop_id=" + shopId : ""}`),
};
export const stats = {

View File

@ -19,8 +19,27 @@ const STATUS_LABEL = {
};
const APPROVED_STATUSES = ["draft", "approved", "publishing", "published"];
const USE_STATUS_MAP = {
pending: "bg-gray-100 text-gray-600",
publishing: "bg-orange-100 text-orange-700",
used: "bg-emerald-100 text-emerald-800",
failed: "bg-red-100 text-red-700",
};
const USE_STATUS_LABEL = {
pending: "待发布",
publishing: "发布中",
used: "已使用",
failed: "使用失败",
};
const PAGE_SIZE = 10;
function getUseStatus(note) {
if (note.use_status) return note.use_status;
if (note.status === "published") return "used";
if (note.status === "publishing") return "publishing";
return "pending";
}
export default function NotesApproved() {
const [list, setList] = useState([]);
const [shopList, setShopList] = useState([]);
@ -57,7 +76,7 @@ export default function NotesApproved() {
const filteredList = useMemo(() => {
let result = list;
if (filterStatus) {
result = result.filter((n) => n.status === filterStatus);
result = result.filter((n) => getUseStatus(n) === filterStatus);
}
if (search.trim()) {
const q = search.trim().toLowerCase();
@ -107,12 +126,24 @@ export default function NotesApproved() {
} catch (e) { toast(e.message, "error"); }
};
const batchResetPublish = async () => {
if (selected.size === 0) { toast("请先选择笔记", "error"); return; }
if (!window.confirm("确定将选中的使用失败笔记重置为待发布吗?")) return;
try {
const result = await notes.batchResetPublish([...selected]);
toast(`已重置 ${result.updated || 0} 条使用失败笔记`, "success");
setSelected(new Set());
load();
} catch (e) { toast(e.message, "error"); }
};
const handleExport = () => {
const data = filteredList.map((n) => ({
红薯名称: shopList.find((s) => s.id === n.shop_id)?.shop_name || "",
标题: n.title,
正文内容: n.content,
热推话题: Array.isArray(n.topics) ? n.topics.join(", ") : n.topics,
使用状态: USE_STATUS_LABEL[getUseStatus(n)] || getUseStatus(n),
状态: STATUS_LABEL[n.status] || n.status,
创建时间: n.created_at,
更新时间: n.updated_at,
@ -152,11 +183,11 @@ export default function NotesApproved() {
</select>
<input className="border rounded-lg px-3 py-2 text-sm w-48" placeholder="商品标题" value={search} onChange={(e) => setSearch(e.target.value)} />
<select className="border rounded-lg px-3 py-2 text-sm" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value)}>
<option value="">全部状态</option>
<option value="generated">待审核</option>
<option value="approved">已通过</option>
<option value="published">已发布</option>
<option value="failed">失败</option>
<option value="">全部使用状态</option>
<option value="pending">待发布</option>
<option value="publishing">发布中</option>
<option value="used">已使用</option>
<option value="failed">使用失败</option>
</select>
<select className="border rounded-lg px-3 py-2 text-sm" value={sortMode} onChange={(e) => setSortMode(e.target.value)}>
<option value="">默认排序</option>
@ -165,6 +196,7 @@ export default function NotesApproved() {
</div>
<div className="flex items-center gap-2 mt-3">
<button onClick={batchDelete} disabled={selected.size === 0} className="px-3 py-1.5 bg-red-500 text-white text-sm rounded-lg hover:bg-red-600 disabled:opacity-40">删除</button>
<button onClick={batchResetPublish} disabled={selected.size === 0} className="px-3 py-1.5 bg-orange-500 text-white text-sm rounded-lg hover:bg-orange-600 disabled:opacity-40">使用失败改待发布</button>
<button onClick={batchRewrite} disabled={selected.size === 0} className="px-3 py-1.5 bg-blue-500 text-white text-sm rounded-lg hover:bg-blue-600 disabled:opacity-40">+ 一键重新生成笔记</button>
<button onClick={handleExport} className="px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600">导出</button>
</div>
@ -206,8 +238,16 @@ export default function NotesApproved() {
<button onClick={() => setImagePreview(n)} className="text-blue-600 hover:underline text-xs">查看图片({n.image_paths.length})</button>
) : <span className="text-gray-400 text-xs">暂无图片</span>}
</td>
<td className="px-3 py-3"><span className="text-xs text-gray-500">未使用</span></td>
<td className="px-3 py-3"><span className="text-xs text-green-600">正常</span></td>
<td className="px-3 py-3">
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${USE_STATUS_MAP[getUseStatus(n)] || "bg-gray-100 text-gray-600"}`}>
{USE_STATUS_LABEL[getUseStatus(n)] || getUseStatus(n)}
</span>
</td>
<td className="px-3 py-3">
<span className={`text-xs ${getUseStatus(n) === "failed" ? "text-red-600" : "text-green-600"}`}>
{getUseStatus(n) === "failed" ? "失败" : "正常"}
</span>
</td>
<td className="px-3 py-3 text-gray-400 text-xs whitespace-nowrap">{n.created_at?.slice(0, 19).replace("T", " ")}</td>
<td className="px-3 py-3 text-gray-400 text-xs whitespace-nowrap">{n.updated_at?.slice(0, 19).replace("T", " ")}</td>
<td className="px-3 py-3 space-x-2 whitespace-nowrap">