初始提交:全国记者站管理系统

This commit is contained in:
selfrelease
2026-08-01 23:09:49 +08:00
commit 45fbba0308
96 changed files with 21514 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
-- Migration: 001_init_schema
-- Description: 初始化基础表结构(从 server/index.js 迁移)
-- Date: 2026-08-01
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS schema_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS work_records (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
reporter TEXT NOT NULL,
station TEXT NOT NULL,
occurred_date TEXT NOT NULL,
platform TEXT NOT NULL,
status TEXT NOT NULL,
score INTEGER,
description TEXT,
review_note TEXT,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
record_id TEXT NOT NULL REFERENCES work_records(id),
actor_role TEXT NOT NULL,
actor_name TEXT NOT NULL,
action TEXT NOT NULL,
from_status TEXT,
to_status TEXT NOT NULL,
score INTEGER,
note TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
+14
View File
@@ -0,0 +1,14 @@
-- Migration: 002_seed_data
-- Description: 初始种子数据(7 条工作记录)
-- Date: 2026-08-01
INSERT OR IGNORE INTO work_records
(id, title, type, reporter, station, occurred_date, platform, status, score, description, review_note, updated_at)
VALUES
('WK-202607-086', '暑运客流持续攀升,多部门保障出行', '文字稿件', '林晓', '北京记者站', '2026-07-30', '全国日报', 'station_review', NULL, NULL, NULL, '2026-07-31 09:42:00'),
('WK-202607-081', '老街更新:城市记忆与新消费共生', '视频供稿', '周宁', '北京记者站', '2026-07-29', '新闻客户端', 'headquarters_review', 8, NULL, NULL, '2026-07-30 17:26:00'),
('WK-202607-074', '长三角一体化重点项目集中签约', '重要报道', '陈屿', '上海记者站', '2026-07-28', '全国日报', 'headquarters_review', 12, NULL, NULL, '2026-07-29 15:18:00'),
('WK-202607-063', '县域公共文化服务观察', '图片供稿', '许言', '浙江记者站', '2026-07-26', '新闻周刊', 'returned', NULL, NULL, '请补充刊发版面截图,并核对发布日期。', '2026-07-28 11:05:00'),
('WK-202607-052', '防汛一线应急响应纪实', '文字稿件', '方澄', '广东记者站', '2026-07-24', '全国日报', 'archived', 10, NULL, NULL, '2026-07-26 16:31:00'),
('WK-202607-041', '融合报道生产能力专题培训', '培训参与', '林晓', '北京记者站', '2026-07-22', '总部培训中心', 'archived', 3, NULL, NULL, '2026-07-23 10:20:00'),
('WK-202607-032', '社区养老服务站走访', '文字稿件', '周宁', '北京记者站', '2026-07-18', '新闻客户端', 'draft', NULL, NULL, NULL, '2026-07-18 18:42:00');
+28
View File
@@ -0,0 +1,28 @@
-- Migration: 003_create_people
-- Description: 人员管理表
-- Date: 2026-08-01
CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
station TEXT NOT NULL,
title TEXT,
phone TEXT,
joined_at TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
INSERT OR IGNORE INTO people
(code, name, station, title, phone, joined_at, status)
VALUES
('PERSON_001', '林晓', '北京记者站', '记者', '13800001001', '2024-03-01', 'active'),
('PERSON_002', '周宁', '北京记者站', '记者', '13800001002', '2024-05-15', 'active'),
('PERSON_003', '陈屿', '上海记者站', '记者', '13900001001', '2023-09-01', 'active'),
('PERSON_004', '许言', '浙江记者站', '记者', '13900001002', '2024-01-10', 'active'),
('PERSON_005', '方澄', '广东记者站', '记者', '13900001003', '2023-11-20', 'active'),
('PERSON_006', '苏明远', '北京记者站', '分站负责人', '13800001003', '2023-01-01', 'active'),
('PERSON_007', '林致远', '总部', '总部管理员', '13800001000', '2022-01-01', 'active'),
('PERSON_008', '赵雨薇', '北京记者站', '记者', '13800001004', '2025-02-01', 'inactive');
+27
View File
@@ -0,0 +1,27 @@
-- Migration: 004_create_stations
-- Description: 记者站管理表
-- Date: 2026-08-01
CREATE TABLE IF NOT EXISTS stations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
region TEXT,
address TEXT,
leader TEXT,
phone TEXT,
status TEXT NOT NULL DEFAULT 'active',
established_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
INSERT OR IGNORE INTO stations
(code, name, region, address, leader, phone, status, established_at)
VALUES
('STATION_BJ', '北京记者站', '华北', '北京市朝阳区', '苏明远', '010-12345678', 'active', '2020-01-01'),
('STATION_SH', '上海记者站', '华东', '上海市浦东新区', '李华东', '021-12345678', 'active', '2020-01-01'),
('STATION_ZJ', '浙江记者站', '华东', '杭州市西湖区', '张浙江', '0571-12345678', 'active', '2021-03-01'),
('STATION_GD', '广东记者站', '华南', '广州市天河区', '王广东', '020-12345678', 'active', '2021-06-01'),
('STATION_SC', '四川记者站', '西南', '成都市武侯区', '刘四川', '028-12345678', 'active', '2022-01-01'),
('STATION_HB', '湖北记者站', '华中', '武汉市武昌区', '陈湖北', '027-12345678', 'inactive', '2022-06-01');
+38
View File
@@ -0,0 +1,38 @@
-- Migration: 005_create_notices
-- Description: 通知公告及回执表
-- Date: 2026-08-01
CREATE TABLE IF NOT EXISTS notices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
content TEXT,
priority TEXT NOT NULL DEFAULT 'normal',
scope TEXT NOT NULL DEFAULT 'all',
stations TEXT,
roles TEXT,
attachment TEXT,
published_by TEXT NOT NULL,
published_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS notice_receipts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notice_id INTEGER NOT NULL REFERENCES notices(id),
receiver_name TEXT NOT NULL,
receiver_role TEXT NOT NULL,
read INTEGER NOT NULL DEFAULT 0,
confirmed INTEGER NOT NULL DEFAULT 0,
read_at TEXT,
confirmed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
INSERT OR IGNORE INTO notices
(code, title, content, priority, scope, published_by, published_at)
VALUES
('NOTICE_20260801_001', '关于开展第三季度考核工作的通知', '各记者站请于8月15日前完成第三季度工作记录提交,总部将在8月底完成评分。', 'high', 'all', '林致远', '2026-08-01 09:00:00'),
('NOTICE_20260801_002', '记者站年度考核评分标准调整说明', '自本通知发布之日起,新提交的稿件将按更新后的评分标准执行。详见附件。', 'normal', 'station', '林致远', '2026-08-01 10:30:00'),
('NOTICE_20260720_001', '暑期安全生产宣传报道提示', '请各站注意收集暑期出行、安全生产等相关素材,及时投稿。', 'normal', 'all', '林致远', '2026-07-20 14:00:00');
+20
View File
@@ -0,0 +1,20 @@
-- Migration: 006_optimize_indexes
-- Description: 索引优化(audit_logs 表)
-- Date: 2026-08-01
CREATE INDEX IF NOT EXISTS idx_audit_record_id ON audit_logs(record_id);
CREATE INDEX IF NOT EXISTS idx_audit_actor_role ON audit_logs(actor_role);
CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_work_station ON work_records(station);
CREATE INDEX IF NOT EXISTS idx_work_reporter ON work_records(reporter);
CREATE INDEX IF NOT EXISTS idx_work_status ON work_records(status);
CREATE INDEX IF NOT EXISTS idx_work_updated_at ON work_records(updated_at);
CREATE INDEX IF NOT EXISTS idx_people_station ON people(station);
CREATE INDEX IF NOT EXISTS idx_people_status ON people(status);
CREATE INDEX IF NOT EXISTS idx_stations_region ON stations(region);
CREATE INDEX IF NOT EXISTS idx_stations_status ON stations(status);
CREATE INDEX IF NOT EXISTS idx_notice_receipt_notice ON notice_receipts(notice_id);
+42
View File
@@ -0,0 +1,42 @@
-- Migration: 007_create_rules
-- Description: 考核规则及指标项表
-- Date: 2026-08-01
-- Refs: pmdocs/changes/2026-08-01-001-V0.2考核规则配置.md
CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
period_type TEXT NOT NULL,
period_start TEXT,
period_end TEXT,
status TEXT NOT NULL DEFAULT 'draft',
version INTEGER NOT NULL DEFAULT 1,
parent_id INTEGER REFERENCES rules(id),
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS rule_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id INTEGER NOT NULL REFERENCES rules(id) ON DELETE CASCADE,
category TEXT NOT NULL,
name TEXT NOT NULL,
metric_key TEXT NOT NULL,
weight REAL NOT NULL,
min_score REAL NOT NULL DEFAULT 0,
max_score REAL NOT NULL DEFAULT 100,
formula_type TEXT NOT NULL,
formula_params TEXT,
display_order INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_rules_status ON rules(status);
CREATE INDEX IF NOT EXISTS idx_rules_period ON rules(period_start, period_end);
CREATE INDEX IF NOT EXISTS idx_rule_items_rule ON rule_items(rule_id);
CREATE INDEX IF NOT EXISTS idx_rule_items_category ON rule_items(category);
+29
View File
@@ -0,0 +1,29 @@
-- Migration: 008_create_scores
-- Description: 考核评分结果表
-- Date: 2026-08-01
-- Refs: pmdocs/changes/2026-08-01-001-V0.2考核规则配置.md
CREATE TABLE IF NOT EXISTS scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
rule_id INTEGER NOT NULL REFERENCES rules(id),
reporter TEXT NOT NULL,
station TEXT NOT NULL,
period TEXT NOT NULL,
period_type TEXT NOT NULL,
total_score REAL,
quality_score REAL,
quantity_score REAL,
efficiency_score REAL,
compliance_score REAL,
item_details TEXT,
computed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_scores_reporter ON scores(reporter);
CREATE INDEX IF NOT EXISTS idx_scores_station ON scores(station);
CREATE INDEX IF NOT EXISTS idx_scores_period ON scores(period);
CREATE INDEX IF NOT EXISTS idx_scores_rule ON scores(rule_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_scores_unique ON scores(reporter, station, period);
+95
View File
@@ -0,0 +1,95 @@
-- Migration: 009_seed_default_rules
-- Description: 插入默认考核规则模板
-- Date: 2026-08-01
-- Refs: pmdocs/changes/2026-08-01-001-V0.2考核规则配置.md
-- 默认考核规则模板(草稿状态,待总部激活)
INSERT OR IGNORE INTO rules
(code, name, description, period_type, period_start, period_end, status, version, created_by)
VALUES
('RULE_2026_Q3', '2026年第三季度考核规则', '第三季度综合考核,包含数量、质量、时效、合规四个维度', 'quarterly', '2026-07-01', '2026-09-30', 'draft', 1, '林致远');
-- 默认指标项(6个,覆盖四维度,权重之和为1.0)
INSERT OR IGNORE INTO rule_items
(rule_id, category, name, metric_key, weight, min_score, max_score, formula_type, formula_params, display_order)
SELECT
r.id,
'quantity',
'原创报道数量',
'count_total',
0.20,
0, 100,
'count',
'{"threshold": 10, "base_score": 60, "bonus_per_item": 4, "max_bonus": 20}',
1
FROM rules r WHERE r.code = 'RULE_2026_Q3';
INSERT OR IGNORE INTO rule_items
(rule_id, category, name, metric_key, weight, min_score, max_score, formula_type, formula_params, display_order)
SELECT
r.id,
'quantity',
'视频供稿数量',
'count_video',
0.10,
0, 100,
'count',
'{"threshold": 5, "base_score": 60, "bonus_per_item": 5, "max_bonus": 20}',
2
FROM rules r WHERE r.code = 'RULE_2026_Q3';
INSERT OR IGNORE INTO rule_items
(rule_id, category, name, metric_key, weight, min_score, max_score, formula_type, formula_params, display_order)
SELECT
r.id,
'quality',
'平均审核得分',
'avg_score',
0.25,
0, 100,
'avg_score',
'{}',
3
FROM rules r WHERE r.code = 'RULE_2026_Q3';
INSERT OR IGNORE INTO rule_items
(rule_id, category, name, metric_key, weight, min_score, max_score, formula_type, formula_params, display_order)
SELECT
r.id,
'quality',
'原创稿件占比',
'original_ratio',
0.15,
0, 100,
'rate',
'{"target": 0.6, "base_score": 60, "bonus_per_pct": 40}',
4
FROM rules r WHERE r.code = 'RULE_2026_Q3';
INSERT OR IGNORE INTO rule_items
(rule_id, category, name, metric_key, weight, min_score, max_score, formula_type, formula_params, display_order)
SELECT
r.id,
'efficiency',
'按时提交率',
'on_time_rate',
0.15,
0, 100,
'rate',
'{"target": 0.9, "base_score": 60, "bonus_per_pct": 40}',
5
FROM rules r WHERE r.code = 'RULE_2026_Q3';
INSERT OR IGNORE INTO rule_items
(rule_id, category, name, metric_key, weight, min_score, max_score, formula_type, formula_params, display_order)
SELECT
r.id,
'compliance',
'审核通过率',
'pass_rate',
0.15,
0, 100,
'rate',
'{"target": 0.85, "base_score": 60, "bonus_per_pct": 40}',
6
FROM rules r WHERE r.code = 'RULE_2026_Q3';
+9
View File
@@ -0,0 +1,9 @@
-- Migration: 010_add_soft_delete
-- Description: 为人员、站点、工作记录添加逻辑删除字段
-- Date: 2026-08-01
ALTER TABLE people ADD COLUMN deleted_at TEXT;
ALTER TABLE stations ADD COLUMN deleted_at TEXT;
ALTER TABLE work_records ADD COLUMN deleted_at TEXT;
ALTER TABLE work_records ADD COLUMN attachments TEXT;
ALTER TABLE work_records ADD COLUMN created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'));
+32
View File
@@ -0,0 +1,32 @@
-- Migration: 011_add_auth_and_audit
-- Description: 登录会话表 + 系统操作日志表(全覆盖审计)
-- Date: 2026-08-01
CREATE TABLE IF NOT EXISTS login_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
user_code TEXT NOT NULL,
user_name TEXT NOT NULL,
role TEXT NOT NULL,
station TEXT,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
revoked_at TEXT
);
CREATE TABLE IF NOT EXISTS system_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_role TEXT NOT NULL,
actor_name TEXT NOT NULL,
module TEXT NOT NULL,
action TEXT NOT NULL,
target_type TEXT,
target_id TEXT,
detail TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_login_sessions_token ON login_sessions(token);
CREATE INDEX IF NOT EXISTS idx_system_logs_actor ON system_logs(actor_name);
CREATE INDEX IF NOT EXISTS idx_system_logs_module ON system_logs(module);
+30
View File
@@ -0,0 +1,30 @@
-- Migration: 012_add_record_extras
-- Description: 工作记录版本历史表 + 人员调站历史表
-- Date: 2026-08-01
CREATE TABLE IF NOT EXISTS record_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
record_id TEXT NOT NULL REFERENCES work_records(id),
version_no INTEGER NOT NULL,
title TEXT,
type TEXT,
platform TEXT,
description TEXT,
attachments TEXT,
edited_by TEXT NOT NULL,
edited_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_record_versions_record ON record_versions(record_id);
CREATE TABLE IF NOT EXISTS person_transfers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
person_id INTEGER NOT NULL REFERENCES people(id),
from_station TEXT NOT NULL,
to_station TEXT NOT NULL,
reason TEXT,
operated_by TEXT NOT NULL,
transferred_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE INDEX IF NOT EXISTS idx_person_transfers_person ON person_transfers(person_id);
+23
View File
@@ -0,0 +1,23 @@
-- Migration: 013_add_appeals
-- Description: 申诉复议表
-- Date: 2026-08-01
CREATE TABLE IF NOT EXISTS appeals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
record_id TEXT NOT NULL REFERENCES work_records(id),
appellant TEXT NOT NULL,
station TEXT NOT NULL,
reason TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
handler TEXT,
handler_role TEXT,
response TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
handled_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_appeals_record ON appeals(record_id);
CREATE INDEX IF NOT EXISTS idx_appeals_status ON appeals(status);
CREATE INDEX IF NOT EXISTS idx_appeals_appellant ON appeals(appellant);
+7
View File
@@ -0,0 +1,7 @@
-- Migration: 014_add_notice_extras
-- Description: 通知公告增加定时发布和撤回字段
-- Date: 2026-08-01
ALTER TABLE notices ADD COLUMN scheduled_at TEXT;
ALTER TABLE notices ADD COLUMN withdrawn_at TEXT;
ALTER TABLE notices ADD COLUMN status TEXT NOT NULL DEFAULT 'published';
+27
View File
@@ -0,0 +1,27 @@
-- Migration: 015_seed_system_logs
-- Description: 插入系统操作日志示例数据
-- Date: 2026-08-01
INSERT INTO system_logs (actor_role, actor_name, module, action, target_type, target_id, detail, ip_address, created_at) VALUES
('headquarters', '林致远', 'auth', 'login', NULL, NULL, '总部管理员登录系统', '127.0.0.1', datetime('now', 'localtime', '-3 hours')),
('reporter', '林晓', 'record', 'create', 'record', 'R2026_001', '创建工作记录《北京暴雨直击》', '127.0.0.1', datetime('now', 'localtime', '-170 minutes')),
('reporter', '林晓', 'record', 'save_draft', 'record', 'R2026_002', '保存草稿《上海地铁新线开通》', '127.0.0.1', datetime('now', 'localtime', '-165 minutes')),
('station', '苏明远', 'record', 'pass', 'record', 'R2026_001', '审核通过《北京暴雨直击》', '127.0.0.1', datetime('now', 'localtime', '-150 minutes')),
('station', '苏明远', 'record', 'return', 'record', 'R2026_003', '审核退回《广州城中村改造》', '127.0.0.1', datetime('now', 'localtime', '-140 minutes')),
('headquarters', '林致远', 'people', 'create', 'person', 'PERSON_011', '新增记者赵雨晴', '127.0.0.1', datetime('now', 'localtime', '-120 minutes')),
('headquarters', '林致远', 'people', 'update', 'person', 'PERSON_003', '更新记者联系方式', '127.0.0.1', datetime('now', 'localtime', '-110 minutes')),
('headquarters', '林致远', 'people', 'transfer', 'person', 'PERSON_005', '从北京记者站调入上海记者站', '127.0.0.1', datetime('now', 'localtime', '-100 minutes')),
('station', '苏明远', 'notices', 'create', 'notice', 'N006', '发布通知《关于第三季度考核安排》', '127.0.0.1', datetime('now', 'localtime', '-90 minutes')),
('reporter', '林晓', 'appeals', 'create', 'appeal', 'APPEAL_003', '对评分结果提出申诉', '127.0.0.1', datetime('now', 'localtime', '-80 minutes')),
('headquarters', '林致远', 'appeals', 'uphold', 'appeal', 'APPEAL_001', '驳回申诉 APPEAL_001', '127.0.0.1', datetime('now', 'localtime', '-70 minutes')),
('headquarters', '林致远', 'appeals', 'overturn', 'appeal', 'APPEAL_002', '通过申诉 APPEAL_002,调整评分', '127.0.0.1', datetime('now', 'localtime', '-65 minutes')),
('headquarters', '林致远', 'export', 'records', NULL, NULL, '导出工作记录 CSV,共 156 条', '127.0.0.1', datetime('now', 'localtime', '-50 minutes')),
('headquarters', '林致远', 'export', 'scores', NULL, NULL, '导出评分结果 CSV,共 42 条', '127.0.0.1', datetime('now', 'localtime', '-45 minutes')),
('headquarters', '林致远', 'export', 'people', NULL, NULL, '导出人员名单 CSV,共 28 条', '127.0.0.1', datetime('now', 'localtime', '-40 minutes')),
('reporter', '林晓', 'record', 'upload_attachment', 'record', 'R2026_004', '上传附件《现场照片.zip》', '127.0.0.1', datetime('now', 'localtime', '-35 minutes')),
('station', '苏明远', 'record', 'withdraw', 'record', 'R2026_002', '撤回工作记录《上海地铁新线开通》', '127.0.0.1', datetime('now', 'localtime', '-30 minutes')),
('headquarters', '林致远', 'auth', 'logout', NULL, NULL, '总部管理员退出系统', '127.0.0.1', datetime('now', 'localtime', '-20 minutes')),
('reporter', '林晓', 'auth', 'login', NULL, NULL, '记者林晓登录系统', '127.0.0.1', datetime('now', 'localtime', '-15 minutes')),
('station', '苏明远', 'auth', 'login', NULL, NULL, '分站负责人苏明远登录系统', '127.0.0.1', datetime('now', 'localtime', '-10 minutes')),
('headquarters', '林致远', 'people', 'delete', 'person', 'PERSON_012', '删除已离职记者记录', '127.0.0.1', datetime('now', 'localtime', '-5 minutes')),
('headquarters', '林致远', 'stations', 'update', 'station', 'ST_003', '更新广州记者站信息', '127.0.0.1', datetime('now', 'localtime', '-3 minutes'));
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env node
/**
* 数据库迁移运行器
* 按顺序执行 migrations/ 目录下的所有 .sql 文件
* 记录已执行的迁移到 schema_migrations 表(幂等性保障)
*
* 用法:
* node migrations/_runner.js # 执行所有待执行迁移
* node migrations/_runner.js --status # 查看迁移状态
* node migrations/_runner.js --reset # 重置数据库(慎用)
*/
import { DatabaseSync } from 'node:sqlite'
import { readFileSync, readdirSync, mkdirSync, unlinkSync, existsSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const root = join(__dirname, '..')
const dataDir = join(root, 'data')
mkdirSync(dataDir, { recursive: true })
const dbPath = join(dataDir, 'reporter-station.db')
/** 初始化数据库连接(确保 schema_migrations 表存在) */
function initDb() {
const db = new DatabaseSync(dbPath)
db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS schema_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
`)
return db
}
const args = process.argv.slice(2)
if (args.includes('--status')) {
const db = initDb()
const applied = db.prepare('SELECT name, applied_at FROM schema_migrations ORDER BY id').all()
const allFiles = readdirSync(__dirname)
.filter(f => f.endsWith('.sql') && !f.startsWith('_'))
.sort()
console.log('\n迁移状态:')
for (const f of allFiles) {
const name = f.replace('.sql', '')
const row = applied.find(a => a.name === name)
if (row) {
console.log(` [x] ${name} (applied at ${row.applied_at})`)
} else {
console.log(` [ ] ${name}`)
}
}
db.close()
process.exit(0)
}
if (args.includes('--seed')) {
const db = initDb()
const seedFile = '002_seed_data.sql'
const sql = readFileSync(join(__dirname, seedFile), 'utf-8')
try {
db.exec(sql)
console.log(`已重新执行 seed 数据(${seedFile}`)
} catch (err) {
console.error(`seed 失败: ${err.message}`)
process.exit(1)
}
db.close()
process.exit(0)
}
if (args.includes('--reset')) {
for (const ext of ['', '-wal', '-shm']) {
const f = dbPath + ext
if (existsSync(f)) { unlinkSync(f); console.log(`已删除: ${f}`) }
}
console.log('数据库已重置,重新启动服务将自动执行迁移。')
process.exit(0)
}
// 默认:执行待执行的迁移
const db = initDb()
const applied = new Set(db.prepare('SELECT name FROM schema_migrations').all().map(r => r.name))
const allFiles = readdirSync(__dirname)
.filter(f => f.endsWith('.sql') && !f.startsWith('_'))
.sort()
let appliedCount = 0
for (const file of allFiles) {
const name = file.replace('.sql', '')
if (applied.has(name)) {
console.log(` 跳过 (已执行): ${name}`)
continue
}
const sql = readFileSync(join(__dirname, file), 'utf-8')
try {
db.exec('BEGIN')
db.exec(sql)
db.prepare('INSERT INTO schema_migrations (name) VALUES (?)').run(name)
db.exec('COMMIT')
console.log(` [x] ${name}`)
appliedCount++
} catch (err) {
db.exec('ROLLBACK')
console.error(` [!] ${name} 执行失败: ${err.message}`)
process.exit(1)
}
}
db.close()
if (appliedCount === 0) {
console.log('\n没有待执行的迁移,数据库已是最新。')
} else {
console.log(`\n成功执行 ${appliedCount} 个迁移。`)
}