feat(api): 完善家谱系统API客户端契约

- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询
- 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作
- 集成通知详情获取方法和通知ID安全验证机制
- 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约
- 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作
- 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示
- 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
fizzleaf
2026-07-29 16:57:27 +08:00
parent 59a72fb22b
commit fb1743aa2a
74 changed files with 13166 additions and 1320 deletions
+111 -19
View File
@@ -17,7 +17,7 @@
var documentRef = root.document;
var poemsById = Object.create(null);
var lastPreviewSignature = '';
var managementAccess = true;
var managementAccess = false;
var writePending = false;
function getApi() {
@@ -150,11 +150,16 @@
}
function splitPoemText(value) {
return String(value || '').trim().split(/[\s,;;、/|]+/).filter(Boolean);
var text = String(value || '').trim();
if (!text) return [];
if (/[\s,;;、/|]/.test(text)) {
return text.split(/[\s,;;、/|]+/).filter(Boolean);
}
return Array.from(text);
}
function getPoemId(item) {
var value = item && item.poemId;
function normalizeId(value) {
var text;
if (value === undefined || value === null || value === '') return '';
@@ -163,8 +168,13 @@
return /^\d+$/.test(text) ? text : '';
}
function getPoemId(item) {
return normalizeId(item && item.poemId);
}
function normalizeGenerationPoem(item) {
var poemId = getPoemId(item);
var genealogyId = normalizeId(item && item.genealogyId);
var generationNo = toIntegerOrUndefined(item && item.generationNo);
var generationText = trimOrUndefined(item && item.generationText);
var status = trimOrUndefined(item && item.status);
@@ -177,11 +187,24 @@
generationText: generationText,
status: status
};
if (genealogyId) poem.genealogyId = genealogyId;
if (item.genealogyNo !== undefined && item.genealogyNo !== null) poem.genealogyNo = String(item.genealogyNo);
if (item.genealogyName !== undefined && item.genealogyName !== null) poem.genealogyName = String(item.genealogyName);
if (item.description !== undefined && item.description !== null) poem.description = String(item.description);
if (toIntegerOrUndefined(item.sortOrder) !== undefined) poem.sortOrder = toIntegerOrUndefined(item.sortOrder);
if (item.remark !== undefined && item.remark !== null) poem.remark = String(item.remark);
return poem;
}
function normalizeGenerationAccess(genealogy) {
var canEditContent = Boolean(genealogy && genealogy.canEditContent === true);
return {
canEditContent: canEditContent,
listMethod: canEditContent ? 'generationPoemsManagement' : 'generationPoems'
};
}
function normalizeList(data) {
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
@@ -200,16 +223,20 @@
function renderGenerationPoem(poem) {
var details = [];
var stateText = poem.status === '1' ? '已停用' : '正常';
var actions = '';
if (poem.description) details.push('说明:' + poem.description);
if (poem.sortOrder !== undefined) details.push('排序:' + poem.sortOrder);
details.push('状态:' + stateText);
if (managementAccess) {
actions = '<div class="row-actions">' +
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poem.poemId) + '">编辑</button>' +
'<button class="pill" type="button" data-generation-status="' + escapeHtml(poem.poemId) + '">' + (poem.status === '1' ? '恢复' : '停用') + '</button>' +
'</div>';
}
return '<div class="module-row" data-generation-poem-id="' + escapeHtml(poem.poemId) + '">' +
'<div><h3>第 ' + escapeHtml(poem.generationNo) + ' 代:' + escapeHtml(poem.generationText) + '</h3><p>' + escapeHtml(details.join(' · ')) + '</p></div>' +
'<div class="row-actions">' +
'<button class="pill" type="button" data-generation-edit="' + escapeHtml(poem.poemId) + '">编辑</button>' +
'<button class="pill" type="button" data-generation-status="' + escapeHtml(poem.poemId) + '">' + (poem.status === '1' ? '恢复' : '停用') + '</button>' +
'</div></div>';
actions + '</div>';
}
function renderGenerationPoems(data) {
@@ -234,16 +261,71 @@
container.innerHTML = poems.map(renderGenerationPoem).join('');
}
function normalizePreview(data) {
if (!data || !Array.isArray(data.items)) return null;
function normalizeCount(value) {
var count = toIntegerOrUndefined(value);
return {
createCount: toIntegerOrUndefined(data.createCount),
updateCount: toIntegerOrUndefined(data.updateCount),
keepCount: toIntegerOrUndefined(data.keepCount),
disableCount: toIntegerOrUndefined(data.disableCount),
items: data.items
return count !== undefined && count >= 0 ? count : undefined;
}
function normalizePreviewItem(item) {
var action = item && item.action;
var generationNo = toIntegerOrUndefined(item && item.generationNo);
var poemId = getPoemId(item);
var oldText = trimOrUndefined(item && item.oldGenerationText);
var newText = trimOrUndefined(item && item.newGenerationText);
var oldStatus = trimOrUndefined(item && item.oldStatus);
var newStatus = trimOrUndefined(item && item.newStatus);
var normalized;
if (!generationNo || ['create', 'update', 'keep', 'disable'].indexOf(action) === -1) return null;
if (newStatus !== '0' && newStatus !== '1') return null;
if (action === 'create' && !newText) return null;
if (action !== 'create' && (!poemId || !oldText || (oldStatus !== '0' && oldStatus !== '1'))) return null;
if ((action === 'update' || action === 'keep') && !newText) return null;
normalized = {
generationNo: generationNo
};
if (poemId) normalized.poemId = poemId;
if (oldText) normalized.oldGenerationText = oldText;
if (newText) normalized.newGenerationText = newText;
if (oldStatus !== undefined) normalized.oldStatus = oldStatus;
normalized.newStatus = newStatus;
normalized.action = action;
if (item.warning !== undefined && item.warning !== null) normalized.warning = String(item.warning);
return normalized;
}
function normalizePreview(data) {
var counts;
var items;
var actualCounts = { create: 0, update: 0, keep: 0, disable: 0 };
if (!data || !Array.isArray(data.items) || !data.items.length) return null;
counts = {
createCount: normalizeCount(data.createCount),
updateCount: normalizeCount(data.updateCount),
keepCount: normalizeCount(data.keepCount),
disableCount: normalizeCount(data.disableCount)
};
if (Object.keys(counts).some(function (key) { return counts[key] === undefined; })) return null;
items = data.items.map(normalizePreviewItem);
if (items.some(function (item) { return !item; })) return null;
items.forEach(function (item) { actualCounts[item.action] += 1; });
if (counts.createCount !== actualCounts.create ||
counts.updateCount !== actualCounts.update ||
counts.keepCount !== actualCounts.keep ||
counts.disableCount !== actualCounts.disable) return null;
counts.items = items;
return counts;
}
function buildBatchConfirmMessage(disableMissing) {
if (disableMissing) {
return '确认按当前预览保存字辈,并停用未出现在文本中的后续世代吗?历史记录不会删除。';
}
return '确认按当前预览保存字辈吗?';
}
function renderBatchPreview(data) {
@@ -307,9 +389,15 @@
function refreshManagementControls() {
var disabled = !managementAccess || writePending;
queryAll('[data-generation-management]').forEach(function (element) {
element.hidden = !managementAccess;
});
queryAll('[data-generation-add], [data-generation-batch-action], [data-generation-edit], [data-generation-status]').forEach(function (control) {
control.disabled = disabled;
});
queryAll('[data-generation-management] textarea, [data-generation-management] input').forEach(function (control) {
control.disabled = disabled;
});
}
function setManagementEnabled(enabled) {
@@ -385,14 +473,16 @@
async function loadGenerationPoems() {
var api = getApi();
var genealogyId;
var access;
if (redirectUnauthorized(api)) return;
genealogyId = requireGenealogyId();
if (!genealogyId) return;
syncGenealogyLinks(genealogyId);
try {
renderGenerationPoems(await api.generationPoemsManagement(genealogyId));
setManagementEnabled(true);
access = normalizeGenerationAccess(await api.genealogyDetail(genealogyId));
setManagementEnabled(access.canEditContent);
renderGenerationPoems(await api[access.listMethod](genealogyId));
} catch (error) {
if (redirectUnauthorized(api, error)) return;
if (isForbidden(error)) {
@@ -533,7 +623,7 @@
setBatchStatus('请先预览当前内容,再保存');
return;
}
if (root.confirm && !root.confirm('确认按当前预览保存字辈吗?')) return;
if (root.confirm && !root.confirm(buildBatchConfirmMessage(body.disableMissing))) return;
setBatchStatus('正在保存…');
setWritePending(true);
try {
@@ -592,7 +682,9 @@
validateBatchBody: validateBatchBody,
splitPoemText: splitPoemText,
normalizeGenerationPoem: normalizeGenerationPoem,
normalizeGenerationAccess: normalizeGenerationAccess,
normalizePreview: normalizePreview,
buildBatchConfirmMessage: buildBatchConfirmMessage,
normalizeList: normalizeList,
shouldRedirectToLogin: shouldRedirectToLogin,
isForbidden: isForbidden,