feat(demo): 25 footprints w/ 10 competition awards + real drawn certificate attachments, grade data aligned to gpa formula, seed.mjs reuses demo_seed command layer
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { loadCert, CERT_BY_TITLE } from './demo-certs.mjs';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
export const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', '.data');
|
export const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', '.data');
|
||||||
@@ -412,6 +413,27 @@ export const DEMO_ROWS = [
|
|||||||
['证书技能', '计算机技术与软件专业技术资格(软件设计师)', '参加', '国家级', null, '工业和信息化部', '2025-11-08', null, 'ongoing',
|
['证书技能', '计算机技术与软件专业技术资格(软件设计师)', '参加', '国家级', null, '工业和信息化部', '2025-11-08', null, 'ongoing',
|
||||||
'备考软件设计师(中级)', '', ['软考'], ['职业发展'],
|
'备考软件设计师(中级)', '', ['软考'], ['职业发展'],
|
||||||
{}, {}],
|
{}, {}],
|
||||||
|
['竞赛', 'ACM-ICPC 国际大学生程序设计竞赛', '核心', '国家级', '银奖', '中国大学生程序设计竞赛组委会', '2025-10-18', '2025-10-19', 'finished',
|
||||||
|
'赛区内团队排名前 5%,与队友分工负责图论与动态规划、数据结构与搜索方向', '亚洲区域赛银奖', ['算法', '团队协作'], ['学业深造'],
|
||||||
|
{ my_work: '图论建模与状态转移设计', team_members: '王五、赵六' }, {}],
|
||||||
|
['竞赛', '中国大学生计算机设计大赛', '主导', '国家级', '二等奖', '中国大学生计算机设计大赛组织委员会', '2025-05-10', '2025-07-30', 'finished',
|
||||||
|
'作品《基于流量特征的入侵检测系统》,负责整体方案设计与检测算法实现', '全国总决赛二等奖', ['计算机', '安全'], ['学业深造'],
|
||||||
|
{ my_work: '方案设计、检测算法实现', team_members: '张三、李四' }, {}],
|
||||||
|
['竞赛', '第三届"强网杯"全国大学生网络安全挑战赛', '核心', '国家级', '二等奖', '战略支援部队信息工程大学', '2024-11-09', '2024-11-11', 'finished',
|
||||||
|
'CTF 攻防模式,负责 Web 渗透与 RSA 共模攻击类密码题', '半决赛二等奖', ['CTF', '密码', '安全'], ['职业发展'],
|
||||||
|
{ my_work: 'Web 渗透与密码工具编写', team_members: '张三、王五' }, {}],
|
||||||
|
['竞赛', '"挑战杯"全国大学生课外学术科技作品竞赛(省赛)', '核心', '省市级', '三等奖', '共青团重庆市委员会', '2025-03-15', '2025-06-20', 'finished',
|
||||||
|
'作品「基于流量特征的入侵检测研究」,负责研究报告撰写与答辩', '省赛三等奖', ['科研', '学术'], ['学业深造'],
|
||||||
|
{ my_work: '研究报告撰写' }, {}],
|
||||||
|
['竞赛', '全国大学生英语竞赛', '参加', '省市级', '三等奖', '高等学校大学外语教学研究会', '2024-04-14', '2024-04-14', 'finished',
|
||||||
|
'C 类笔试,词汇与阅读优势明显', 'C类省级三等奖', ['英语'], ['学业深造'],
|
||||||
|
{ score: '88' }, {}],
|
||||||
|
['竞赛', '院"新生杯"程序设计竞赛', '参加', '二级学院', '一等奖', '计算机网络学院', '2023-11-18', '2023-11-18', 'finished',
|
||||||
|
'大一入门竞赛,C 语言实现图书管理系统', '院级一等奖', ['编程'], ['学业深造'],
|
||||||
|
{}, {}],
|
||||||
|
['竞赛', '全国大学生密码技术竞赛', '参加', '国家级', null, '中国密码学会', '2025-11-20', null, 'ongoing',
|
||||||
|
'组队备战国赛(SM9 聚群签名方向)', '', ['密码', '学术'], ['学业深造'],
|
||||||
|
{}, {}],
|
||||||
['实习实践', '某安全公司渗透测试实习生', '参加', null, null, '某网络安全科技有限公司', '2025-07-01', '2025-08-25', 'finished',
|
['实习实践', '某安全公司渗透测试实习生', '参加', null, null, '某网络安全科技有限公司', '2025-07-01', '2025-08-25', 'finished',
|
||||||
'参与企业授权渗透测试项目 3 个,编写测试报告', '获实习优秀评价', ['实习', '安全'], ['职业发展'],
|
'参与企业授权渗透测试项目 3 个,编写测试报告', '获实习优秀评价', ['实习', '安全'], ['职业发展'],
|
||||||
{ position: '渗透测试实习生', mentor: '陈工' }, {}]
|
{ position: '渗透测试实习生', mentor: '陈工' }, {}]
|
||||||
@@ -446,27 +468,27 @@ export const DEMO_PROFILE = {
|
|||||||
resume: DEMO_RESUME
|
resume: DEMO_RESUME
|
||||||
};
|
};
|
||||||
|
|
||||||
// 演示附件用真实 PNG(1x1,可在预览查看并进入综测证明材料)
|
|
||||||
const DEMO_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
||||||
|
|
||||||
|
|
||||||
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点, 类别][] }
|
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点, 类别][] }
|
||||||
|
// 学期: { term, gpa, rank, total, courses: [课程名, 学分, 分数, 绩点, 类别][] }
|
||||||
|
// 演示成绩与成绩梯度公式一致:分段线性 gp(90→4.0 至 100→5.0,80→3.0 至 89→3.9 …),
|
||||||
|
// 学期 GPA 为分类加权(必修 1.2 / 专业选修 1.1),与 grades_overview 的综合算法结果吻合。
|
||||||
export const DEMO_SEMESTERS = [
|
export const DEMO_SEMESTERS = [
|
||||||
{ term: '2023-2024-1', gpa: 3.42, rank: 38, total: 156, courses: [
|
{ term: '2023-2024-1', gpa: 4.13, rank: 48, total: 156, courses: [
|
||||||
['高等数学(上)', 5, 78, 3.0, '必修课'], ['大学英语(一)', 3, 85, 3.7, '必修课'], ['程序设计基础', 4, 91, 4.0, '必修课'],
|
['高等数学(上)', 5, 78, 2.8, '必修课'], ['大学英语(一)', 3, 85, 3.5, '必修课'], ['程序设计基础', 4, 91, 4.1, '必修课'],
|
||||||
['思想道德与法治', 2, 88, 3.9, '必修课'], ['体育(一)', 1, 82, 3.5, '必修课']
|
['思想道德与法治', 2, 88, 3.8, '必修课'], ['体育(一)', 1, 82, 3.2, '必修课']
|
||||||
]},
|
]},
|
||||||
{ term: '2023-2024-2', gpa: 3.61, rank: 29, total: 156, courses: [
|
{ term: '2023-2024-2', gpa: 4.15, rank: 36, total: 156, courses: [
|
||||||
['高等数学(下)', 5, 84, 3.7, '必修课'], ['线性代数', 3, 76, 2.9, '必修课'], ['数据结构', 4, 90, 4.0, '必修课'],
|
['高等数学(下)', 5, 84, 3.4, '必修课'], ['线性代数', 3, 76, 2.6, '必修课'], ['数据结构', 4, 90, 4.0, '必修课'],
|
||||||
['大学英语(二)', 3, 87, 3.8, '必修课'], ['中国近现代史纲要', 2, 84, 3.5, '必修课'], ['体育(二)', 1, 86, 3.8, '必修课']
|
['大学英语(二)', 3, 87, 3.7, '必修课'], ['中国近现代史纲要', 2, 84, 3.4, '必修课'], ['体育(二)', 1, 86, 3.6, '必修课']
|
||||||
]},
|
]},
|
||||||
{ term: '2024-2025-1', gpa: 3.78, rank: 18, total: 156, courses: [
|
{ term: '2024-2025-1', gpa: 4.63, rank: 11, total: 156, courses: [
|
||||||
['概率论与数理统计', 3, 88, 3.9, '必修课'], ['操作系统', 4, 92, 4.0, '必修课'], ['计算机网络', 4, 86, 3.8, '必修课'],
|
['概率论与数理统计', 3, 88, 3.8, '必修课'], ['操作系统', 4, 92, 4.18, '必修课'], ['计算机网络', 4, 86, 3.6, '必修课'],
|
||||||
['数据库原理', 3, 94, 4.0, '必修课'], ['马克思主义基本原理', 2, 81, 3.3, '必修课'], ['体育(三)', 1, 88, 3.9, '必修课']
|
['数据库原理', 3, 94, 4.36, '必修课'], ['马克思主义基本原理', 2, 81, 3.1, '必修课'], ['体育(三)', 1, 88, 3.8, '必修课']
|
||||||
]},
|
]},
|
||||||
{ term: '2024-2025-2', gpa: 3.85, rank: 12, total: 156, courses: [
|
{ term: '2024-2025-2', gpa: 4.65, rank: 7, total: 156, courses: [
|
||||||
['软件工程', 3, 90, 4.0, '必修课'], ['信息安全导论', 3, 95, 4.0, '必修课'], ['密码学基础', 3, 89, 3.9, '必修课'],
|
['软件工程', 3, 90, 4.0, '必修课'], ['信息安全导论', 3, 95, 4.45, '必修课'], ['密码学基础', 3, 89, 3.9, '必修课'],
|
||||||
['Web 应用开发', 2, 96, 4.0, '专业选修课'], ['数字逻辑', 3, 79, 3.1, '必修课'], ['体育(四)', 1, 90, 4.0, '必修课']
|
['Web 应用开发', 2, 96, 4.55, '专业选修课'], ['数字逻辑', 3, 79, 2.9, '必修课'], ['体育(四)', 1, 90, 4.0, '必修课']
|
||||||
]}
|
]}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1261,11 +1283,16 @@ function listFootprints(d, query) {
|
|||||||
dimension_ids: (dimNames || []).map((nm) => dimIds[nm]).filter(Boolean),
|
dimension_ids: (dimNames || []).map((nm) => dimIds[nm]).filter(Boolean),
|
||||||
}});
|
}});
|
||||||
n++;
|
n++;
|
||||||
// 演示附件:竞赛/荣誉记录附带证书图片(可预览、可进综测证明材料)
|
// 演示附件:按标题匹配对应证书图片(可预览、可进综测证明材料)
|
||||||
if (/竞赛|优秀共青团员/.test(String(title)) && status === 'finished') {
|
const certKey = CERT_BY_TITLE.find((p2) => p2[0].test(String(title)))?.[1];
|
||||||
try {
|
if (certKey && status === 'finished') {
|
||||||
handleCommand('attachment_add', { owner_id: fr.item.id, file_name: '获奖证书.png', mime: 'image/png', size: DEMO_PNG.length, data_b64: DEMO_PNG, thumb_b64: DEMO_PNG });
|
const cert = loadCert(certKey);
|
||||||
} catch {}
|
if (cert) {
|
||||||
|
try {
|
||||||
|
const att = handleCommand('attachment_add', { owner_id: fr.id, ...cert });
|
||||||
|
console.log('[demo-seed] 证书附件已挂到「' + title + '」:', att.item?.id || att.item?.uuid || '?');
|
||||||
|
} catch (e) { console.log('[demo-seed] 证书附件失败 ' + title + ':', e.message); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let sems = 0;
|
let sems = 0;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 演示证书图片索引:demo 播种时按记录标题匹配,附加对应证书 WebP 附件。
|
||||||
|
// 图片为 Pillow 离线绘制的获奖证书(720x508 WebP + 480px 缩略图),仅作演示数据。
|
||||||
|
// 如需替换:直接覆盖 demo-certs/*.webp 即可,无需改代码。
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const DIR = path.join(__dirname, 'demo-certs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主名(如 cert-ctf)→ 证书附件负载(attachment_add 所需格式);文件缺失返回 undefined
|
||||||
|
*/
|
||||||
|
export function loadCert(assetName) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(path.join(DIR, assetName + '.webp'));
|
||||||
|
let thumbB64 = '';
|
||||||
|
try { thumbB64 = fs.readFileSync(path.join(DIR, assetName + '.thumb.webp')).toString('base64'); } catch {}
|
||||||
|
return {
|
||||||
|
file_name: '获奖证书.webp', mime: 'image/webp',
|
||||||
|
data_b64: data.toString('base64'), thumb_b64: thumbB64, size: data.length,
|
||||||
|
};
|
||||||
|
} catch { return undefined; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录标题 → 证书资源名(demo 播种时逐条匹配) */
|
||||||
|
export const CERT_BY_TITLE = [
|
||||||
|
[/信息安全竞赛/, 'cert-ctf'],
|
||||||
|
[/数学建模/, 'cert-math'],
|
||||||
|
[/蓝桥杯/, 'cert-lanqiao'],
|
||||||
|
[/计算机设计大赛/, 'cert-cd'],
|
||||||
|
[/英语竞赛/, 'cert-eng'],
|
||||||
|
[/ACM-ICPC/, 'cert-acm'],
|
||||||
|
[/优秀共青团员/, 'cert-wusi'],
|
||||||
|
];
|
||||||
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -1,54 +1,12 @@
|
|||||||
// 演示数据:仅当足迹表为空时写入,用于界面验收
|
// 演示数据播种(本地开发兼容入口)
|
||||||
import { getDb, buildSearchText, migrateSync, uuidv7, hlcNext, DEMO_ROWS, DEMO_SEMESTERS } from './db.mjs';
|
// 与云端 demo 账号共用同一份 demo_seed 命令层逻辑(app/server/db.mjs),
|
||||||
|
// 演示内容统一维护于 DEMO_ROWS / DEMO_SEMESTERS,竞赛/荣誉记录附带证书演示附件。
|
||||||
|
import { handleCommand, getDb, migrateSync } from './db.mjs';
|
||||||
|
|
||||||
const db = getDb();
|
const r = handleCommand('demo_seed', {});
|
||||||
const count = db.prepare('SELECT COUNT(*) AS c FROM footprint').get().c;
|
if (!r.seeded) {
|
||||||
if (count > 0) {
|
console.log('已存在 ' + r.footprints + ' 条记录 / ' + r.semesters + ' 个学期,跳过演示数据(如需重建请删除 app/.data 目录)');
|
||||||
console.log('已存在 ' + count + ' 条记录,跳过演示数据(如需重建请删除 app/.data 目录)');
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const types = Object.fromEntries(
|
|
||||||
db.prepare('SELECT id, name FROM footprint_type').all().map(r => [r.name, r.id])
|
|
||||||
);
|
|
||||||
const dims = Object.fromEntries(
|
|
||||||
db.prepare('SELECT id, name FROM dimension').all().map(r => [r.name, r.id])
|
|
||||||
);
|
|
||||||
|
|
||||||
// [type, title, role, level, grade, organizer, start, end, status, desc, result, tags, dims, extras, metrics]
|
|
||||||
// 数据统一维护在 ./db.mjs(DEMO_ROWS / DEMO_SEMESTERS)
|
|
||||||
|
|
||||||
const ins = db.prepare(`INSERT INTO footprint (type_id, title, role, level, award_grade, organizer,
|
|
||||||
start_date, end_date, status, description, result, tags, search_text, extras, metrics)
|
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
||||||
const setDim = db.prepare('INSERT OR IGNORE INTO footprint_dimension (footprint_id, dimension_id) VALUES (?,?)');
|
|
||||||
|
|
||||||
for (const [t, title, role, level, grade, organizer, start, end, status, desc, result, tags, dimNames, extras, metrics] of DEMO_ROWS) {
|
|
||||||
const search = buildSearchText(title, desc, result, tags, extras);
|
|
||||||
const r = ins.run(types[t], title, role, level, grade, organizer, start, end, status, desc, result,
|
|
||||||
JSON.stringify(tags), search, JSON.stringify(extras), JSON.stringify(metrics));
|
|
||||||
const id = Number(r.lastInsertRowid);
|
|
||||||
for (const name of dimNames) {
|
|
||||||
if (dims[name]) setDim.run(id, dims[name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('已写入 ' + DEMO_ROWS.length + ' 条演示记录');
|
|
||||||
|
|
||||||
// ---------- 学年学科成绩演示 ----------
|
|
||||||
const semIns = db.prepare('INSERT INTO semester (term, gpa, rank, total_students, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?)');
|
|
||||||
const couIns = db.prepare('INSERT INTO course_grade (semester_id, course_name, credit, score, grade_point, uuid, hlc, origin) VALUES (?,?,?,?,?,?,?,?)');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for (const s of DEMO_SEMESTERS) {
|
|
||||||
const h1 = hlcNext(db);
|
|
||||||
const sid = Number(semIns.run(s.term, s.gpa, s.rank, s.total, uuidv7(), h1, 'seed').lastInsertRowid);
|
|
||||||
for (const [name, credit, score, gp] of s.courses) {
|
|
||||||
couIns.run(sid, name, credit, score, gp, uuidv7(), hlcNext(db), 'seed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('已写入 ' + DEMO_SEMESTERS.length + ' 个学期、' + DEMO_SEMESTERS.reduce((a, s) => a + s.courses.length, 0) + ' 门课程成绩');
|
|
||||||
|
|
||||||
migrateSync(getDb());
|
migrateSync(getDb());
|
||||||
console.log('同步元数据(uuid/hlc/origin)已回填');
|
console.log('演示数据已写入:足迹 ' + r.footprints + ' 条、学期成绩 ' + r.semesters + ' 个(竞赛/荣誉记录附带证书演示附件)');
|
||||||
|
|||||||