feat(review): 重构项目级单方案验收协作
This commit is contained in:
105
api/db.ts
105
api/db.ts
@@ -102,6 +102,8 @@ db.exec(`
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
client_description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
review_status TEXT NOT NULL DEFAULT 'draft',
|
||||
review_completed_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS collections (
|
||||
@@ -116,6 +118,7 @@ db.exec(`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER,
|
||||
external_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
@@ -136,16 +139,23 @@ db.exec(`
|
||||
x REAL NOT NULL,
|
||||
y REAL NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_role TEXT NOT NULL DEFAULT 'client',
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
closure_reason TEXT NOT NULL DEFAULT '',
|
||||
withdrawn_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS work_comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
note_id INTEGER NOT NULL,
|
||||
version_number INTEGER NOT NULL DEFAULT 1,
|
||||
content TEXT NOT NULL,
|
||||
author_name TEXT NOT NULL DEFAULT '客户',
|
||||
author_role TEXT NOT NULL DEFAULT 'client',
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
closure_reason TEXT NOT NULL DEFAULT '',
|
||||
withdrawn_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -154,9 +164,30 @@ db.exec(`
|
||||
note_id INTEGER NOT NULL,
|
||||
version_number INTEGER NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
start_offset INTEGER NOT NULL DEFAULT 0,
|
||||
end_offset INTEGER NOT NULL DEFAULT 0,
|
||||
selected_text TEXT NOT NULL DEFAULT '',
|
||||
prefix_text TEXT NOT NULL DEFAULT '',
|
||||
suffix_text TEXT NOT NULL DEFAULT '',
|
||||
content TEXT NOT NULL,
|
||||
author_name TEXT NOT NULL DEFAULT '客户',
|
||||
author_role TEXT NOT NULL DEFAULT 'client',
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
closure_reason TEXT NOT NULL DEFAULT '',
|
||||
withdrawn_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS feedback_replies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
note_id INTEGER NOT NULL,
|
||||
version_number INTEGER NOT NULL,
|
||||
feedback_type TEXT NOT NULL,
|
||||
feedback_id INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_name TEXT NOT NULL,
|
||||
author_role TEXT NOT NULL,
|
||||
withdrawn_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -182,6 +213,7 @@ db.exec(`
|
||||
selected_version_number INTEGER,
|
||||
created_by INTEGER,
|
||||
completed_at TEXT,
|
||||
completion_reason TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(note_id, round_number),
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
|
||||
@@ -211,6 +243,7 @@ function addColumn(table: string, definition: string) {
|
||||
}
|
||||
|
||||
addColumn('notes', "collection_id INTEGER");
|
||||
addColumn('notes', 'project_id INTEGER');
|
||||
addColumn('notes', 'external_id TEXT');
|
||||
addColumn('notes', "tags TEXT NOT NULL DEFAULT '[]'");
|
||||
addColumn('notes', "review_status TEXT NOT NULL DEFAULT 'pending'");
|
||||
@@ -219,7 +252,12 @@ addColumn('notes', 'active_round_id INTEGER');
|
||||
addColumn('notes', 'approved_version_number INTEGER');
|
||||
addColumn('annotations', "author_name TEXT NOT NULL DEFAULT '客户'");
|
||||
addColumn('annotations', "status TEXT NOT NULL DEFAULT 'open'");
|
||||
addColumn('annotations', "author_role TEXT NOT NULL DEFAULT 'client'");
|
||||
addColumn('annotations', 'withdrawn_at TEXT');
|
||||
addColumn('annotations', "closure_reason TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('projects', 'group_id INTEGER');
|
||||
addColumn('projects', "review_status TEXT NOT NULL DEFAULT 'draft'");
|
||||
addColumn('projects', 'review_completed_at TEXT');
|
||||
addColumn('projects', "access_password_hash TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('projects', 'customer_access_enabled INTEGER NOT NULL DEFAULT 0');
|
||||
addColumn('projects', 'access_expires_at TEXT');
|
||||
@@ -228,9 +266,21 @@ addColumn('images', "storage_key TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('images', 'version_number INTEGER NOT NULL DEFAULT 1');
|
||||
addColumn('users', 'last_login_at TEXT');
|
||||
addColumn('collections', 'completed_at TEXT');
|
||||
addColumn('work_comments', 'version_number INTEGER NOT NULL DEFAULT 1');
|
||||
addColumn('work_comments', 'withdrawn_at TEXT');
|
||||
addColumn('work_comments', "closure_reason TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('text_annotations', 'start_offset INTEGER NOT NULL DEFAULT 0');
|
||||
addColumn('text_annotations', 'end_offset INTEGER NOT NULL DEFAULT 0');
|
||||
addColumn('text_annotations', "selected_text TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('text_annotations', "prefix_text TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('text_annotations', "suffix_text TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('text_annotations', "author_role TEXT NOT NULL DEFAULT 'client'");
|
||||
addColumn('text_annotations', 'withdrawn_at TEXT');
|
||||
addColumn('text_annotations', "closure_reason TEXT NOT NULL DEFAULT ''");
|
||||
addColumn('work_versions', 'review_round_id INTEGER');
|
||||
addColumn('work_versions', "candidate_name TEXT NOT NULL DEFAULT '方案 A'");
|
||||
addColumn('work_versions', "candidate_status TEXT NOT NULL DEFAULT 'pending'");
|
||||
addColumn('review_rounds', "completion_reason TEXT NOT NULL DEFAULT ''");
|
||||
|
||||
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_collection_external_id ON notes(collection_id, external_id) WHERE external_id IS NOT NULL AND external_id != ''");
|
||||
|
||||
@@ -270,6 +320,8 @@ if (!collectionId) {
|
||||
}
|
||||
db.prepare('UPDATE notes SET collection_id = ? WHERE collection_id IS NULL').run(collectionId);
|
||||
db.prepare('UPDATE projects SET group_id = ? WHERE group_id IS NULL').run(groupId);
|
||||
db.prepare('UPDATE notes SET project_id = (SELECT c.project_id FROM collections c WHERE c.id = notes.collection_id) WHERE project_id IS NULL').run();
|
||||
db.prepare('UPDATE work_comments SET version_number = (SELECT n.version_number FROM notes n WHERE n.id = work_comments.note_id) WHERE version_number IS NULL OR version_number < 1').run();
|
||||
db.prepare(`UPDATE collections SET name = '2026 年 7 月任务', client_description = '本月内容作品交付集'
|
||||
WHERE project_id = (SELECT id FROM projects WHERE slug = 'light-notes') AND (name LIKE '%?%' OR client_description LIKE '%?%')`).run();
|
||||
db.prepare(`INSERT OR IGNORE INTO work_versions (note_id, version_number, title, description, tags, review_status)
|
||||
@@ -300,6 +352,22 @@ db.exec(`
|
||||
approved_version_number = (SELECT MAX(v.version_number) FROM work_versions v WHERE v.note_id = notes.id AND v.review_status = 'approved')
|
||||
WHERE active_round_id IS NULL;
|
||||
`);
|
||||
db.exec(`
|
||||
UPDATE projects
|
||||
SET review_status = CASE
|
||||
WHEN status = 'archived' THEN 'archived'
|
||||
WHEN NOT EXISTS (SELECT 1 FROM notes n WHERE n.project_id = projects.id AND n.review_status != 'draft') THEN 'draft'
|
||||
WHEN NOT EXISTS (SELECT 1 FROM notes n WHERE n.project_id = projects.id AND n.review_status != 'draft' AND n.review_status != 'approved') THEN 'completed'
|
||||
ELSE 'reviewing'
|
||||
END,
|
||||
review_completed_at = CASE
|
||||
WHEN status != 'archived'
|
||||
AND EXISTS (SELECT 1 FROM notes n WHERE n.project_id = projects.id AND n.review_status != 'draft')
|
||||
AND NOT EXISTS (SELECT 1 FROM notes n WHERE n.project_id = projects.id AND n.review_status != 'draft' AND n.review_status != 'approved')
|
||||
THEN COALESCE(review_completed_at, datetime('now'))
|
||||
ELSE NULL
|
||||
END;
|
||||
`);
|
||||
db.exec(`
|
||||
UPDATE collections
|
||||
SET status = CASE
|
||||
@@ -319,10 +387,12 @@ db.exec(`
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_collections_project_id ON collections(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_collection_id ON notes(collection_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_project_id ON notes(project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_images_note_id ON images(note_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_annotations_image_id ON annotations(image_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_note_id ON work_comments(note_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_text_annotations_note_id ON text_annotations(note_id, version_number);
|
||||
CREATE INDEX IF NOT EXISTS idx_feedback_replies_target ON feedback_replies(note_id, version_number, feedback_type, feedback_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_customer_sessions_token_hash ON customer_sessions(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_customer_sessions_project_id ON customer_sessions(project_id);
|
||||
@@ -336,4 +406,39 @@ db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_review_events_note_id ON review_events(note_id, version_number);
|
||||
`);
|
||||
|
||||
export function repairMultiSchemeRounds() {
|
||||
db.transaction(() => {
|
||||
const duplicates = db.prepare(`SELECT review_round_id, note_id FROM work_versions WHERE review_round_id IS NOT NULL GROUP BY review_round_id, note_id HAVING COUNT(*) > 1`).all() as Array<{ review_round_id: number; note_id: number }>;
|
||||
for (const duplicate of duplicates) {
|
||||
const round = db.prepare('SELECT * FROM review_rounds WHERE id=?').get(duplicate.review_round_id) as Record<string, unknown> | undefined;
|
||||
const note = db.prepare('SELECT active_round_id,version_number FROM notes WHERE id=?').get(duplicate.note_id) as { active_round_id: number | null; version_number: number } | undefined;
|
||||
const versions = db.prepare('SELECT version_number FROM work_versions WHERE review_round_id=? ORDER BY version_number').all(duplicate.review_round_id) as Array<{ version_number: number }>;
|
||||
if (!round || !note || versions.length < 2) continue;
|
||||
const keeper = versions.some((item) => Number(item.version_number) === Number(note.version_number)) ? Number(note.version_number) : Number(versions[0].version_number);
|
||||
for (const version of versions.filter((item) => Number(item.version_number) !== keeper)) {
|
||||
const nextRound = Number((db.prepare('SELECT COALESCE(MAX(round_number),0)+1 AS value FROM review_rounds WHERE note_id=?').get(duplicate.note_id) as { value: number }).value);
|
||||
const remainsActive = Number(note.active_round_id) === Number(duplicate.review_round_id) && Number(note.version_number) === Number(version.version_number);
|
||||
const result = db.prepare(`INSERT INTO review_rounds (note_id,round_number,status,selected_version_number,completed_at,created_by,created_at,completion_reason)
|
||||
VALUES (?,?,?,?,?,?,?,?)`).run(
|
||||
duplicate.note_id,
|
||||
nextRound,
|
||||
remainsActive ? round.status : 'completed',
|
||||
Number(round.selected_version_number) === Number(version.version_number) ? version.version_number : null,
|
||||
remainsActive ? round.completed_at : (round.completed_at || new Date().toISOString()),
|
||||
round.created_by ?? null,
|
||||
round.created_at,
|
||||
remainsActive ? round.completion_reason : (round.completion_reason || 'migrated_single_scheme'),
|
||||
);
|
||||
const newRoundId = Number(result.lastInsertRowid);
|
||||
db.prepare('UPDATE work_versions SET review_round_id=? WHERE note_id=? AND version_number=?').run(newRoundId, duplicate.note_id, version.version_number);
|
||||
if (remainsActive) db.prepare('UPDATE notes SET active_round_id=? WHERE id=?').run(newRoundId, duplicate.note_id);
|
||||
}
|
||||
if (Number(round.selected_version_number) !== keeper) db.prepare('UPDATE review_rounds SET selected_version_number=NULL WHERE id=?').run(duplicate.review_round_id);
|
||||
}
|
||||
})();
|
||||
}
|
||||
repairMultiSchemeRounds();
|
||||
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_project_external_id ON notes(project_id, external_id) WHERE external_id IS NOT NULL AND external_id != ''");
|
||||
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_work_versions_one_per_round ON work_versions(review_round_id) WHERE review_round_id IS NOT NULL');
|
||||
|
||||
export default db;
|
||||
|
||||
Reference in New Issue
Block a user