feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
+111
-19
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user