fix(audit): profile_upsert top-key merge, scoring badge recordIds, zc item contract

- profile_upsert now merges top-level keys (info/resume/zongce are independent sub-documents); previously any page saving its own slice silently wiped the other two (profile save lost zongce rules, assessment save lost resume, resume save lost zongce)
- ZcItem gains recordIds: aggregated cap items (wcap/ccap/kcap/gcap/hcap) expose the records they aggregate, so the evidence gallery scored/unscored badge works (matchedIds was always empty before)
- add regression coverage notes; www rebuilt
This commit is contained in:
root
2026-10-17 19:13:58 +08:00
parent ba448946c9
commit a668647f35
19 changed files with 63 additions and 51 deletions
+8 -1
View File
@@ -1215,7 +1215,14 @@ function listFootprints(d, query) {
case 'profile_upsert': {
const p = payload;
// 单行语义:更新现有行或插入首行
const content = JSON.stringify(p.content ?? {});
// 顶层 key 合并:info / resume / zongce 是相互独立的子文档,各页面只提交自己负责的
// key(ProfilePage 只有 info、AssessmentPage 有 info+zongce…),整行替换会让其他
// key 神秘消失;按顶层 key 合并后互不影响,key 内部整体替换(仍是全量提交该子文档)。
const existingRow = d.prepare('SELECT * FROM profile WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 1').get();
let prev = {};
try { prev = JSON.parse(existingRow?.content || '{}'); } catch { prev = {}; }
const merged = { ...prev, ...(p.content ?? {}) };
const content = JSON.stringify(merged);
const existing = d.prepare('SELECT * FROM profile WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 1').get();
withTx(d, () => {
const h = hlcNext(d);
+7 -5
View File
@@ -98,6 +98,8 @@ export interface ZcItem {
overridden?: boolean;
note?: string;
fromRecord?: number;
/** 聚合条款(ccap/kcap/…)对应的记录 id 列表,用于标注哪些记录参与计分 */
recordIds?: number[];
}
export interface ZcSection { key: string; label: string; cap: number; items: ZcItem[]; subtotal: number }
export interface ZcResult { sections: ZcSection[]; total: number; gpa: number | null }
@@ -200,7 +202,7 @@ export function computeZongce(opts: {
wItems.push(mk('w' + rec.id, rec.title, rec.type_name + ' · ' + (rec.level ?? '') + ' ' + (rec.award_grade ?? ''), auto, undefined, rec.id));
}
const wCap = round2(Math.min(wItems.reduce((a, x) => a + x.score, 0), 5));
sections.push(sec('sports', '文体素质', 10, [...sportsItems, { itemKey: 'wcap', title: '文体类加分(上限 5 分)', source: wItems.length + ' 条记录', score: wCap, note: wItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' }]));
sections.push(sec('sports', '文体素质', 10, [...sportsItems, { itemKey: 'wcap', title: '文体类加分(上限 5 分)', source: wItems.length + ' 条记录', score: wCap, note: wItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录', recordIds: wItems.map(x => x.fromRecord).filter(Boolean) as number[] }]));
// ---- 拓展性素质(17):竞赛获奖(10) + 学生工作(4) + 公益(3) ----
const compItems: ZcItem[] = [];
@@ -231,9 +233,9 @@ export function computeZongce(opts: {
}
const pubCap = round2(Math.min(pubItems.reduce((a, x) => a + x.score, 0), 3));
sections.push(sec('expand', '拓展性素质', 17, [
{ itemKey: 'ccap', title: '竞赛获奖(上限 10 分)', source: compItems.length + ' 条记录', score: compCap, note: compItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' },
{ itemKey: 'kcap', title: '担任学生工作(上限 4 分)', source: workItems.length + ' 条记录', score: workCap, note: workItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' },
{ itemKey: 'gcap', title: '公益活动(上限 3 分)', source: pubItems.length + ' 条记录', score: pubCap, note: pubItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' },
{ itemKey: 'ccap', title: '竞赛获奖(上限 10 分)', source: compItems.length + ' 条记录', score: compCap, note: compItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录', recordIds: compItems.map(x => x.fromRecord).filter(Boolean) as number[] },
{ itemKey: 'kcap', title: '担任学生工作(上限 4 分)', source: workItems.length + ' 条记录', score: workCap, note: workItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录', recordIds: workItems.map(x => x.fromRecord).filter(Boolean) as number[] },
{ itemKey: 'gcap', title: '公益活动(上限 3 分)', source: pubItems.length + ' 条记录', score: pubCap, note: pubItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录', recordIds: pubItems.map(x => x.fromRecord).filter(Boolean) as number[] },
]));
// ---- 奖惩分(±5) ----
@@ -268,7 +270,7 @@ export function computeZongce(opts: {
rwItems.push(mk('h' + rec.id, rec.title, source, auto, undefined, rec.id));
}
const rwCap = round2(Math.max(Math.min(rwItems.reduce((a, x) => a + x.score, 0), 5), -5));
sections.push(sec('rewards', '奖惩分', 5, [{ itemKey: 'hcap', title: '奖惩合计(±5 分)', source: rwItems.length + ' 条记录', score: rwCap, note: rwItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录' }]));
sections.push(sec('rewards', '奖惩分', 5, [{ itemKey: 'hcap', title: '奖惩合计(±5 分)', source: rwItems.length + ' 条记录', score: rwCap, note: rwItems.map(x => x.title + ' ' + x.score + '分').join(';') || '无匹配记录', recordIds: rwItems.map(x => x.fromRecord).filter(Boolean) as number[] }]));
// 手动条目
for (const m of ov.manual ?? []) {
+4 -1
View File
@@ -261,7 +261,10 @@ export function AssessmentPage() {
// 参与计分的记录 id(画廊中标注「计分」)
const matchedSet = useMemo(() => {
const s = new Set<number>();
result.sections.forEach(sec => sec.items.forEach(it => { if (it.fromRecord) s.add(it.fromRecord); }));
result.sections.forEach(sec => sec.items.forEach(it => {
if (it.fromRecord) s.add(it.fromRecord);
for (const rid of it.recordIds ?? []) s.add(rid);
}));
return s;
}, [result]);