feat(api): 完善家谱系统API客户端契约
- 实现家谱管理相关方法,包括创建、详情、概览、我的家谱和选项查询 - 添加家谱加入申请功能,支持申请、审核、取消和待审核列表操作 - 集成通知详情获取方法和通知ID安全验证机制 - 完善功德记录、谱文、相册、视频、祭祀活动的完整CRUD操作契约 - 实现家谱成员管理功能,包含成员列表、更新、移除和转让所有者操作 - 优化路径ID验证逻辑,拒绝不安全的数值ID并提供明确错误提示 - 更新测试用例以验证所有新增API方法的路径和请求体白名单机制
This commit is contained in:
@@ -13,16 +13,6 @@
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
||||
'use strict';
|
||||
|
||||
var TARGET_LABELS = {
|
||||
'profile-family-home.html': '家谱主页',
|
||||
'profile-feed.html': '家族圈动态',
|
||||
'profile-growth.html': '成长记录',
|
||||
'profile-relative.html': '亲友往来',
|
||||
'profile-memo.html': '备忘录',
|
||||
'profile-tree.html': '世系图',
|
||||
'profile-generation.html': '字辈谱'
|
||||
};
|
||||
|
||||
function getTargetPage(search) {
|
||||
var params = new URLSearchParams(String(search === undefined && root.location ? root.location.search : search || '').replace(/^\?/, ''));
|
||||
var target = params.get('next') || '';
|
||||
@@ -30,38 +20,297 @@
|
||||
return /^profile-[a-z-]+\.html$/.test(target) ? target : 'profile-family-home.html';
|
||||
}
|
||||
|
||||
function buildStatus(quota, target) {
|
||||
function buildStatus(quota) {
|
||||
var source = quota || {};
|
||||
var details = [];
|
||||
var label = TARGET_LABELS[target] || '该家谱业务';
|
||||
|
||||
if (Number(source.createRemaining) >= 0) details.push('还可创建 ' + source.createRemaining + ' 部');
|
||||
if (Number(source.joinRemaining) >= 0) details.push('还可加入 ' + source.joinRemaining + ' 部');
|
||||
return '当前 PC 接口尚未提供“我的家谱”列表或家谱详情,不能伪造家谱编号进入“' + label + '”。' +
|
||||
(details.length ? ' 已读取额度:' + details.join(',') + '。' : '');
|
||||
if (Number(source.createRemaining) === -1) details.push('创建不限');
|
||||
else if (Number(source.createRemaining) >= 0) details.push('还可创建 ' + source.createRemaining + ' 部');
|
||||
if (Number(source.joinRemaining) === -1) details.push('加入不限');
|
||||
else if (Number(source.joinRemaining) >= 0) details.push('还可加入 ' + source.joinRemaining + ' 部');
|
||||
return details.length ? '家谱额度:' + details.join(',') + '。' : '';
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function trimOrUndefined(value) {
|
||||
var text;
|
||||
|
||||
if (value === undefined || value === null) return undefined;
|
||||
text = String(value).trim();
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
var text;
|
||||
|
||||
if (typeof value === 'number' && !Number.isSafeInteger(value)) return '';
|
||||
if (value === undefined || value === null) return '';
|
||||
text = String(value).trim();
|
||||
return /^[1-9][0-9]*$/.test(text) ? text : '';
|
||||
}
|
||||
|
||||
function buildGenealogyCreateBody(values) {
|
||||
var source = values || {};
|
||||
var body = {
|
||||
genealogyName: String(source.genealogyName || '').trim(),
|
||||
surname: String(source.surname || '').trim(),
|
||||
regionCode: String(source.regionCode || '').trim()
|
||||
};
|
||||
var optionalNames = ['ancestralHall', 'originPlace', 'addressDetail', 'intro'];
|
||||
var coverOssId = normalizeId(source.coverOssId);
|
||||
var visibility = trimOrUndefined(source.visibility);
|
||||
var joinMode = trimOrUndefined(source.joinMode);
|
||||
|
||||
optionalNames.forEach(function (name) {
|
||||
var value = trimOrUndefined(source[name]);
|
||||
|
||||
if (value !== undefined) body[name] = value;
|
||||
});
|
||||
if (source.coverOssId !== undefined && source.coverOssId !== null && source.coverOssId !== '') {
|
||||
if (coverOssId) body.coverOssId = coverOssId;
|
||||
else body.invalidCoverOssId = true;
|
||||
}
|
||||
if (visibility !== undefined) body.visibility = visibility;
|
||||
if (joinMode !== undefined) body.joinMode = joinMode;
|
||||
return body;
|
||||
}
|
||||
|
||||
function validateGenealogyCreateBody(body) {
|
||||
if (!body || !body.genealogyName) return '请填写谱名';
|
||||
if (!body.surname) return '请填写姓氏';
|
||||
if (!body.regionCode) return '请选择家谱所在地区';
|
||||
if (body.visibility !== undefined && ['0', '1', '2'].indexOf(body.visibility) < 0) return '可见范围选项无效';
|
||||
if (body.joinMode !== undefined && ['0', '1', '2'].indexOf(body.joinMode) < 0) return '加入方式选项无效';
|
||||
if (body.invalidCoverOssId) return '封面文件无效,请重新选择';
|
||||
return '';
|
||||
}
|
||||
|
||||
function canCreateGenealogy(quota) {
|
||||
var remaining = Number(quota && quota.createRemaining);
|
||||
|
||||
if (quota && typeof quota.canCreate === 'boolean') return quota.canCreate;
|
||||
return Number.isInteger(remaining) && (remaining === -1 || remaining > 0);
|
||||
}
|
||||
|
||||
function normalizeCreatedGenealogy(item) {
|
||||
var genealogyId = normalizeId(item && item.genealogyId);
|
||||
var genealogyName = String(item && item.genealogyName || '').trim();
|
||||
var surname = String(item && item.surname || '').trim();
|
||||
|
||||
if (!genealogyId || !genealogyName || !surname) return null;
|
||||
return {
|
||||
genealogyId: genealogyId,
|
||||
genealogyName: genealogyName,
|
||||
surname: surname,
|
||||
regionFullName: String(item && item.regionFullName || '')
|
||||
};
|
||||
}
|
||||
|
||||
function buildCreatedGenealogyUrl(item) {
|
||||
var genealogy = normalizeCreatedGenealogy(item);
|
||||
|
||||
return genealogy ? buildEntryUrl(genealogy.genealogyId, 'profile-family-home.html') : '';
|
||||
}
|
||||
|
||||
function shouldRedirectToLogin(api, error) {
|
||||
var status = error && (error.status || error.code);
|
||||
|
||||
return !api || !api.getToken || !api.getToken() || Number(status) === 401;
|
||||
}
|
||||
|
||||
function redirectToLogin(api) {
|
||||
if (api && api.clearToken) api.clearToken();
|
||||
if (!root.location) return;
|
||||
if (typeof root.location.replace === 'function') root.location.replace('login.html');
|
||||
else root.location.href = 'login.html';
|
||||
}
|
||||
|
||||
function getFormValues(form) {
|
||||
var values = {};
|
||||
|
||||
Array.prototype.forEach.call(form.querySelectorAll('[name]'), function (field) {
|
||||
values[field.name] = field.value;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function setCreateStatus(message) {
|
||||
var status = root.document && root.document.querySelector('[data-genealogy-create-status]');
|
||||
|
||||
if (status) status.textContent = message || '';
|
||||
}
|
||||
|
||||
function setCreatePending(pending) {
|
||||
var button = root.document && root.document.querySelector('[data-genealogy-create-submit]');
|
||||
|
||||
if (button) button.disabled = Boolean(pending);
|
||||
}
|
||||
|
||||
async function initGenealogyCreatePage() {
|
||||
var form = root.document && root.document.querySelector('[data-genealogy-create-form]');
|
||||
var quotaElement = root.document && root.document.querySelector('[data-genealogy-create-quota]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var quota;
|
||||
var writePending = false;
|
||||
|
||||
if (!form) return;
|
||||
if (shouldRedirectToLogin(api)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
quota = await api.genealogyQuota();
|
||||
if (quotaElement) quotaElement.textContent = buildStatus(quota) || '当前额度信息不可用';
|
||||
if (!canCreateGenealogy(quota)) {
|
||||
setCreateStatus('当前没有可用的家谱创建额度');
|
||||
setCreatePending(true);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
if (quotaElement) quotaElement.textContent = '家谱额度读取失败';
|
||||
setCreateStatus(error.message || '无法读取家谱创建额度');
|
||||
setCreatePending(true);
|
||||
return;
|
||||
}
|
||||
if (form.__genealogyCreateBound) return;
|
||||
form.__genealogyCreateBound = true;
|
||||
form.addEventListener('submit', async function (event) {
|
||||
var body;
|
||||
var validation;
|
||||
var created;
|
||||
var verified;
|
||||
|
||||
event.preventDefault();
|
||||
if (writePending) return;
|
||||
body = buildGenealogyCreateBody(getFormValues(form));
|
||||
validation = validateGenealogyCreateBody(body);
|
||||
if (validation) {
|
||||
setCreateStatus(validation);
|
||||
return;
|
||||
}
|
||||
delete body.invalidCoverOssId;
|
||||
writePending = true;
|
||||
setCreatePending(true);
|
||||
setCreateStatus('正在创建家谱…');
|
||||
try {
|
||||
created = normalizeCreatedGenealogy(await api.createGenealogy(body));
|
||||
if (!created) throw new Error('创建响应缺少有效家谱信息');
|
||||
verified = normalizeCreatedGenealogy(await api.genealogyDetail(created.genealogyId));
|
||||
if (!verified || verified.genealogyId !== created.genealogyId) throw new Error('创建后无法读取同一家谱');
|
||||
if (root.location) root.location.href = buildCreatedGenealogyUrl(verified);
|
||||
} catch (error) {
|
||||
if (shouldRedirectToLogin(api, error)) {
|
||||
redirectToLogin(api);
|
||||
return;
|
||||
}
|
||||
setCreateStatus(error.message || '创建家谱失败,请稍后重试');
|
||||
} finally {
|
||||
writePending = false;
|
||||
setCreatePending(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeGenealogy(item) {
|
||||
var source = item || {};
|
||||
var id = source.genealogyId === undefined || source.genealogyId === null
|
||||
? ''
|
||||
: String(source.genealogyId);
|
||||
|
||||
if (!/^[1-9][0-9]*$/.test(id) || !source.genealogyName) return null;
|
||||
return {
|
||||
genealogyId: id,
|
||||
genealogyName: String(source.genealogyName),
|
||||
surname: source.surname === undefined || source.surname === null ? '' : String(source.surname),
|
||||
regionFullName: source.regionFullName === undefined || source.regionFullName === null ? '' : String(source.regionFullName),
|
||||
memberCount: source.memberCount,
|
||||
personCount: source.personCount,
|
||||
roleType: source.roleType === undefined || source.roleType === null ? '' : String(source.roleType),
|
||||
canManage: source.canManage === true
|
||||
};
|
||||
}
|
||||
|
||||
function buildEntryUrl(genealogyId, target) {
|
||||
var params = new URLSearchParams();
|
||||
|
||||
params.set('genealogyId', String(genealogyId));
|
||||
return (target || 'profile-family-home.html') + '?' + params.toString();
|
||||
}
|
||||
|
||||
function renderGenealogies(container, data, target) {
|
||||
var items = Array.isArray(data) ? data.map(normalizeGenealogy).filter(Boolean) : [];
|
||||
|
||||
if (!container) return;
|
||||
if (!items.length) {
|
||||
container.innerHTML = '<div class="api-empty">当前账号还没有可进入的家谱</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = items.map(function (item) {
|
||||
var meta = [
|
||||
item.surname ? item.surname + '氏' : '',
|
||||
item.regionFullName,
|
||||
item.memberCount === undefined || item.memberCount === null ? '' : item.memberCount + ' 位成员',
|
||||
item.personCount === undefined || item.personCount === null ? '' : item.personCount + ' 位世系人物'
|
||||
].filter(Boolean).join(' · ');
|
||||
|
||||
return '<a class="module-row" data-genealogy-id="' + escapeHtml(item.genealogyId) +
|
||||
'" href="' + escapeHtml(buildEntryUrl(item.genealogyId, target)) + '">' +
|
||||
'<div><h3>' + escapeHtml(item.genealogyName) + '</h3><p>' + escapeHtml(meta) +
|
||||
'</p></div><span class="pill">' + (item.canManage ? '管理并进入' : '进入家谱') + '</span></a>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (root.document && root.document.querySelector('[data-genealogy-create-page]')) {
|
||||
await initGenealogyCreatePage();
|
||||
return;
|
||||
}
|
||||
var status = root.document && root.document.querySelector('[data-genealogy-entry-status]');
|
||||
var api = root.GenealogyApi && root.GenealogyApi.defaultClient;
|
||||
var target = getTargetPage();
|
||||
var container = root.document && root.document.querySelector('[data-genealogy-list="mine"]');
|
||||
var result;
|
||||
|
||||
if (!status) return;
|
||||
if (!api || !api.genealogyQuota) {
|
||||
if (!api || !api.genealogyQuota || !api.genealogiesMine) {
|
||||
status.textContent = '家谱入口接口初始化失败,请刷新后重试。';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
status.textContent = buildStatus(await api.genealogyQuota(), target);
|
||||
result = await Promise.all([api.genealogiesMine(), api.genealogyQuota()]);
|
||||
renderGenealogies(container, result[0], target);
|
||||
status.textContent = buildStatus(result[1]);
|
||||
} catch (error) {
|
||||
status.textContent = '无法读取家谱额度,请稍后重试。当前 PC 接口仍未提供家谱列表或详情,不能伪造家谱编号。';
|
||||
status.textContent = '无法读取我的家谱,请稍后重试。';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getTargetPage: getTargetPage,
|
||||
buildStatus: buildStatus,
|
||||
buildGenealogyCreateBody: buildGenealogyCreateBody,
|
||||
validateGenealogyCreateBody: validateGenealogyCreateBody,
|
||||
canCreateGenealogy: canCreateGenealogy,
|
||||
normalizeCreatedGenealogy: normalizeCreatedGenealogy,
|
||||
buildCreatedGenealogyUrl: buildCreatedGenealogyUrl,
|
||||
shouldRedirectToLogin: shouldRedirectToLogin,
|
||||
initGenealogyCreatePage: initGenealogyCreatePage,
|
||||
normalizeGenealogy: normalizeGenealogy,
|
||||
buildEntryUrl: buildEntryUrl,
|
||||
renderGenealogies: renderGenealogies,
|
||||
init: init
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user