diff --git a/app/server/db.mjs b/app/server/db.mjs index 4e781e3..262d841 100644 --- a/app/server/db.mjs +++ b/app/server/db.mjs @@ -5,6 +5,7 @@ import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { fileURLToPath } from 'node:url'; +import { loadCert, CERT_BY_TITLE } from './demo-certs.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); 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', '备考软件设计师(中级)', '', ['软考'], ['职业发展'], {}, {}], + ['竞赛', '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', '参与企业授权渗透测试项目 3 个,编写测试报告', '获实习优秀评价', ['实习', '安全'], ['职业发展'], { position: '渗透测试实习生', mentor: '陈工' }, {}] @@ -446,27 +468,27 @@ export const DEMO_PROFILE = { resume: DEMO_RESUME }; -// 演示附件用真实 PNG(1x1,可在预览查看并进入综测证明材料) -const DEMO_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; - // 学期: { 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 = [ - { term: '2023-2024-1', gpa: 3.42, rank: 38, total: 156, courses: [ - ['高等数学(上)', 5, 78, 3.0, '必修课'], ['大学英语(一)', 3, 85, 3.7, '必修课'], ['程序设计基础', 4, 91, 4.0, '必修课'], - ['思想道德与法治', 2, 88, 3.9, '必修课'], ['体育(一)', 1, 82, 3.5, '必修课'] + { term: '2023-2024-1', gpa: 4.13, rank: 48, total: 156, courses: [ + ['高等数学(上)', 5, 78, 2.8, '必修课'], ['大学英语(一)', 3, 85, 3.5, '必修课'], ['程序设计基础', 4, 91, 4.1, '必修课'], + ['思想道德与法治', 2, 88, 3.8, '必修课'], ['体育(一)', 1, 82, 3.2, '必修课'] ]}, - { term: '2023-2024-2', gpa: 3.61, rank: 29, total: 156, courses: [ - ['高等数学(下)', 5, 84, 3.7, '必修课'], ['线性代数', 3, 76, 2.9, '必修课'], ['数据结构', 4, 90, 4.0, '必修课'], - ['大学英语(二)', 3, 87, 3.8, '必修课'], ['中国近现代史纲要', 2, 84, 3.5, '必修课'], ['体育(二)', 1, 86, 3.8, '必修课'] + { term: '2023-2024-2', gpa: 4.15, rank: 36, total: 156, courses: [ + ['高等数学(下)', 5, 84, 3.4, '必修课'], ['线性代数', 3, 76, 2.6, '必修课'], ['数据结构', 4, 90, 4.0, '必修课'], + ['大学英语(二)', 3, 87, 3.7, '必修课'], ['中国近现代史纲要', 2, 84, 3.4, '必修课'], ['体育(二)', 1, 86, 3.6, '必修课'] ]}, - { term: '2024-2025-1', gpa: 3.78, rank: 18, total: 156, courses: [ - ['概率论与数理统计', 3, 88, 3.9, '必修课'], ['操作系统', 4, 92, 4.0, '必修课'], ['计算机网络', 4, 86, 3.8, '必修课'], - ['数据库原理', 3, 94, 4.0, '必修课'], ['马克思主义基本原理', 2, 81, 3.3, '必修课'], ['体育(三)', 1, 88, 3.9, '必修课'] + { term: '2024-2025-1', gpa: 4.63, rank: 11, total: 156, courses: [ + ['概率论与数理统计', 3, 88, 3.8, '必修课'], ['操作系统', 4, 92, 4.18, '必修课'], ['计算机网络', 4, 86, 3.6, '必修课'], + ['数据库原理', 3, 94, 4.36, '必修课'], ['马克思主义基本原理', 2, 81, 3.1, '必修课'], ['体育(三)', 1, 88, 3.8, '必修课'] ]}, - { term: '2024-2025-2', gpa: 3.85, rank: 12, total: 156, courses: [ - ['软件工程', 3, 90, 4.0, '必修课'], ['信息安全导论', 3, 95, 4.0, '必修课'], ['密码学基础', 3, 89, 3.9, '必修课'], - ['Web 应用开发', 2, 96, 4.0, '专业选修课'], ['数字逻辑', 3, 79, 3.1, '必修课'], ['体育(四)', 1, 90, 4.0, '必修课'] + { term: '2024-2025-2', gpa: 4.65, rank: 7, total: 156, courses: [ + ['软件工程', 3, 90, 4.0, '必修课'], ['信息安全导论', 3, 95, 4.45, '必修课'], ['密码学基础', 3, 89, 3.9, '必修课'], + ['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), }}); n++; - // 演示附件:竞赛/荣誉记录附带证书图片(可预览、可进综测证明材料) - if (/竞赛|优秀共青团员/.test(String(title)) && status === 'finished') { - try { - 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 }); - } catch {} + // 演示附件:按标题匹配对应证书图片(可预览、可进综测证明材料) + const certKey = CERT_BY_TITLE.find((p2) => p2[0].test(String(title)))?.[1]; + if (certKey && status === 'finished') { + const cert = loadCert(certKey); + 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; diff --git a/app/server/demo-certs.mjs b/app/server/demo-certs.mjs new file mode 100644 index 0000000..01bd842 --- /dev/null +++ b/app/server/demo-certs.mjs @@ -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'], +]; diff --git a/app/server/demo-certs/cert-acm.thumb.webp b/app/server/demo-certs/cert-acm.thumb.webp new file mode 100644 index 0000000..80c3b37 Binary files /dev/null and b/app/server/demo-certs/cert-acm.thumb.webp differ diff --git a/app/server/demo-certs/cert-acm.webp b/app/server/demo-certs/cert-acm.webp new file mode 100644 index 0000000..d291a59 Binary files /dev/null and b/app/server/demo-certs/cert-acm.webp differ diff --git a/app/server/demo-certs/cert-cd.thumb.webp b/app/server/demo-certs/cert-cd.thumb.webp new file mode 100644 index 0000000..6318990 Binary files /dev/null and b/app/server/demo-certs/cert-cd.thumb.webp differ diff --git a/app/server/demo-certs/cert-cd.webp b/app/server/demo-certs/cert-cd.webp new file mode 100644 index 0000000..b131633 Binary files /dev/null and b/app/server/demo-certs/cert-cd.webp differ diff --git a/app/server/demo-certs/cert-ctf.thumb.webp b/app/server/demo-certs/cert-ctf.thumb.webp new file mode 100644 index 0000000..58ae8bc Binary files /dev/null and b/app/server/demo-certs/cert-ctf.thumb.webp differ diff --git a/app/server/demo-certs/cert-ctf.webp b/app/server/demo-certs/cert-ctf.webp new file mode 100644 index 0000000..b5208bd Binary files /dev/null and b/app/server/demo-certs/cert-ctf.webp differ diff --git a/app/server/demo-certs/cert-eng.thumb.webp b/app/server/demo-certs/cert-eng.thumb.webp new file mode 100644 index 0000000..04be04c Binary files /dev/null and b/app/server/demo-certs/cert-eng.thumb.webp differ diff --git a/app/server/demo-certs/cert-eng.webp b/app/server/demo-certs/cert-eng.webp new file mode 100644 index 0000000..8e60968 Binary files /dev/null and b/app/server/demo-certs/cert-eng.webp differ diff --git a/app/server/demo-certs/cert-lanqiao.thumb.webp b/app/server/demo-certs/cert-lanqiao.thumb.webp new file mode 100644 index 0000000..aa0cede Binary files /dev/null and b/app/server/demo-certs/cert-lanqiao.thumb.webp differ diff --git a/app/server/demo-certs/cert-lanqiao.webp b/app/server/demo-certs/cert-lanqiao.webp new file mode 100644 index 0000000..249a36e Binary files /dev/null and b/app/server/demo-certs/cert-lanqiao.webp differ diff --git a/app/server/demo-certs/cert-math.thumb.webp b/app/server/demo-certs/cert-math.thumb.webp new file mode 100644 index 0000000..09c4377 Binary files /dev/null and b/app/server/demo-certs/cert-math.thumb.webp differ diff --git a/app/server/demo-certs/cert-math.webp b/app/server/demo-certs/cert-math.webp new file mode 100644 index 0000000..7e62e67 Binary files /dev/null and b/app/server/demo-certs/cert-math.webp differ diff --git a/app/server/demo-certs/cert-wusi.thumb.webp b/app/server/demo-certs/cert-wusi.thumb.webp new file mode 100644 index 0000000..451ffe7 Binary files /dev/null and b/app/server/demo-certs/cert-wusi.thumb.webp differ diff --git a/app/server/demo-certs/cert-wusi.webp b/app/server/demo-certs/cert-wusi.webp new file mode 100644 index 0000000..96a1d55 Binary files /dev/null and b/app/server/demo-certs/cert-wusi.webp differ diff --git a/app/server/seed.mjs b/app/server/seed.mjs index 545516f..19cf450 100644 --- a/app/server/seed.mjs +++ b/app/server/seed.mjs @@ -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 count = db.prepare('SELECT COUNT(*) AS c FROM footprint').get().c; -if (count > 0) { - console.log('已存在 ' + count + ' 条记录,跳过演示数据(如需重建请删除 app/.data 目录)'); +const r = handleCommand('demo_seed', {}); +if (!r.seeded) { + console.log('已存在 ' + r.footprints + ' 条记录 / ' + r.semesters + ' 个学期,跳过演示数据(如需重建请删除 app/.data 目录)'); 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()); -console.log('同步元数据(uuid/hlc/origin)已回填'); +console.log('演示数据已写入:足迹 ' + r.footprints + ' 条、学期成绩 ' + r.semesters + ' 个(竞赛/荣誉记录附带证书演示附件)');