chore: initial import
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const db = require('../db');
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log('正在添加 token_version 字段...');
|
||||
await db.query('ALTER TABLE users ADD COLUMN IF NOT EXISTS token_version INTEGER DEFAULT 0');
|
||||
console.log('成功添加 token_version 字段');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('执行失败:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const db = require('../db');
|
||||
|
||||
async function createAuditTable() {
|
||||
const client = await db.pool.connect();
|
||||
try {
|
||||
console.log('正在创建 audit_logs 表...');
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
username VARCHAR(50),
|
||||
real_name VARCHAR(50),
|
||||
ip_address VARCHAR(50),
|
||||
method VARCHAR(10),
|
||||
url TEXT,
|
||||
description VARCHAR(255),
|
||||
status INTEGER,
|
||||
duration INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
console.log('正在创建索引...');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at DESC);');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_audit_logs_username ON audit_logs(username);');
|
||||
|
||||
console.log('audit_logs 表创建成功!');
|
||||
} catch (err) {
|
||||
console.error('创建表失败:', err);
|
||||
} finally {
|
||||
client.release();
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
createAuditTable();
|
||||
@@ -0,0 +1,42 @@
|
||||
const { Client } = require('pg');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
const createDb = async () => {
|
||||
const dbName = process.env.DB_DATABASE || 'stsystem';
|
||||
|
||||
// 连接到默认的 postgres 数据库
|
||||
const client = new Client({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: 'postgres', // 连接到默认数据库
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log(`已连接到 postgres 数据库,正在检查 ${dbName} 是否存在...`);
|
||||
|
||||
const res = await client.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]);
|
||||
|
||||
if (res.rows.length === 0) {
|
||||
console.log(`数据库 ${dbName} 不存在,正在创建...`);
|
||||
// CREATE DATABASE 不能在事务块中运行,也不能使用参数化查询
|
||||
await client.query(`CREATE DATABASE "${dbName}"`);
|
||||
console.log(`数据库 ${dbName} 创建成功!`);
|
||||
} else {
|
||||
console.log(`数据库 ${dbName} 已存在,跳过创建。`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建数据库失败:', err);
|
||||
// 如果是因为 postgres 数据库不存在(极少见)或者密码错误,这里会报错
|
||||
// 但我们不应该阻塞后续流程,也许用户已经手动创建了 stsystem
|
||||
// 不过为了安全起见,如果这一步失败,通常意味着配置错误,还是退出比较好
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
};
|
||||
|
||||
createDb();
|
||||
@@ -0,0 +1,48 @@
|
||||
const db = require('../db');
|
||||
|
||||
async function createTables() {
|
||||
const client = await db.pool.connect();
|
||||
try {
|
||||
console.log('开始创建通知相关表...');
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 创建 notifications 表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
publisher_id INTEGER REFERENCES users(id),
|
||||
publisher_name VARCHAR(100),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
is_top BOOLEAN DEFAULT FALSE,
|
||||
status VARCHAR(20) DEFAULT 'published' -- published, draft, archived
|
||||
);
|
||||
`);
|
||||
console.log('notifications 表创建成功');
|
||||
|
||||
// 创建 notification_attachments 表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS notification_attachments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
notification_id INTEGER REFERENCES notifications(id) ON DELETE CASCADE,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
console.log('notification_attachments 表创建成功');
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('所有表创建完成');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('创建表失败:', err);
|
||||
} finally {
|
||||
client.release();
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
createTables();
|
||||
@@ -0,0 +1,63 @@
|
||||
const { Pool } = require('pg');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_DATABASE || process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
async function debug() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
console.log('🔍 开始调试统计数据...');
|
||||
|
||||
// 1. 检查状态分布
|
||||
console.log('\n📊 1. 成果状态分布:');
|
||||
const statusRes = await client.query('SELECT status, COUNT(*) FROM achievements GROUP BY status');
|
||||
console.table(statusRes.rows);
|
||||
|
||||
// 2. 检查部门字段样本
|
||||
console.log('\n📋 2. 成果部门字段样本 (前5条):');
|
||||
const deptRes = await client.query('SELECT id, assigned_departments FROM achievements LIMIT 5');
|
||||
console.table(deptRes.rows);
|
||||
|
||||
// 3. 检查部门表
|
||||
console.log('\n🏢 3. 部门表数据:');
|
||||
const deptsRes = await client.query('SELECT name FROM departments');
|
||||
console.table(deptsRes.rows);
|
||||
|
||||
// 4. 运行统计查询 (模拟 statistics.js 中的查询)
|
||||
console.log('\n📈 4. 运行统计查询 (status=\'approved\'):');
|
||||
const statsQuery = `
|
||||
SELECT
|
||||
d.name as department,
|
||||
a.type,
|
||||
EXTRACT(YEAR FROM a.achievement_date) as year,
|
||||
COUNT(*) as count
|
||||
FROM departments d
|
||||
JOIN achievements a ON d.name = ANY(a.assigned_departments)
|
||||
WHERE a.status = 'approved' AND a.achievement_date IS NOT NULL
|
||||
GROUP BY d.name, a.type, year
|
||||
ORDER BY d.name
|
||||
`;
|
||||
const statsRes = await client.query(statsQuery);
|
||||
console.log(`查询返回 ${statsRes.rows.length} 行数据`);
|
||||
if (statsRes.rows.length > 0) {
|
||||
console.table(statsRes.rows.slice(0, 10)); // 只显示前10条
|
||||
} else {
|
||||
console.log('⚠️ 查询结果为空!');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ 调试出错:', e);
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
debug();
|
||||
@@ -0,0 +1,45 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const db = require('../db');
|
||||
|
||||
async function listSchema() {
|
||||
try {
|
||||
const res = await db.query(`
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_schema = 'public'
|
||||
ORDER BY
|
||||
table_name, ordinal_position
|
||||
`);
|
||||
|
||||
const tables = {};
|
||||
res.rows.forEach(row => {
|
||||
if (!tables[row.table_name]) {
|
||||
tables[row.table_name] = [];
|
||||
}
|
||||
tables[row.table_name].push({
|
||||
column: row.column_name,
|
||||
type: row.data_type,
|
||||
nullable: row.is_nullable
|
||||
});
|
||||
});
|
||||
|
||||
console.log('--- 数据库全量表结构 ---');
|
||||
for (const [tableName, columns] of Object.entries(tables)) {
|
||||
console.log(`\n表名: ${tableName}`);
|
||||
console.table(columns);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('查询失败:', err);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
listSchema();
|
||||
@@ -0,0 +1,212 @@
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_DATABASE || process.env.DB_NAME, // 兼容 .env 中的不同命名
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
// 随机数据源
|
||||
const DEPARTMENTS = ['研发部', '市场部', '人事部', '财务部', '销售部', '运维部'];
|
||||
const SURNAMES = ['赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨'];
|
||||
const NAMES = ['伟', '芳', '娜', '敏', '静', '强', '磊', '军', '洋', '勇', '艳', '杰', '娟', '涛', '明', '超'];
|
||||
const TITLES_PREFIX = ['基于AI的', '高性能', '分布式', '新一代', '云原生', '跨平台', '智能', '自动化'];
|
||||
const TITLES_SUFFIX = ['系统设计', '算法研究', '平台开发', '应用实践', '优化方案', '技术白皮书', '分析报告'];
|
||||
|
||||
// 字典数据
|
||||
const DICTS = {
|
||||
dict_organizations: ['科技大学', '软件研究所', '创新中心', '数据实验室'],
|
||||
dict_paper_types: ['SCI', 'EI', '核心期刊', '会议论文'],
|
||||
dict_award_types: ['科技进步奖', '技术发明奖', '自然科学奖'],
|
||||
dict_award_levels: ['一等奖', '二等奖', '三等奖', '特等奖'],
|
||||
dict_project_categories: ['国家重点研发计划', '自然科学基金', '省部级项目', '横向课题'],
|
||||
dict_standard_types: ['国家标准', '行业标准', '团体标准', '企业标准']
|
||||
};
|
||||
|
||||
const ACHIEVEMENT_TYPES = [
|
||||
'paper', 'project', 'award', 'standard', 'monograph', 'report', 'plan', 'patent', 'transformation', 'software'
|
||||
];
|
||||
|
||||
const STATUSES = ['pending', 'approved', 'rejected'];
|
||||
|
||||
// 辅助函数
|
||||
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const randomChoice = (arr) => arr[randomInt(0, arr.length - 1)];
|
||||
const generatePhone = () => `1${randomChoice(['3', '5', '7', '8', '9'])}${randomInt(100000000, 999999999)}`;
|
||||
const generateName = () => `${randomChoice(SURNAMES)}${randomChoice(NAMES)}${Math.random() > 0.5 ? randomChoice(NAMES) : ''}`;
|
||||
const generateTitle = () => `${randomChoice(TITLES_PREFIX)}${randomChoice(TITLES_SUFFIX)}`;
|
||||
const generateDate = (startYear, endYear) => {
|
||||
const year = randomInt(startYear, endYear);
|
||||
const month = randomInt(0, 11);
|
||||
const day = randomInt(1, 28);
|
||||
return new Date(year, month, day);
|
||||
};
|
||||
|
||||
async function seed() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
console.log('🚀 开始注入测试数据...');
|
||||
|
||||
// 0. 初始化数据库架构
|
||||
console.log('🏗️ 初始化数据库架构...');
|
||||
const schemaPath = path.join(__dirname, '../../database/schema.sql');
|
||||
if (fs.existsSync(schemaPath)) {
|
||||
const schemaSql = fs.readFileSync(schemaPath, 'utf8');
|
||||
await client.query(schemaSql);
|
||||
console.log('✅ 数据库架构已应用');
|
||||
} else {
|
||||
console.warn('⚠️ 未找到 schema.sql,跳过架构初始化');
|
||||
}
|
||||
|
||||
// 1. 清理数据 (保留特定账号)
|
||||
console.log('🧹 清理旧数据...');
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 级联删除所有成果 (这将自动删除详情表和附件表)
|
||||
await client.query('TRUNCATE achievements CASCADE');
|
||||
|
||||
// 删除除保留账号外的所有用户
|
||||
const keepUsers = ['18888888888', '19999999999'];
|
||||
await client.query('DELETE FROM users WHERE username != $1 AND username != $2', [keepUsers[0], keepUsers[1]]);
|
||||
|
||||
console.log('✅ 旧数据清理完成');
|
||||
|
||||
// 2. 填充字典和部门
|
||||
console.log('📚 填充字典数据...');
|
||||
for (const [table, values] of Object.entries(DICTS)) {
|
||||
for (const val of values) {
|
||||
await client.query(`INSERT INTO ${table} (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`, [val]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dept of DEPARTMENTS) {
|
||||
await client.query(
|
||||
'INSERT INTO departments (name) VALUES ($1) ON CONFLICT (name) DO NOTHING',
|
||||
[dept]
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 生成新用户
|
||||
console.log('👥 生成测试用户...');
|
||||
const passwordHash = await bcrypt.hash('123456', 10);
|
||||
const userIds = [];
|
||||
|
||||
// 获取保留用户的ID
|
||||
const existingUsers = await client.query('SELECT id FROM users WHERE username = $1 OR username = $2', [keepUsers[0], keepUsers[1]]);
|
||||
existingUsers.rows.forEach(row => userIds.push(row.id));
|
||||
|
||||
// 生成 20 个新用户
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const username = generatePhone();
|
||||
const realName = generateName();
|
||||
const dept = randomChoice(DEPARTMENTS);
|
||||
|
||||
try {
|
||||
const res = await client.query(
|
||||
'INSERT INTO users (username, password_hash, role, real_name, department, dept_status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id',
|
||||
[username, passwordHash, 'user', realName, dept, 'verified']
|
||||
);
|
||||
userIds.push(res.rows[0].id);
|
||||
} catch (e) {
|
||||
if (e.code !== '23505') console.error(e);
|
||||
}
|
||||
}
|
||||
console.log(`✅ 已生成 ${userIds.length} 个用户`);
|
||||
|
||||
// 4. 生成成果数据
|
||||
console.log('📊 生成成果数据 (目标 500+ 条)...');
|
||||
const targetCount = 550;
|
||||
let insertedCount = 0;
|
||||
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
const userId = randomChoice(userIds);
|
||||
const type = randomChoice(ACHIEVEMENT_TYPES);
|
||||
const status = randomChoice(STATUSES);
|
||||
const date = generateDate(2023, 2026);
|
||||
const title = generateTitle();
|
||||
const dept = randomChoice(DEPARTMENTS);
|
||||
|
||||
// 构造 contributors JSONB
|
||||
const contributors = [
|
||||
{ name: generateName(), phone: generatePhone(), isMain: true },
|
||||
{ name: generateName(), phone: generatePhone(), isMain: false }
|
||||
];
|
||||
|
||||
// 插入主表
|
||||
const res = await client.query(
|
||||
`INSERT INTO achievements
|
||||
(user_id, type, name, contributors, assigned_departments, achievement_date, status, remarks, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
|
||||
[
|
||||
userId,
|
||||
type,
|
||||
title,
|
||||
JSON.stringify(contributors),
|
||||
[dept], // 数组格式
|
||||
date,
|
||||
status,
|
||||
'自动生成的数据',
|
||||
date
|
||||
]
|
||||
);
|
||||
const achievementId = res.rows[0].id;
|
||||
|
||||
// 插入附件
|
||||
await client.query(
|
||||
'INSERT INTO achievement_attachments (achievement_id, file_name, file_path) VALUES ($1, $2, $3)',
|
||||
[achievementId, 'test_report.pdf', 'uploads/mock_file.pdf']
|
||||
);
|
||||
|
||||
// 插入详情表 (根据类型)
|
||||
switch (type) {
|
||||
case 'paper':
|
||||
await client.query(
|
||||
'INSERT INTO achievement_paper (achievement_id, paper_type, journal_name, publish_date) VALUES ($1, $2, $3, $4)',
|
||||
[achievementId, randomChoice(DICTS.dict_paper_types), '计算机学报', date]
|
||||
);
|
||||
break;
|
||||
case 'award':
|
||||
await client.query(
|
||||
'INSERT INTO achievement_award (achievement_id, award_type, award_level, award_unit) VALUES ($1, $2, $3, $4)',
|
||||
[achievementId, randomChoice(DICTS.dict_award_types), randomChoice(DICTS.dict_award_levels), '科技部']
|
||||
);
|
||||
break;
|
||||
case 'project':
|
||||
await client.query(
|
||||
'INSERT INTO achievement_project (achievement_id, project_category, source) VALUES ($1, $2, $3)',
|
||||
[achievementId, randomChoice(DICTS.dict_project_categories), '国家自然科学基金委员会']
|
||||
);
|
||||
break;
|
||||
case 'standard':
|
||||
await client.query(
|
||||
'INSERT INTO achievement_standard (achievement_id, standard_type, standard_no, implement_date) VALUES ($1, $2, $3, $4)',
|
||||
[achievementId, randomChoice(DICTS.dict_standard_types), `GB/T ${randomInt(1000, 9999)}-2025`, date]
|
||||
);
|
||||
break;
|
||||
// 其他类型暂略,或插入空记录
|
||||
}
|
||||
|
||||
insertedCount++;
|
||||
if (insertedCount % 100 === 0) process.stdout.write('.');
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(`\n✅ 成功注入 ${insertedCount} 条成果数据!`);
|
||||
console.log('🎉 数据注入完成!');
|
||||
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('❌ 数据注入失败:', e);
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
seed();
|
||||
@@ -0,0 +1,412 @@
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_DATABASE || process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
const USER_COUNT = 20000;
|
||||
const ACHIEVEMENT_COUNT = 500000;
|
||||
const BATCH_SIZE = 1000;
|
||||
const RUN_CLEAN = process.argv.includes('--clean');
|
||||
|
||||
const dictTables = [
|
||||
'dict_organizations',
|
||||
'dict_award_types',
|
||||
'dict_award_levels',
|
||||
'dict_paper_types',
|
||||
'dict_standard_types',
|
||||
'dict_project_categories',
|
||||
];
|
||||
|
||||
const achievementTypes = [
|
||||
'paper',
|
||||
'award',
|
||||
'project',
|
||||
'standard',
|
||||
'monograph',
|
||||
'report',
|
||||
'plan',
|
||||
'patent',
|
||||
'transformation',
|
||||
'software',
|
||||
];
|
||||
const USERNAME_PREFIX = '176';
|
||||
const SEED_REMARK = 'seed_500k';
|
||||
const STATUSES = ['pending', 'approved', 'rejected'];
|
||||
const PATENT_TYPES = ['invention', 'utility', 'design'];
|
||||
const REPORT_TYPES = ['technical', 'inspection', 'analysis', 'feasibility'];
|
||||
const TRANS_METHODS = ['transfer', 'license', 'cooperation', 'self-use'];
|
||||
const ACQUISITION_METHODS = ['independent', 'co-development', 'transfer'];
|
||||
const SOFTWARE_SCOPES = ['national', 'industry', 'department'];
|
||||
|
||||
const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const randomChoice = (arr) => arr[randomInt(0, arr.length - 1)];
|
||||
const pad = (num, size) => String(num).padStart(size, '0');
|
||||
const randomDateInYears = (years) =>
|
||||
new Date(randomChoice(years), randomInt(0, 11), randomInt(1, 28));
|
||||
|
||||
function buildInsert(table, columns, rows) {
|
||||
const values = [];
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
for (const row of rows) {
|
||||
const placeholders = [];
|
||||
for (const value of row) {
|
||||
params.push(value);
|
||||
placeholders.push(`$${paramIndex++}`);
|
||||
}
|
||||
values.push(`(${placeholders.join(', ')})`);
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${values.join(', ')}`;
|
||||
return { sql, params };
|
||||
}
|
||||
|
||||
async function fetchNames(client, table) {
|
||||
const res = await client.query(`SELECT name FROM ${table} ORDER BY id`);
|
||||
return res.rows.map((row) => row.name);
|
||||
}
|
||||
|
||||
async function seedUsers(client, departments) {
|
||||
const passwordHash = await bcrypt.hash('123456', 10);
|
||||
let created = 0;
|
||||
|
||||
for (let offset = 0; offset < USER_COUNT; offset += BATCH_SIZE) {
|
||||
const batchSize = Math.min(BATCH_SIZE, USER_COUNT - offset);
|
||||
const rows = [];
|
||||
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const index = offset + i;
|
||||
const username = `${USERNAME_PREFIX}${pad(index, 8)}`; // 11 digits, deterministic
|
||||
const realName = `Perf User ${index + 1}`;
|
||||
const department = randomChoice(departments);
|
||||
rows.push([username, passwordHash, 'user', realName, department, 'verified']);
|
||||
}
|
||||
|
||||
const { sql, params } = buildInsert(
|
||||
'users',
|
||||
['username', 'password_hash', 'role', 'real_name', 'department', 'dept_status'],
|
||||
rows
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`${sql} ON CONFLICT (username) DO UPDATE SET
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
role = EXCLUDED.role,
|
||||
real_name = EXCLUDED.real_name,
|
||||
department = EXCLUDED.department,
|
||||
dept_status = EXCLUDED.dept_status`
|
||||
, params);
|
||||
|
||||
created += batchSize;
|
||||
if (created % 5000 === 0) {
|
||||
console.log(`[seed-data-500k] users upserted: ${created}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function seedAchievements(client, departments, dicts, userIds) {
|
||||
let inserted = 0;
|
||||
const dateYears = [2023, 2024, 2026];
|
||||
|
||||
for (let offset = 0; offset < ACHIEVEMENT_COUNT; offset += BATCH_SIZE) {
|
||||
const batchSize = Math.min(BATCH_SIZE, ACHIEVEMENT_COUNT - offset);
|
||||
await client.query('BEGIN');
|
||||
try {
|
||||
const rows = [];
|
||||
const achievementDates = [];
|
||||
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const index = offset + i;
|
||||
const userId = randomChoice(userIds);
|
||||
const type = randomChoice(achievementTypes);
|
||||
const name = `Perf Achievement ${index + 1}`;
|
||||
const assignedDept = randomChoice(departments);
|
||||
const date = randomDateInYears(dateYears);
|
||||
achievementDates.push(date);
|
||||
const contributors = [
|
||||
{ name: `Contributor ${index + 1}A`, phone: `130${pad(index % 100000000, 8)}`, isMain: true },
|
||||
{ name: `Contributor ${index + 1}B`, phone: `131${pad(index % 100000000, 8)}`, isMain: false },
|
||||
];
|
||||
rows.push([
|
||||
userId,
|
||||
type,
|
||||
name,
|
||||
JSON.stringify(contributors),
|
||||
[assignedDept],
|
||||
date,
|
||||
randomChoice(STATUSES),
|
||||
SEED_REMARK,
|
||||
date,
|
||||
]);
|
||||
}
|
||||
|
||||
const { sql, params } = buildInsert(
|
||||
'achievements',
|
||||
[
|
||||
'user_id',
|
||||
'type',
|
||||
'name',
|
||||
'contributors',
|
||||
'assigned_departments',
|
||||
'achievement_date',
|
||||
'status',
|
||||
'remarks',
|
||||
'created_at',
|
||||
],
|
||||
rows
|
||||
);
|
||||
|
||||
const res = await client.query(`${sql} RETURNING id, type`, params);
|
||||
|
||||
const attachmentsRows = [];
|
||||
const paperRows = [];
|
||||
const awardRows = [];
|
||||
const projectRows = [];
|
||||
const standardRows = [];
|
||||
const monographRows = [];
|
||||
const reportRows = [];
|
||||
const planRows = [];
|
||||
const patentRows = [];
|
||||
const transformationRows = [];
|
||||
const softwareRows = [];
|
||||
|
||||
for (let i = 0; i < res.rows.length; i++) {
|
||||
const { id, type } = res.rows[i];
|
||||
const date = achievementDates[i];
|
||||
const idx = offset + i + 1;
|
||||
attachmentsRows.push([id, `attachment_${idx}.pdf`, 'uploads/mock_file.pdf']);
|
||||
|
||||
if (type === 'paper') {
|
||||
paperRows.push([id, randomChoice(dicts.dict_paper_types), 'Perf Journal', randomDateInYears(dateYears)]);
|
||||
} else if (type === 'award') {
|
||||
awardRows.push([
|
||||
id,
|
||||
randomChoice(dicts.dict_award_types),
|
||||
randomChoice(dicts.dict_award_levels),
|
||||
randomChoice(dicts.dict_organizations),
|
||||
]);
|
||||
} else if (type === 'project') {
|
||||
projectRows.push([id, randomChoice(dicts.dict_project_categories), 'Perf Project Source']);
|
||||
} else if (type === 'standard') {
|
||||
const standardYear = date.getFullYear();
|
||||
standardRows.push([
|
||||
id,
|
||||
randomChoice(dicts.dict_standard_types),
|
||||
`STD-${standardYear}-${pad(idx, 6)}`,
|
||||
date,
|
||||
]);
|
||||
} else if (type === 'monograph') {
|
||||
monographRows.push([id, 'Perf Press', `ISBN-${pad(idx, 9)}`]);
|
||||
} else if (type === 'report') {
|
||||
reportRows.push([id, randomChoice(REPORT_TYPES), 'Recipient Unit', null, `Approver ${idx}`]);
|
||||
} else if (type === 'plan') {
|
||||
planRows.push([id]);
|
||||
} else if (type === 'patent') {
|
||||
patentRows.push([id, `PAT-${pad(idx, 8)}`, randomChoice(PATENT_TYPES), 'Perf Assignee']);
|
||||
} else if (type === 'transformation') {
|
||||
transformationRows.push([
|
||||
id,
|
||||
randomChoice(TRANS_METHODS),
|
||||
randomInt(50, 5000) * 1000 + randomInt(0, 99) / 100,
|
||||
randomChoice(dicts.dict_project_categories),
|
||||
]);
|
||||
} else if (type === 'software') {
|
||||
softwareRows.push([
|
||||
id,
|
||||
`SW-${pad(idx, 8)}`,
|
||||
randomChoice(ACQUISITION_METHODS),
|
||||
randomChoice(SOFTWARE_SCOPES),
|
||||
'Perf Owner Unit',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentsRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_attachments',
|
||||
['achievement_id', 'file_name', 'file_path'],
|
||||
attachmentsRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
|
||||
if (paperRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_paper',
|
||||
['achievement_id', 'paper_type', 'journal_name', 'publish_date'],
|
||||
paperRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (awardRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_award',
|
||||
['achievement_id', 'award_type', 'award_level', 'award_unit'],
|
||||
awardRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (projectRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_project',
|
||||
['achievement_id', 'project_category', 'source'],
|
||||
projectRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (standardRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_standard',
|
||||
['achievement_id', 'standard_type', 'standard_no', 'implement_date'],
|
||||
standardRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (monographRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_monograph',
|
||||
['achievement_id', 'publisher', 'isbn'],
|
||||
monographRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (reportRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_report',
|
||||
['achievement_id', 'report_type', 'recipient', 'approver_id', 'approver_name'],
|
||||
reportRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (planRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_plan',
|
||||
['achievement_id'],
|
||||
planRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (patentRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_patent',
|
||||
['achievement_id', 'patent_no', 'patent_type', 'assignee'],
|
||||
patentRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (transformationRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_transformation',
|
||||
['achievement_id', 'trans_method', 'trans_amount', 'project_category'],
|
||||
transformationRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
if (softwareRows.length) {
|
||||
const insert = buildInsert(
|
||||
'achievement_software',
|
||||
['achievement_id', 'reg_no', 'acquisition_method', 'scope', 'owner_unit'],
|
||||
softwareRows
|
||||
);
|
||||
await client.query(insert.sql, insert.params);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
inserted += batchSize;
|
||||
if (inserted % 10000 === 0) {
|
||||
console.log(`[seed-data-500k] achievements inserted: ${inserted}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanSeedData(client) {
|
||||
console.log('[seed-data-500k] cleaning existing seeded data...');
|
||||
await client.query('BEGIN');
|
||||
try {
|
||||
const usersRes = await client.query(
|
||||
'SELECT id FROM users WHERE username LIKE $1',
|
||||
[`${USERNAME_PREFIX}%`]
|
||||
);
|
||||
const userIds = usersRes.rows.map((row) => row.id);
|
||||
|
||||
await client.query(
|
||||
'DELETE FROM achievements WHERE remarks = $1 OR user_id = ANY($2)',
|
||||
[SEED_REMARK, userIds.length ? userIds : [0]]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
'DELETE FROM users WHERE username LIKE $1',
|
||||
[`${USERNAME_PREFIX}%`]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('[seed-data-500k] clean done');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
console.log('[seed-data-500k] start');
|
||||
|
||||
if (RUN_CLEAN) {
|
||||
await cleanSeedData(client);
|
||||
}
|
||||
|
||||
const departments = await fetchNames(client, 'departments');
|
||||
if (departments.length === 0) {
|
||||
throw new Error('No departments found. Run "npm run init-db" first.');
|
||||
}
|
||||
|
||||
const dicts = {};
|
||||
for (const table of dictTables) {
|
||||
const values = await fetchNames(client, table);
|
||||
if (values.length === 0) {
|
||||
throw new Error(`No data in ${table}. Run "npm run init-db" first.`);
|
||||
}
|
||||
dicts[table] = values;
|
||||
}
|
||||
|
||||
await client.query('BEGIN');
|
||||
await seedUsers(client, departments);
|
||||
await client.query('COMMIT');
|
||||
|
||||
const userIdsRes = await client.query(
|
||||
'SELECT id FROM users WHERE username LIKE $1 ORDER BY id',
|
||||
[`${USERNAME_PREFIX}%`]
|
||||
);
|
||||
const userIds = userIdsRes.rows.map((row) => row.id);
|
||||
if (userIds.length === 0) {
|
||||
throw new Error('No seeded users found.');
|
||||
}
|
||||
|
||||
await seedAchievements(client, departments, dicts, userIds);
|
||||
|
||||
console.log('[seed-data-500k] done');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('[seed-data-500k] failed:', err.message || err);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,103 @@
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
const db = require('../db');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs');
|
||||
|
||||
const setup = async () => {
|
||||
try {
|
||||
console.log('--- 科技成果转化系统 部署工具 ---');
|
||||
|
||||
// 1. 执行完整建表语句
|
||||
console.log('正在初始化数据库表结构...');
|
||||
const sqlPath = path.join(__dirname, '../../database/schema.sql');
|
||||
if (!fs.existsSync(sqlPath)) {
|
||||
throw new Error(`找不到 SQL 文件: ${sqlPath}`);
|
||||
}
|
||||
|
||||
const sql = fs.readFileSync(sqlPath, 'utf8');
|
||||
// 简单的 SQL 分割逻辑,按分号分割
|
||||
const statements = sql.split(';').filter(s => s.trim());
|
||||
|
||||
for (let statement of statements) {
|
||||
try {
|
||||
await db.query(statement);
|
||||
} catch (e) {
|
||||
// 忽略“已存在”类的错误
|
||||
if (!e.message.includes('already exists')) {
|
||||
console.log(`执行语句时提示: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 初始化默认部门
|
||||
console.log('正在初始化默认部门...');
|
||||
const defaultDepts = [
|
||||
'院领导', '卫星中心综合部', '卫星中心生产部', '卫星中心研发部', '大数据中心',
|
||||
'生产保障部', '生产技术部', '土地调查监测室', '森林调查监测室', '湿地调查监测室',
|
||||
'专业调查室', '权籍调查室', '碳汇研究中心', '权益研究中心', '科技与档案室',
|
||||
'办公室', '党群工作部', '待定'
|
||||
];
|
||||
for (const dept of defaultDepts) {
|
||||
await db.query('INSERT INTO departments (name) VALUES ($1) ON CONFLICT (name) DO NOTHING', [dept]);
|
||||
}
|
||||
|
||||
// 3. 初始化字典表数据
|
||||
console.log('正在初始化字典表数据...');
|
||||
|
||||
const dictData = {
|
||||
dict_organizations: ['黑龙江省自然资源权益调查监测院','黑龙江省自然资源卫星应用技术中心','黑龙江省自然资源权益调查监测院(黑龙江省自然资源卫星应用技术中心)','待定'],
|
||||
dict_award_types: ['国家级', '省部级', '社会力量', '其他', '待定'],
|
||||
dict_award_levels: ['一等奖', '二等奖', '三等奖', '其他', '待定'],
|
||||
dict_paper_types: ['Nature/Science/Cell', 'CCF A类会议','SCI一区期刊','SCI二区期刊', 'CCF B类会议','CCF C类会议','SCI三区期刊','SCI四区期刊','EI源刊', '中文核心期刊', 'EI会议', '普通期刊', '普通会议', '待定'],
|
||||
dict_standard_types: ['国家标准', '国际标准', '行业标准', '地方标准', '待定'],
|
||||
dict_project_categories: ['科技', '财政', '横向', '成果转化', '待定']
|
||||
};
|
||||
|
||||
for (const [tableName, values] of Object.entries(dictData)) {
|
||||
for (const val of values) {
|
||||
await db.query(`INSERT INTO ${tableName} (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`, [val]);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建初始超级管理员
|
||||
const adminPhone = '18888888888';
|
||||
const adminPass = 'admin123';
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const passwordHash = await bcrypt.hash(adminPass, salt);
|
||||
|
||||
console.log('正在创建初始超级管理员...');
|
||||
await db.query(
|
||||
`INSERT INTO users (username, password_hash, role, real_name, department, dept_status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (username) DO UPDATE SET role = 'super_admin'`,
|
||||
[adminPhone, passwordHash, 'super_admin', '系统管理员', null, 'verified']
|
||||
);
|
||||
|
||||
// 5. 创建初始系统维护员
|
||||
const maintainerPhone = '19999999999';
|
||||
const maintainerPass = 'maintainer123';
|
||||
const maintainerHash = await bcrypt.hash(maintainerPass, salt);
|
||||
|
||||
console.log('正在创建初始系统维护员...');
|
||||
await db.query(
|
||||
`INSERT INTO users (username, password_hash, role, real_name, department, dept_status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (username) DO UPDATE SET role = 'maintainer'`,
|
||||
[maintainerPhone, maintainerHash, 'maintainer', '系统维护员', null, 'verified']
|
||||
);
|
||||
|
||||
console.log('\n-----------------------------------');
|
||||
console.log('部署成功!');
|
||||
console.log(`初始超管账号: ${adminPhone} / ${adminPass}`);
|
||||
console.log(`初始维护员账号: ${maintainerPhone} / ${maintainerPass}`);
|
||||
console.log('-----------------------------------');
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('\n部署失败:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
setup();
|
||||
Reference in New Issue
Block a user