fix: profile_upsert payload binding

This commit is contained in:
root
2026-10-17 12:05:56 +08:00
parent 7331111379
commit 500d59ba58
4 changed files with 61 additions and 0 deletions
+9
View File
@@ -203,6 +203,15 @@ CREATE TRIGGER IF NOT EXISTS footprint_au AFTER UPDATE OF search_text ON footpri
END;
CREATE INDEX IF NOT EXISTS idx_footprint_start ON footprint(start_date);
CREATE TABLE IF NOT EXISTS profile (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT,
content TEXT NOT NULL DEFAULT '{}',
deleted_at TEXT,
origin TEXT,
hlc TEXT
);
CREATE INDEX IF NOT EXISTS idx_footprint_type ON footprint(type_id);
CREATE INDEX IF NOT EXISTS idx_footprint_group ON footprint(group_id);
CREATE INDEX IF NOT EXISTS idx_footprint_hlc ON footprint(hlc);
+28
View File
@@ -1107,6 +1107,34 @@ function listFootprints(d, query) {
return { ok: true };
}
case 'profile_get': {
// 个人信息(单行 JSON):供个人信息页与简历生成使用
const row = d.prepare('SELECT * FROM profile WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 1').get();
let content = {};
if (row) { try { content = JSON.parse(row.content || '{}'); } catch { content = {}; } }
return { content };
}
case 'profile_upsert': {
const p = payload;
// 单行语义:更新现有行或插入首行
const content = JSON.stringify(p.content ?? {});
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);
if (existing) {
d.prepare('UPDATE profile SET content=?, hlc=? WHERE id=?').run(content, h, existing.id);
touchOplog(d, 'profile', existing.uuid, 'upsert', h);
} else {
const uuid = uuidv7();
d.prepare('INSERT INTO profile (content, uuid, hlc, origin) VALUES (?,?,?,?)')
.run(content, uuid, h, getDeviceId(d));
touchOplog(d, 'profile', uuid, 'upsert', h);
}
});
return { ok: true };
}
case 'view_list':
return { items: d.prepare('SELECT * FROM saved_view WHERE deleted_at IS NULL ORDER BY id').all().map(v => ({
...v, filter: JSON.parse(v.filter || '{}'), sort: JSON.parse(v.sort || '{}'), is_default: !!v.is_default
+10
View File
@@ -86,6 +86,16 @@ ok(tSet.body?.ok === true, 'attachment_set_thumb 回填缩略图', tSet.body);
const tGet = await api(A, 'POST', '/api/attachment_thumb', { id: rawUp.b?.data?.item?.id }, T);
ok(tGet.body?.data?.data_b64 === Buffer.from('RAWSUMB').toString('base64'), 'attachment_thumb 读回 raw 缩略图', tGet.body);
// 个人信息:upsert → get 往返
const profileContent = { info: { name: '张三', school: '重庆城市科技学院', major: '网络工程' }, resume: { selfIntro: '热爱安全研究' } };
const pUp = await api(A, 'POST', '/api/profile_upsert', { content: profileContent }, T);
ok(pUp.body?.ok === true, 'profile_upsert', pUp.body);
const pGet = await api(A, 'POST', '/api/profile_get', {}, T);
ok(pGet.body?.data?.content?.info?.name === '张三' && pGet.body?.data?.content?.resume?.selfIntro === '热爱安全研究', 'profile_get 往返一致', pGet.body);
const pUp2 = await api(A, 'POST', '/api/profile_upsert', { content: { ...profileContent, info: { ...profileContent.info, name: '李四' } } }, T);
const pGet2 = await api(A, 'POST', '/api/profile_get', {}, T);
ok(pGet2.body?.data?.content?.info?.name === '李四', 'profile 单行更新语义', pGet2.body);
// 删除整条记录 → 附件级联清理
const fpDel = await api(A, 'POST', '/api/footprint_delete', { id: fpId }, T);
ok(fpDel.body?.ok === true, '删除整条记录');
+14
View File
@@ -319,6 +319,20 @@ const server = http.createServer(async (req, res) => {
});
}
// 个人信息证件照:经图床网关直传 COS,返回访问 URL
if (req.method === 'POST' && route === '/api/profile_photo') {
const body = await readJson(req);
if (!LSKY_API || !LSKY_TOKEN) return send(400, false, '未配置图床,无法上传证件照');
try {
const buf = Buffer.from(body.data_b64 || '', 'base64');
if (!buf.length) return send(400, false, '空文件');
const up = await lskyUpload(buf, body.file_name || 'profile-photo.png', body.mime || 'image/png');
return send(200, true, { url: sanitizeImageUrl(up.url) });
} catch (e) {
return send(500, false, '证件照上传失败: ' + e.message);
}
}
// 附件上传(原始二进制 PUT):无 base64 膨胀;图床上传改为后台异步,保存立即返回
if (req.method === 'PUT' && route === '/sync/attachment/raw') {
const q = url.searchParams;