diff --git a/components/AppDialog.vue b/components/AppDialog.vue index d45649c..33db97a 100644 --- a/components/AppDialog.vue +++ b/components/AppDialog.vue @@ -12,7 +12,8 @@ :aria-label="title" tabindex="-1" @click.stop - @keydown.esc.stop="cancel" + @keydown.esc.stop.prevent="cancel" + @keydown.tab="trapFocus" > @@ -62,6 +63,38 @@ const emit = defineEmits(["confirm", "cancel", "close"]); const dialogRef = ref(null); let previousFocus = null; +const getDialogElement = () => { + const target = dialogRef.value; + if (target && typeof target.querySelectorAll === "function") return target; + if (target?.$el && typeof target.$el.querySelectorAll === "function") return target.$el; + return null; +}; + +const trapFocus = (event) => { + if (typeof document === "undefined") return; + const dialog = getDialogElement() || event.currentTarget; + if (!dialog || typeof dialog.querySelectorAll !== "function") return; + const focusable = Array.from( + dialog.querySelectorAll( + 'button:not([disabled]), [href], input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ).filter((element) => element.offsetParent !== null); + if (focusable.length === 0) { + event.preventDefault(); + dialog.focus?.(); + return; + } + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } +}; + watch( () => props.visible, async (visible) => { @@ -69,7 +102,7 @@ watch( if (visible) { previousFocus = document.activeElement; await nextTick(); - dialogRef.value?.focus?.(); + getDialogElement()?.focus?.(); return; } previousFocus?.focus?.(); diff --git a/components/AppTabbar.vue b/components/AppTabbar.vue index 5cd1440..774dfda 100644 --- a/components/AppTabbar.vue +++ b/components/AppTabbar.vue @@ -1,7 +1,7 @@ @@ -78,8 +82,16 @@ const switchTab = (item) => { flex-direction: column; align-items: center; justify-content: center; + margin: 0; padding: 4rpx 0; box-sizing: border-box; + border: 0; + border-radius: 0; + background: transparent; + line-height: normal; +} +.tab-item::after { + border: 0; } .tab-item--pressed { opacity: 0.72; diff --git a/components/ModulePage.vue b/components/ModulePage.vue deleted file mode 100644 index b02d3c0..0000000 --- a/components/ModulePage.vue +++ /dev/null @@ -1,516 +0,0 @@ - - - - - - diff --git a/components/PageHeader.vue b/components/PageHeader.vue index db3439c..9739712 100644 --- a/components/PageHeader.vue +++ b/components/PageHeader.vue @@ -8,37 +8,40 @@ class="header-texture" src="/static/assets/foundation/opaque/root-header-cinnabar.jpg" mode="scaleToFill" + aria-hidden="true" /> - + @@ -47,49 +50,57 @@ - + + @@ -180,6 +191,17 @@ const goBack = () => { background: transparent; line-height: normal; } +.header-brand { + display: grid; + width: 88rpx; + min-height: 88rpx; + place-items: center; + margin: 0; + padding: 0; + border: 0; + background: transparent; + line-height: normal; +} .header-icon-button::after, .header-back::after, .header-action::after { @@ -214,13 +236,27 @@ const goBack = () => { } .header-action { + display: flex; width: 100%; + min-height: 88rpx; + align-items: center; + justify-content: flex-end; margin: 0; padding: 0; border: 0; background: transparent; + color: #ffe3a7; font-size: 27rpx; line-height: normal; + text-align: right; +} +.header-action-placeholder { + width: 100%; + min-height: 88rpx; +} +.header-action--root { + color: #ffe3a7; + font-weight: 700; } .header-back { @@ -244,11 +280,6 @@ const goBack = () => { .header-back--pressed { opacity: 0.62; } -.header-action { - color: #ffe3a7; - text-align: right; -} - .header-title { flex: 1; overflow: hidden; diff --git a/components/TacVerification.vue b/components/TacVerification.vue new file mode 100644 index 0000000..6fafcb8 --- /dev/null +++ b/components/TacVerification.vue @@ -0,0 +1,564 @@ + + + + + + + diff --git a/components/tree/TreeMemberForm.vue b/components/tree/TreeMemberForm.vue deleted file mode 100644 index 17087b7..0000000 --- a/components/tree/TreeMemberForm.vue +++ /dev/null @@ -1,334 +0,0 @@ - - - - - - diff --git a/data/mock.js b/data/mock.js index 9c6574d..0ad428b 100644 --- a/data/mock.js +++ b/data/mock.js @@ -1,3 +1,8 @@ +import { + GENEALOGY_ACCESS_PRESET, + getGenealogyAccessPresetLabel, +} from '../utils/genealogy-contracts.js' + export const currentUser = { id: 1, name: '汤文远', @@ -7,7 +12,7 @@ export const currentUser = { export const genealogies = [ { - id: 1001, + id: '1001', surname: '汤', name: '汤氏家谱', hall: '敦睦堂', @@ -15,12 +20,17 @@ export const genealogies = [ memberCount: 158, updatedAt: '2024-05-12', activeCount: 8, - visibility: '仅成员可见', + accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY, membership: 'created', - motto: '敦亲睦族,敬祖传家。' + motto: '敦亲睦族,敬祖传家。', + ancestorName: '汤文远', + parentName: '汤氏中华总谱', + branchName: '洛阳主支', + source: '由洛阳汤氏族人整理并维护', + publicDescription: '公开展示家谱身份、地区、堂号与支系信息。' }, { - id: 1002, + id: '1002', surname: '汤', name: '汤氏宗谱', hall: '承志堂', @@ -28,43 +38,658 @@ export const genealogies = [ memberCount: 286, updatedAt: '2024-04-28', activeCount: 15, - visibility: '公开可申请', + accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, membership: 'joined', - motto: '继往开来,世守家风。' + motto: '继往开来,世守家风。', + ancestorName: '汤正明', + parentName: '汤氏鲁西总谱', + branchName: '济宁主支', + source: '由济宁汤氏族人整理并维护', + publicDescription: '公开展示家谱身份、地区、堂号与支系信息。' } ] -export const treeMembers = [ - { id: 1, name: '汤文远', relation: '始祖', generation: 1, years: '1940—2012', branch: '主支', x: 50, y: 7 }, - { id: 2, name: '汤正国', relation: '长子', generation: 2, years: '1965—', branch: '主支', x: 20, y: 38 }, - { id: 3, name: '汤正华', relation: '次子', generation: 2, years: '1968—', branch: '主支', x: 50, y: 38 }, - { id: 4, name: '汤正强', relation: '三子', generation: 2, years: '1972—', branch: '支系', x: 80, y: 38 }, - { id: 5, name: '汤凯', relation: '长孙', generation: 3, years: '1992—', branch: '主支', x: 16, y: 70 }, - { id: 6, name: '汤悦', relation: '长女', generation: 3, years: '1995—', branch: '主支', x: 35, y: 70 }, - { id: 7, name: '汤晨', relation: '次子', generation: 3, years: '1998—', branch: '主支', x: 54, y: 70 }, - { id: 8, name: '汤昊', relation: '三子', generation: 3, years: '2001—', branch: '支系', x: 73, y: 70 }, - { id: 9, name: '汤宁', relation: '四女', generation: 3, years: '2005—', branch: '支系', x: 90, y: 70 } +// G01、G05、G06 与 G08 共用这一份家谱展示夹具。成员关系只在 +// genealogies 和本地创建预览中出现;公开搜索结果绝不能据此获得管理权限。 +const toPublicProjection = ({ membership: _membership, ...genealogy }) => genealogy +export const publicGenealogies = [ + { + id: '2001', + surname: '汤', + name: '汤氏南阳宗谱', + hall: '敦睦堂', + location: '河南·南阳', + parentName: '汤氏中华总谱', + branchName: '南阳主支', + manager: '管理员 汤文礼', + certification: '资料已认证', + memberCount: 428, + activeCount: 316, + updatedAt: '2026-07-12', + relation: 'available', + source: '由南阳汤氏族人整理并维护', + publicDescription: '公开展示家谱身份、地区、堂号与支系信息;成员资料和世系详情仅向已加入成员开放。', + motto: '敦亲睦族,敬祖传家。', + accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, + ancestorName: '汤文远' + }, + { + ...toPublicProjection(genealogies[1]), + manager: '管理员 汤文远', + certification: '资料已认证', + relation: 'joined' + }, + { + id: '2003', + surname: '汤', + name: '汤氏济宁宗谱', + hall: '敬宗堂', + location: '山东·济宁', + parentName: '汤氏鲁西总谱', + branchName: '济宁主支', + manager: '管理员 汤正明', + certification: '管理员已实名', + memberCount: 286, + updatedAt: '2026-07-08', + relation: 'pending', + accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, + }, + { + id: '2004', + surname: '汤', + name: '汤氏清河家谱', + hall: '思源堂', + location: '山东·临清', + parentName: '汤氏鲁西总谱', + branchName: '清河支系', + manager: '管理员 汤志成', + certification: '资料已认证', + memberCount: 96, + updatedAt: '2026-07-05', + relation: 'rejected', + accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, + }, + { + id: '2005', + surname: '汤', + name: '汤氏汝南支谱', + hall: '崇本堂', + location: '河南·驻马店', + parentName: '汤氏中原总谱', + branchName: '汝南三支', + manager: '管理员 汤国安', + certification: '管理员已实名', + memberCount: 72, + updatedAt: '2026-07-02', + relation: 'removed', + accessPreset: GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, + }, + { + ...toPublicProjection(genealogies[0]), + manager: '创建者 当前用户', + certification: '资料待完善', + relation: 'owned' + } ] -export const familyFeeds = [ - { id: 1, author: '汤正华', title: '清明祭祖通知', content: '本周六上午举行清明祭祖,敬请各支系族亲相互转告。', date: '今天 09:30', type: '族务' }, - { id: 2, author: '汤悦', title: '老宅修缮旧影', content: '整理出一组敦睦堂老宅照片,已收入家族相册。', date: '昨天 20:18', type: '相册' } -] - -export const familyContent = { - article: [{ id: 1, title: '敦睦堂家训摘录', summary: '孝友传家,勤俭立业,敬祖睦族。', date: '2025年4月' }], - album: [{ id: 1, title: '敦睦堂老宅旧影', summary: '收录修缮前后的珍贵照片,共 6 张。', date: '2025年3月' }], - ceremony: [{ id: 1, title: '清明祭祖', summary: '本周六上午举行祭祖仪式,请族人相互转告。', date: '3 天后' }], - record: [{ id: 1, title: '老宅修缮备忘', summary: '屋脊木构加固方案已确认,等待施工。', date: '今天' }] +const localCreatedGenealogy = { + id: 'local-created-genealogy', + surname: '汤', + name: '本地创建预览', + hall: '堂号待补', + location: '所在地待补', + memberCount: 1, + activeCount: 1, + updatedAt: '尚未同步', + accessPreset: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY, + localPreview: true, + motto: '当前仅为本地流程预览,尚未提交服务器。', + ancestorName: '待补', + parentName: '无上级谱', + branchName: '主支', + source: '本地创建流程预览', + publicDescription: '尚未同步到服务器。' } -export const notifications = [ - { id: 1, title: '有新的入谱申请', content: '陈先生申请加入四川武胜汤氏族谱,等待你的审核。', time: '10:24', unread: true }, - { id: 2, title: '祭祀活动提醒', content: '清明祭祖将在 3 天后开始。', time: '昨天', unread: true }, - { id: 3, title: '相册有新照片', content: '汤悦上传了 6 张老宅修缮照片。', time: '4月9日', unread: false } +const localGenealogyPreviews = new Map([ + [localCreatedGenealogy.id, localCreatedGenealogy] +]) +let localPreviewSequence = 0 + +const buildLocalGenealogyPreview = (id, draft, previous = {}) => { + const accessPreset = Object.values(GENEALOGY_ACCESS_PRESET).includes(draft.accessPreset) + ? draft.accessPreset + : GENEALOGY_ACCESS_PRESET.MEMBER_ONLY + return { + ...previous, + id, + surname: String(draft.surname || '').trim(), + name: String(draft.name || '').trim(), + hall: String(draft.hall || '').trim() || '堂号待补', + location: String(draft.location || '').trim(), + memberCount: 1, + activeCount: 1, + updatedAt: '尚未同步', + accessPreset, + localPreview: true, + motto: '当前仅为本地流程预览,尚未提交服务器。', + ancestorName: previous.ancestorName || '待补', + parentName: '无上级谱', + branchName: '主支', + source: '本地创建流程预览', + publicDescription: `尚未同步到服务器;访问规则为“${getGenealogyAccessPresetLabel(accessPreset)}”。` + } +} + +// G03 与 G05 共用这一份临时预览 owner。它只在当前运行实例中保存用户刚提交的 +// 快照,不建立成员关系、不写持久存储,也不冒充后端创建结果。 +export const createLocalGenealogyPreview = (draft) => { + localPreviewSequence += 1 + const id = `local-created-${Date.now().toString(36)}-${localPreviewSequence}` + localGenealogyPreviews.set(id, buildLocalGenealogyPreview(id, draft)) + return id +} + +export const updateLocalGenealogyPreview = (genealogyId, draft) => { + const normalizedId = String(genealogyId || '') + const preview = localGenealogyPreviews.get(normalizedId) + if (!preview) return null + localGenealogyPreviews.set( + normalizedId, + buildLocalGenealogyPreview(normalizedId, draft, preview) + ) + return normalizedId +} + +export const updateLocalGenealogyPreviewAncestor = (genealogyId, draft) => { + const normalizedId = String(genealogyId || '') + const preview = localGenealogyPreviews.get(normalizedId) + if (!preview) return null + const updated = { + ...preview, + ancestorName: String(draft.personName || '').trim() || '待补', + ancestor: Object.freeze({ ...draft }) + } + localGenealogyPreviews.set(normalizedId, updated) + return updated +} + +export const removeLocalGenealogyPreview = (genealogyId) => { + const normalizedId = String(genealogyId || '') + if (!normalizedId || normalizedId === localCreatedGenealogy.id) return false + return localGenealogyPreviews.delete(normalizedId) +} + +export const findGenealogyFixture = (genealogyId) => { + const normalizedId = String(genealogyId || '') + return [...genealogies, ...publicGenealogies] + .find((item) => String(item.id) === normalizedId) || + localGenealogyPreviews.get(normalizedId) || null +} + +// 这里只解析本地视觉夹具的显示角色,不代表后端授权。成员关系仍由 +// genealogies[*].membership 唯一持有;localPreview 只能进入无业务入口的预览态。 +export const getGenealogyFixtureAccess = (genealogyId) => { + const normalizedId = String(genealogyId || '') + const fixture = findGenealogyFixture(normalizedId) + const memberFixture = genealogies.find((item) => item.id === normalizedId) + const publicFixture = publicGenealogies.find((item) => item.id === normalizedId) + const access = fixture?.localPreview + ? 'preview' + : memberFixture?.membership === 'created' + ? 'owner' + : memberFixture?.membership === 'joined' + ? 'member' + : 'public' + const relation = fixture?.localPreview + ? 'preview' + : memberFixture?.membership === 'created' + ? 'owned' + : memberFixture?.membership === 'joined' + ? 'joined' + : publicFixture?.relation || 'unknown' + return Object.freeze({ + viewMode: access === 'public' || access === 'preview' ? access : 'member', + accessRole: access === 'owner' || access === 'member' ? access : 'guest', + relation, + canView: + Boolean(fixture?.localPreview) || + access === 'owner' || + access === 'member' || + fixture?.accessPreset === GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY, + canApply: + fixture?.accessPreset === GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY && + ['available', 'rejected', 'removed'].includes(relation) + }) +} + +// 搜索结果的公开性与操作资格消费同一访问预设。成员可看到自己已经加入或 +// 创建的私密家谱;陌生账号只能看到明确为 PUBLIC_APPLY 的投影。 +export const isGenealogySearchVisible = (genealogyId) => { + const fixture = findGenealogyFixture(genealogyId) + if (!fixture) return false + const access = getGenealogyFixtureAccess(genealogyId) + return ( + access.accessRole !== 'guest' || + fixture.accessPreset === GENEALOGY_ACCESS_PRESET.PUBLIC_APPLY + ) +} + +// T01、T03—T08 与旧 mock API 共用这一份成员夹具。utils/api.js 是当前阶段 +// 唯一允许写入裸数组的模块;页面必须通过下方查询函数取得深拷贝,避免页面 +// 表单或关系投影反向污染世系树和其他页面。 +export const treeMembers = [ + { + id: '101', appUserId: '2001', genealogyId: '1001', parentId: '', name: '汤文远', relation: '始祖', + generation: 12, generationName: '文字辈', branch: '主支', years: '1940—2012', + birthDate: '1940-03-01', deathDate: '2012-08-16', birthplace: '河南南阳', + status: 'deceased', summary: '一生敦亲睦族,参与整理家族旧谱。', + note: '始祖 · 档案完整', + relatives: [ + { personId: '102', relation: '长子' }, + { personId: '103', relation: '次子' } + ] + }, + { + id: '102', appUserId: '2002', genealogyId: '1001', parentId: '101', name: '汤正国', relation: '长子', + generation: 13, generationName: '正字辈', branch: '长房', years: '1965—', + birthDate: '1965-05-12', deathDate: '', birthplace: '河南洛阳', + status: 'privacy', summary: '负责长房资料核对。', + note: '家谱管理员', + relatives: [ + { personId: '101', relation: '父亲' }, + { personId: '104', relation: '长子' }, + { personId: '105', relation: '女儿' } + ] + }, + { + id: '103', appUserId: '2003', genealogyId: '1001', parentId: '101', name: '汤正华', relation: '次子', + generation: 13, generationName: '正字辈', branch: '二房', years: '资料受限', + birthDate: '1968-09-03', deathDate: '', birthplace: '河南洛阳', + status: 'forbidden', summary: '资料仍在补充。', + note: '资料待补充', + relatives: [ + { personId: '101', relation: '父亲' }, + { personId: '106', relation: '长子' } + ] + }, + { + id: '104', appUserId: '2004', genealogyId: '1001', parentId: '102', name: '汤凯', relation: '长孙', + generation: 14, generationName: '凯字辈', branch: '长房', years: '1992—', + birthDate: '1992-06-01', deathDate: '', birthplace: '河南洛阳', + status: 'privacy', summary: '协助整理年轻一代成员资料。', + note: '档案已核对', + relatives: [{ personId: '102', relation: '父亲' }] + }, + { + id: '105', appUserId: '2005', genealogyId: '1001', parentId: '102', name: '汤悦', relation: '长孙女', + generation: 14, generationName: '凯字辈', branch: '长房', years: '1995—', + birthDate: '1995-04-18', deathDate: '', birthplace: '河南洛阳', + status: 'privacy', summary: '参与家族影像与口述资料整理。', + note: '档案已核对', + relatives: [{ personId: '102', relation: '父亲' }] + }, + { + id: '106', appUserId: '2006', genealogyId: '1001', parentId: '103', name: '汤晨', relation: '次孙', + generation: 14, generationName: '凯字辈', branch: '二房', years: '1998—', + birthDate: '1998-11-09', deathDate: '', birthplace: '河南洛阳', + status: 'privacy', summary: '二房成员资料已完成初步核对。', + note: '档案已核对', + relatives: [{ personId: '103', relation: '父亲' }] + } ] -export const joinApplications = [ - { id: 1, name: '汤志成', phone: '139****6421', relation: '自述为汤正华堂侄', appliedAt: '今天 10:24', status: 'PENDING' }, - { id: 2, name: '汤雨薇', phone: '136****2798', relation: '自述为汤正国之女', appliedAt: '昨天 18:02', status: 'PENDING' } +const cloneTreeMemberFixture = (member) => ({ + ...member, + relatives: member.relatives.map((relative) => ({ ...relative })) +}) + +export const listTreeMemberFixtures = (genealogyId) => { + const normalizedGenealogyId = String(genealogyId || '') + if (!normalizedGenealogyId) return [] + return treeMembers + .filter((member) => member.genealogyId === normalizedGenealogyId) + .map(cloneTreeMemberFixture) +} + +export const findTreeMemberFixture = (genealogyId, personId) => { + const normalizedGenealogyId = String(genealogyId || '') + const normalizedPersonId = String(personId || '') + if (!normalizedGenealogyId || !normalizedPersonId) return null + const member = treeMembers.find( + (item) => item.genealogyId === normalizedGenealogyId && item.id === normalizedPersonId + ) + return member ? cloneTreeMemberFixture(member) : null +} + +// 页面展示只能消费这个受控投影。privacy/forbidden 成员在 owner 边界即删除 +// 生平、居住地、支系、亲属等字段,避免组件先取得完整对象再依赖模板隐藏。 +const projectTreeMemberPresentation = (member) => { + if (!['privacy', 'forbidden'].includes(member.status)) { + return cloneTreeMemberFixture(member) + } + return { + id: member.id, + appUserId: member.appUserId, + genealogyId: member.genealogyId, + name: member.name, + relation: member.relation, + generation: member.generation, + status: member.status + } +} + +export const listTreeMemberPresentationFixtures = (genealogyId) => { + const normalizedGenealogyId = String(genealogyId || '') + if (!normalizedGenealogyId) return [] + return treeMembers + .filter((member) => member.genealogyId === normalizedGenealogyId) + .map(projectTreeMemberPresentation) +} + +export const findTreeMemberPresentationFixture = (genealogyId, personId) => { + const normalizedGenealogyId = String(genealogyId || '') + const normalizedPersonId = String(personId || '') + if (!normalizedGenealogyId || !normalizedPersonId) return null + const member = treeMembers.find( + (item) => item.genealogyId === normalizedGenealogyId && item.id === normalizedPersonId + ) + return member ? projectTreeMemberPresentation(member) : null +} + +// F01—F09 共用这一组只读内容夹具。家谱 ID 与实体 ID 共同构成身份;页面和 +// mock API 只能通过下方 list/find 查询取得深拷贝,不能把页面草稿写回正式列表。 +const familyFeeds = [ + { + id: '1', genealogyId: '1001', tag: '团圆记忆', time: '今天 10:24', + title: '端午家宴', content: '今年端午全家相聚,长辈讲起祖居旧事,孩子们也为大家拍下了新的全家福。饭后我们把照片和口述片段整理进家族档案,让这份热闹成为往后仍能翻看的共同记忆。', + author: '汤正国', + comments: [ + { id: '1', author: '汤淑华', time: '今天 10:42', content: '一家人能常常相聚,就是最珍贵的福气。' }, + { id: '2', author: '汤文清', time: '今天 11:08', content: '照片已经整理好了,晚些时候放进春节团圆相册。' } + ] + }, + { + id: '2', genealogyId: '1001', tag: '家族通知', time: '昨天 18:02', + title: '修谱资料征集', content: '请家人补充老照片中的人物姓名、拍摄时间和地点。无法确认的信息也可以先写下线索,由熟悉往事的长辈共同核对。', + author: '谱主', comments: [] + } +] + +const familyArticles = [ + { + id: '101', genealogyId: '1001', category: '家风家训', title: '孝友传家的日常', + summary: '从敬老、睦亲与守信的小事里,看见家风如何代代相传。', author: '汤文正', + updatedAt: '2024 年 5 月 12 日', + paragraphs: [ + '孝友传家,不只在族谱序言里,也在一家人每日的言行中。长辈以宽厚待晚辈,晚辈以耐心照料长辈,亲友之间守信互助,便是最朴素也最长久的家风。', + '勤俭并非一味节省,而是珍惜所得、量入为出,也愿意在家人需要时伸出援手。家中每一代人都可以用自己的方式,把这份分寸与担当继续传下去。', + '敬祖睦宗,最终是为了让今天的家人彼此认识、彼此关心。记录姓名与世代之外,也应留下真实的生活、共同经历和温暖记忆。' + ] + }, + { + id: '102', genealogyId: '1001', category: '家族往事', title: '祖居门前的那棵桂花树', + summary: '长辈口述的旧居记忆,以及每年中秋一家人相聚的故事。', author: '汤淑华', + updatedAt: '2024 年 5 月 10 日', + paragraphs: [ + '祖居门前曾有一棵桂花树。每到中秋,院里都是清甜的香气,远道回来的家人也总能循着那股味道找到家门。', + '后来房屋几经修缮,桂花树仍被大家小心保留下来。它见过孩子长大,也见过长辈把往事一遍遍讲给后来人。' + ] + }, + { + id: '103', genealogyId: '1001', category: '族谱序言', title: '续修族谱序', + summary: '说明本次续修的缘起、资料来源与共同参与的家人。', author: '谱主', + updatedAt: '2024 年 5 月 8 日', + paragraphs: [ + '本次续修以旧谱、碑记、户籍资料和长辈口述为基础,由家人共同核对补充。凡暂不能确认之处,均保留来源和疑问,留待后续查证。', + '愿这份记录不仅理清世系,也能保存家风、人物与共同记忆。' + ] + } +] + +const familyAlbums = [ + { + id: '201', genealogyId: '1001', name: '2024 春节团圆', updatedAt: '今天更新', + description: '三代家人的团圆饭与院前合影', cover: '/static/assets/modules/family/f08/f08-reunion-hero.png', + photos: [ + { id: '20101', src: '/static/assets/modules/family/f08/f08-reunion-hero.png', alt: '春节团圆时三代家人的合影', caption: '除夕团圆 · 2024' }, + { id: '20102', src: '/static/assets/modules/family/f08/f08-family-portrait.png', alt: '家人在院落前的春节合影', caption: '院前合影 · 2024' }, + { id: '20103', src: '/static/assets/modules/family/f08/f08-reunion-table.png', alt: '家人围坐吃年夜饭', caption: '围桌守岁 · 2024' }, + { id: '20104', src: '/static/assets/modules/family/f08/f08-ancestral-home.png', alt: '祖居院落的复古旧照', caption: '祖居旧影 · 1968' }, + { id: '20105', src: '/static/assets/modules/family/f08/f08-ancestral-portrait.png', alt: '老一辈家人在祖居门前的合影', caption: '门前合影 · 1972' } + ] + }, + { + id: '202', genealogyId: '1001', name: '祖居旧影', updatedAt: '5 月 10 日更新', + description: '祖居、旧物与长辈珍藏的老照片', cover: '/static/assets/modules/family/f08/f08-ancestral-home.png', + photos: [ + { id: '20201', src: '/static/assets/modules/family/f08/f08-ancestral-home.png', alt: '祖居院落的复古旧照', caption: '祖居旧影 · 1968' }, + { id: '20202', src: '/static/assets/modules/family/f08/f08-ancestral-portrait.png', alt: '老一辈家人在祖居门前的合影', caption: '门前合影 · 1972' } + ] + }, + { + id: '203', genealogyId: '1001', name: '儿童成长', updatedAt: '持续更新', + description: '记录孩子们每一个值得珍藏的瞬间', cover: '/static/assets/modules/family/f08/f08-family-portrait.png', + photos: [ + { id: '20301', src: '/static/assets/modules/family/f08/f08-family-portrait.png', alt: '家人在院落前的春节合影', caption: '院前合影 · 2024' } + ] + } +] + +const cloneFamilyFeedFixture = (feed) => ({ + ...feed, + comments: feed.comments.map((comment) => ({ ...comment })) +}) +const cloneFamilyArticleFixture = (article) => ({ + ...article, + paragraphs: [...article.paragraphs] +}) +const cloneFamilyAlbumFixture = (album) => ({ + ...album, + photoCount: album.photos.length, + photos: album.photos.map((photo) => ({ ...photo })) +}) + +const listScopedFamilyFixtures = (items, clone, genealogyId) => { + const normalizedGenealogyId = String(genealogyId || '') + if (!normalizedGenealogyId) return [] + return items + .filter((item) => item.genealogyId === normalizedGenealogyId) + .map(clone) +} + +const findScopedFamilyFixture = (items, clone, genealogyId, entityId) => { + const normalizedGenealogyId = String(genealogyId || '') + const normalizedEntityId = String(entityId || '') + if (!normalizedGenealogyId || !normalizedEntityId) return null + const item = items.find( + (candidate) => candidate.genealogyId === normalizedGenealogyId && candidate.id === normalizedEntityId + ) + return item ? clone(item) : null +} + +export const listFamilyFeedFixtures = (genealogyId) => + listScopedFamilyFixtures(familyFeeds, cloneFamilyFeedFixture, genealogyId) +export const findFamilyFeedFixture = (genealogyId, feedId) => + findScopedFamilyFixture(familyFeeds, cloneFamilyFeedFixture, genealogyId, feedId) +export const listFamilyArticleFixtures = (genealogyId) => + listScopedFamilyFixtures(familyArticles, cloneFamilyArticleFixture, genealogyId) +export const findFamilyArticleFixture = (genealogyId, articleId) => + findScopedFamilyFixture(familyArticles, cloneFamilyArticleFixture, genealogyId, articleId) +export const listFamilyAlbumFixtures = (genealogyId) => + listScopedFamilyFixtures(familyAlbums, cloneFamilyAlbumFixture, genealogyId) +export const findFamilyAlbumFixture = (genealogyId, albumId) => + findScopedFamilyFixture(familyAlbums, cloneFamilyAlbumFixture, genealogyId, albumId) + +// R03—R11 共用这一组只读记录夹具。它们只描述本地视觉预览,不冒充 +// OpenAPI 响应;每个实体都显式携带 genealogyId,详情必须由家谱与实体 ID +// 共同定位。页面只能通过下方 list/find 选择器取得深拷贝,不能把草稿、完成 +// 状态或新增记录写回这里。 +const relativeRecordFixtures = [ + { + relativeId: '301', genealogyId: '1001', relativeName: '汤文正一家', relationName: '族亲', + eventName: '新春贺礼', eventTime: '2024-02-10', giftAmount: 600, + recordContent: '新春团拜时赠予长辈的心意' + }, + { + relativeId: '302', genealogyId: '1001', relativeName: '汤淑华', relationName: '家族长辈', + eventName: '寿宴礼单', eventTime: '2024-04-18', giftAmount: 1000, + recordContent: '汤老先生八十寿辰' + } +] + +const ceremonyFixtures = [ + { + ceremonyId: '501', genealogyId: '1001', ceremonyType: '祭祖', ceremonyTitle: '清明祭祖', + ceremonyTime: '2025-04-04', location: '汤氏宗祠', + ceremonyDesc: '缅怀先祖,整理祭扫礼序,并由长辈讲述家族往事。', + invitees: [ + { inviteeUserId: '2001', inviteStatus: '1' }, + { inviteeUserId: '2002', inviteStatus: '0' } + ] + }, + { + ceremonyId: '502', genealogyId: '1001', ceremonyType: '家宴', ceremonyTitle: '中秋家宴', + ceremonyTime: '2025-09-17', location: '祖居院落', + ceremonyDesc: '家人团聚,共叙近况并整理年度家族影像。', + invitees: [{ inviteeUserId: '2003', inviteStatus: '1' }] + }, + { + ceremonyId: '503', genealogyId: '1001', ceremonyType: '团拜', ceremonyTitle: '新春团拜', + ceremonyTime: '2025-01-29', location: '家族礼堂', + ceremonyDesc: '新春相聚,向长辈问安并记录家族近况。', invitees: [] + } +] + +const growthRecordFixtures = [ + { + recordId: '701', genealogyId: '1001', lineagePersonId: '101', recordTitle: '整理第一册旧谱', + recordDate: '1988-03', recordContent: '第一次独立整理家中保存的旧谱与口述线索。' + }, + { + recordId: '702', genealogyId: '1001', lineagePersonId: '104', recordTitle: '第一次参与修谱', + recordDate: '2024-06', recordContent: '协助长辈核对照片人物与出生年份。' + } +] + +const memoFixtures = [ + { + memoId: '801', genealogyId: '1001', memoTitle: '修谱资料整理', remindTime: '本月底前', + memoContent: '补充老照片中的人物姓名和拍摄时间。', completedLabel: '待办理' + }, + { + memoId: '802', genealogyId: '1001', memoTitle: '重阳敬老活动', remindTime: '10 月 11 日上午', + memoContent: '在祠堂集合,并确认接送长辈的车辆。', completedLabel: '已完成' + } +] + +const meritRecordFixtures = [ + { + meritId: '901', genealogyId: '1001', meritTypeLabel: '共同修缮', meritTitle: '修缮祠堂', + donorName: '汤氏家人共同参与', meritTime: '2024 年春', amount: null, + meritContent: '协助整理院落、修补门窗并登记旧物。' + }, + { + meritId: '902', genealogyId: '1001', meritTypeLabel: '奖学助学', meritTitle: '支持后辈勤学', + donorName: '家族教育小组', meritTime: '2024 年夏', amount: null, + meritContent: '为家族中努力求学的孩子提供书籍与经验分享。' + } +] + +const cloneRecordFixture = (record) => ({ + ...record, + ...(Array.isArray(record.invitees) + ? { invitees: record.invitees.map((invitee) => ({ ...invitee })) } + : {}) +}) + +const listScopedRecordFixtures = (records, genealogyId) => { + const normalizedGenealogyId = String(genealogyId || '') + if (!normalizedGenealogyId) return [] + return records + .filter((record) => record.genealogyId === normalizedGenealogyId) + .map(cloneRecordFixture) +} + +const findScopedRecordFixture = (records, idField, genealogyId, entityId) => { + const normalizedGenealogyId = String(genealogyId || '') + const normalizedEntityId = String(entityId || '') + if (!normalizedGenealogyId || !normalizedEntityId) return null + const record = records.find( + (item) => item.genealogyId === normalizedGenealogyId && item[idField] === normalizedEntityId + ) + return record ? cloneRecordFixture(record) : null +} + +export const listRelativeRecordFixtures = (genealogyId) => + listScopedRecordFixtures(relativeRecordFixtures, genealogyId) +export const findRelativeRecordFixture = (genealogyId, relativeId) => + findScopedRecordFixture(relativeRecordFixtures, 'relativeId', genealogyId, relativeId) +export const listCeremonyFixtures = (genealogyId) => + listScopedRecordFixtures(ceremonyFixtures, genealogyId) +export const findCeremonyFixture = (genealogyId, ceremonyId) => + findScopedRecordFixture(ceremonyFixtures, 'ceremonyId', genealogyId, ceremonyId) +export const listGrowthRecordFixtures = (genealogyId, lineagePersonId = '') => { + const records = listScopedRecordFixtures(growthRecordFixtures, genealogyId) + const normalizedPersonId = String(lineagePersonId || '') + return normalizedPersonId + ? records.filter((record) => record.lineagePersonId === normalizedPersonId) + : records +} +export const findGrowthRecordFixture = (genealogyId, recordId) => + findScopedRecordFixture(growthRecordFixtures, 'recordId', genealogyId, recordId) +export const listMemoFixtures = (genealogyId) => + listScopedRecordFixtures(memoFixtures, genealogyId) +export const findMemoFixture = (genealogyId, memoId) => + findScopedRecordFixture(memoFixtures, 'memoId', genealogyId, memoId) +export const listMeritRecordFixtures = (genealogyId) => + listScopedRecordFixtures(meritRecordFixtures, genealogyId) +export const findMeritRecordFixture = (genealogyId, meritId) => + findScopedRecordFixture(meritRecordFixtures, 'meritId', genealogyId, meritId) + +export const notifications = [ + { + id: 'review-1', + title: '申请待审核', + content: '汤志成申请加入汤氏家谱,请核实亲属关系。', + body: '汤志成申请加入汤氏家谱,请核实申请人的亲属关系与世代信息后完成审核。', + time: '今天 10:28', + source: '汝南汤氏家谱', + unread: true, + targetType: 'GENEALOGY_REVIEW', + targetParams: { genealogyId: '1001' }, + targetLabel: '前往入谱审核' + }, + { + id: 'approved', + title: '入谱申请已通过', + content: '你申请加入汝南汤氏家谱的请求已通过。', + body: '你申请加入汝南汤氏家谱的请求已通过,现在可以查看家谱与家族动态。', + time: '昨天 18:10', + source: '汝南汤氏家谱', + unread: false, + targetType: 'GENEALOGY_HOME', + targetParams: { genealogyId: '1001' }, + targetLabel: '查看我的家谱' + } +] + +const cloneNotificationFixture = (notification) => ({ + ...notification, + targetParams: { ...notification.targetParams } +}) + +export const listNotificationFixtures = () => + notifications.map(cloneNotificationFixture) + +export const findNotificationFixture = (notificationId) => { + const normalizedId = String(notificationId || '') + if (!normalizedId) return null + const notification = notifications.find((item) => item.id === normalizedId) + return notification ? cloneNotificationFixture(notification) : null +} + +export const joinApplications = [ + { id: '1', name: '汤志成', phone: '139****6421', relation: '自述为汤正华堂侄', appliedAt: '今天 10:24', status: 'PENDING' }, + { id: '2', name: '汤雨薇', phone: '136****2798', relation: '自述为汤正国之女', appliedAt: '昨天 18:02', status: 'PENDING' } ] diff --git a/data/page-catalog.js b/data/page-catalog.js deleted file mode 100644 index eb85305..0000000 --- a/data/page-catalog.js +++ /dev/null @@ -1,46 +0,0 @@ -// 全量视觉交付的本地页面目录:只保存页面内容与状态,样式由 ModulePage 的六类母版统一维护。 -export const pageCatalog = { - - f03: { number: 'F-03', title: '动态详情', subtitle: '一段属于家人的共同记忆', template: 'detail', sections: [['端午家宴', '今年端午全家相聚,留下了许多温暖照片。'], ['家人评论', '愿家族和睦兴旺,岁岁平安。']], action: '写下评论' }, - f04: { number: 'F-04', title: '谱文', subtitle: '收录家族文章、家训与往事', template: 'list', sections: [['家风家训', '代代相传的处世之道'], ['家族往事', '珍贵的口述与文字记录'], ['族谱序言', '一部家谱的起源']], action: '新建谱文' }, - f05: { - number: 'F-05', - title: '谱文详情', - subtitle: '家族文字档案', - template: 'detail', - sections: [['家风家训', '孝友传家,勤俭立业;敬祖睦宗,诚实待人。'], ['收录时间', '2024 年 5 月 12 日']], - action: '收藏谱文', - states: { - empty: { - eyebrow: '谱文已失效', - title: '这篇谱文已无法查看', - copy: '内容可能已被作者删除或取消公开,请返回谱文列表查看其他内容。', - action: '返回谱文列表', - actionMode: 'back', - }, - }, - }, - f07: { number: 'F-07', title: '家族相册', subtitle: '让每一张照片都回到家人身边', template: 'list', sections: [['2024 春节团圆', '18 张照片 · 更新于今天'], ['祖居旧影', '32 张照片 · 家族档案'], ['儿童成长', '46 张照片 · 持续更新']], action: '新建相册' }, - r01: { number: 'R-01', title: '人物录', subtitle: '记录家族中值得铭记的人', template: 'list', sections: [['汤文正', '家谱管理员 · 第 18 世'], ['汤淑华', '家族长辈 · 第 17 世'], ['汤文清', '青年代表 · 第 19 世']], action: '新建人物' }, - r03: { number: 'R-03', title: '贺礼簿', subtitle: '记录每一份家人之间的心意', template: 'list', sections: [['新春贺礼', '2024 年春节 · 12 条记录'], ['寿宴礼单', '汤老先生八十寿辰'], ['添丁祝福', '家族新成员的祝愿']], action: '新增贺礼' }, - r04: { number: 'R-04', title: '编辑贺礼', subtitle: '如实留下礼仪往来记录', template: 'form', fields: ['贺礼名称', '赠送人', '日期', '备注'], action: '保存贺礼', note: '删除确认功能待接入' }, - r05: { number: 'R-05', title: '礼仪活动', subtitle: '家族的重要时刻与礼序', template: 'list', sections: [['清明祭祖', '2024 年 4 月 4 日'], ['中秋家宴', '2024 年 9 月 17 日'], ['新春团拜', '2025 年 1 月 29 日']], action: '新建礼仪' }, - r06: { number: 'R-06', title: '礼仪详情', subtitle: '清明祭祖', template: 'detail', sections: [['活动说明', '缅怀先祖,凝聚家人,共叙家风传承。'], ['参与人员', '已登记 26 位家人']], action: '编辑活动' }, - r07: { number: 'R-07', title: '编辑礼仪', subtitle: '补全时间、地点与活动说明', template: 'form', fields: ['活动名称', '活动日期', '举办地点', '活动说明'], action: '保存活动', note: '发布提醒功能待接入' }, - r08: { number: 'R-08', title: '成长日志', subtitle: '珍藏生命成长中的每一个瞬间', template: 'timeline', sections: [['第一次叫爸爸', '2024 年 3 月 · 家人共同记录'], ['入园第一天', '2024 年 9 月 · 留下勇敢的笑脸']], action: '记录成长' }, - r09: { number: 'R-09', title: '人生事', subtitle: '记录值得回望的人生节点', template: 'timeline', sections: [['大学毕业', '2018 年 6 月 · 新的起点'], ['结为连理', '2022 年 10 月 · 家人见证']], action: '新增人生事' }, - r10: { number: 'R-10', title: '家族备忘', subtitle: '把重要的事留给未来的自己', template: 'list', sections: [['修谱资料整理', '请在本月底前补充老照片信息'], ['重阳敬老活动', '10 月 11 日上午在祠堂集合']], action: '新增备忘' }, - r11: { number: 'R-11', title: '功德记录', subtitle: '致谢每一份对家族的守护', template: 'list', sections: [['修缮祠堂', '2024 年春 · 家族共同支持'], ['奖学助学', '2024 年夏 · 鼓励后辈勤学']], action: '新增记录' }, - - n02: { number: 'N-02', title: '消息详情', subtitle: '家谱提醒', template: 'detail', sections: [['申请待审核', '汤文清申请加入家谱,请在审核页面查看。'], ['收到时间', '今天 10:28']], action: '标记已读' }, - - m02: { number: 'M-02', title: '个人资料', subtitle: '完善你的家谱身份信息', template: 'form', fields: ['头像昵称', '真实姓名', '常住地区', '个人简介'], action: '保存资料', note: '头像上传服务待接入' }, - m03: { number: 'M-03', title: '账号与安全', subtitle: '保护你的家谱账号', template: 'settings', sections: [['登录密码', '建议定期更新'], ['绑定手机号', '用于登录和安全验证'], ['登录设备', '暂无异常设备']], action: '检查账号安全' }, - m04: { number: 'M-04', title: '修改密码', subtitle: '设置新的安全密码', template: 'form', fields: ['当前密码', '新密码', '确认新密码'], action: '确认修改', note: '密码服务待接入' }, - m05: { number: 'M-05', title: '修改手机号', subtitle: '更换用于登录的手机号', template: 'form', fields: ['当前手机号', '新手机号', '验证码'], action: '确认更换', note: '短信验证服务待接入' }, - m06: { number: 'M-06', title: '帮助中心', subtitle: '常见问题与使用说明', template: 'settings', sections: [['如何创建家谱', '从家谱首页点击新建家谱'], ['如何邀请家人', '通过申请审核或邀请码加入'], ['资料隐私说明', '个人信息按权限展示']], action: '联系家谱助手' }, - m07: { number: 'M-07', title: '意见反馈', subtitle: '你的建议会让家谱更好', template: 'form', fields: ['反馈类型', '问题描述', '联系方式'], action: '提交反馈', note: '反馈服务待接入' }, - m08: { number: 'M-08', title: '应用推广', subtitle: '邀请亲友共建家族记忆', template: 'detail', sections: [['邀请家人', '将家谱分享给家人,共同补全家族故事。'], ['推广说明', '邀请功能与奖励规则待接入。']], action: '生成邀请海报' }, - m09: { number: 'M-09', title: 'VIP 与订单', subtitle: '家谱服务与订阅记录', template: 'status', badge: '礼', lead: '当前没有订阅订单', note: 'VIP 服务开放后会在这里展示权益与订单', action: '查看服务说明' }, - m10: { number: 'M-10', title: '关于家谱', subtitle: '传承每一段值得珍藏的家族记忆', template: 'settings', sections: [['用户协议', '功能待接入'], ['隐私政策', '功能待接入'], ['当前版本', '1.0.0'], ['退出登录', '仅本地演示']], action: '查看版本说明' } -} diff --git a/design-pipeline/manifests/application-runtime-assets.json b/design-pipeline/manifests/application-runtime-assets.json index 854cb9c..27c2e33 100644 --- a/design-pipeline/manifests/application-runtime-assets.json +++ b/design-pipeline/manifests/application-runtime-assets.json @@ -42,8 +42,6 @@ { "id": "app-modules-genealogy-transparent-shortcut-generation-poem", "output": "static/assets/modules/genealogy/transparent/shortcut-generation-poem.png", "width": 96, "height": 96, "alpha": true, "bytes": 8169, "sha256": "688e593fa88925ea00acf37570852a2dca9e1a505ba3b8107e0ddd3d146a6f4e", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-shortcut-members", "output": "static/assets/modules/genealogy/transparent/shortcut-members.png", "width": 96, "height": 96, "alpha": true, "bytes": 8931, "sha256": "c89824e8e19210dd746f56019a48c9a833abe4a210b222758a56c3d25dec9d0c", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-genealogy-transparent-shortcut-tree", "output": "static/assets/modules/genealogy/transparent/shortcut-tree.png", "width": 96, "height": 96, "alpha": true, "bytes": 6636, "sha256": "cafe12a3fa800ca79d6c559baa533d7e26124ec8ac0072482ddb89ac2167422d", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-notification-transparent-module-content-frame", "output": "static/assets/modules/notification/transparent/module-content-frame.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, - { "id": "app-modules-notification-transparent-module-field-frame", "output": "static/assets/modules/notification/transparent/module-field-frame.png", "width": 2132, "height": 443, "alpha": true, "bytes": 1065770, "sha256": "bdc65f33e1dbe05ae75562519b0080e2e5cdbe2dba720a6ac49b56b99be75ceb", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-notification-transparent-n01-notice-card", "output": "static/assets/modules/notification/transparent/n01-notice-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-profile-transparent-m01-profile-summary-card", "output": "static/assets/modules/profile/transparent/m01-profile-summary-card.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, { "id": "app-modules-profile-transparent-module-content-frame", "output": "static/assets/modules/profile/transparent/module-content-frame.png", "width": 2139, "height": 675, "alpha": true, "bytes": 254885, "sha256": "01cf572ed3f20d99e10a21fee6834dced385e71d6b14693b04c0cb49560a6fd3", "provenance": "committed-binary", "rebuildable": false }, diff --git a/docs/家谱项目全量治理实施计划.md b/docs/家谱项目全量治理实施计划.md index 4459b36..d15d47a 100644 --- a/docs/家谱项目全量治理实施计划.md +++ b/docs/家谱项目全量治理实施计划.md @@ -1,13 +1,13 @@ -# 导航栈统一与大规模世系树实施计划 +# 家谱项目全量治理实施计划 > **执行要求:** 实施时使用 `superpowers:executing-plans` 按任务顺序执行,并在每个批次结束后由主代理、Lagrange、Bernoulli 三人复核;宣称批次或阶段完成前必须使用 `superpowers:verification-before-completion`。步骤使用复选框跟踪。用户已明确授权三人依据证据直接确定技术方案,不再为可从页面、接口和现有合同推出的细节反复请示。 > > 日期:2026-07-22 -> 状态:阶段 0 已完成;导航栈设计与 T01 大规模世系树设计已完成三人终审;业务代码尚未开始迁移 +> 状态:阶段 0、导航任务 1—10、TAC 认证客户端任务 20—22、领域上下文任务 24 与 M07 反馈客户端任务 25 已完成;家谱工作区任务 26、M06 帮助任务 27、个人资料读取任务 28、通知读取任务 29、通知已读写入任务 30、个人资料写入任务 31、M10 服务端退出任务 32、M04 密码凭证任务 33、M05 手机号换绑任务 34 和 G03 原子创建任务 35 已完成三人审查及 OpenAPI 红灯;T01、认证、工作区、G03、帮助、profile、通知、logout、password 与 phone-change 后端门禁明确阻塞,MuMu 原生矩阵待执行;当前继续无依赖业务域接口审查 -**目标:** 先以测试先行方式消除全项目导航栈歧义,再在新后端图窗口合同通过后,把 T01 建成可稳定阅读几十代、几百代且连接线连续的大规模世系图。 +**目标:** 以测试先行方式消除全项目导航栈歧义,完成可安全上线的认证闭环,并在新后端图窗口合同通过后,把 T01 建成可稳定阅读几十代、几百代且连接线连续的大规模世系图;随后逐域关闭真实接口、领域持久化、无障碍与发布门禁。 -**架构:** 导航阶段只新增两个所有者:`utils/navigation-routes.js` 持有活动路由语义,`utils/navigation.js` 持有全部 Uni 导航调用;页面按认证、G、T、F、R、N/M 独立迁移。导航阶段完全验收后,T01 阶段先锁定 Apifox 的规范图窗口合同,再按“规范化与校验 → 可见投影 → 布局 → Scene 与空间索引 → 单 Canvas 视口 → 页面交互”实施;两个阶段之间设置不可跨越的硬门禁。 +**架构:** 导航阶段由 `utils/navigation-routes.js` 持有活动路由语义、`utils/navigation.js` 持有全部 Uni 导航调用。T01 先锁定 Apifox 规范图窗口合同,再按“规范化与校验 → 可见投影 → 布局 → Scene 与空间索引 → 单 Canvas 视口 → 页面交互”实施。认证由场景合同、TAC 浮层、供应商适配器和 HTTP owner 四层组成;所有阶段各有独立硬门禁,外部接口红灯只阻止其消费者上线,不阻止无依赖业务域继续审查。 **技术栈:** UniApp、Vue 3、JavaScript、SCSS、PowerShell、Node.js、OpenAPI 3.0.1、Apifox、MuMu Android、ADB、App-vue renderjs、Canvas 2D。 @@ -27,7 +27,7 @@ - 普通内容高度由内容决定;弹窗只允许视口 `max-height` 和内部滚动。 - G01—G10 只有在本阶段测试或 MuMu 提供明确全局回归证据时才改动,且只改与当前阶段直接相关的行。 - 用户可见页面提示使用“某某某堂侄”等通用表达;fixture 与 mock 中的姓名不做无关清理。 -- 导航阶段允许只迁移 T01 的现有导航调用;导航门禁通过前,不得实施 T01 新图合同、投影、布局、Scene、Canvas 或新接口接入。T01 完成前不实施短信状态机、领域持久化或全局文字层级第二轮。 +- 导航阶段允许只迁移 T01 的现有导航调用;导航门禁通过前,不得实施 T01 新图合同、投影、布局、Scene、Canvas 或新接口接入。导航门禁关闭后,各业务域必须保持独立批次;T01 或认证因后端合同阻塞时,可以继续不依赖该合同的接口审查和客户端批次,但不得伪造服务端成功或混合所有权。 ## 阶段硬门禁 @@ -35,6 +35,8 @@ 2. **T01 接口门禁:** 用户重新导出的 JSON/YAML 同时包含规范 `LineageGraphWindow`、概览和定位接口,图合同测试通过;当前递归合同不得由客户端兼容。 3. **T01 几何门禁:** 103→106、普通节点 4rpx 间隙和 10×12 多父干线三个已知缺陷先红后绿,十条“连线永不断”不变量全部自动验证。 4. **阶段隔离:** 任一门禁未通过时只修复本阶段问题,不提前混入下一阶段代码。 +5. **认证后端门禁:** `API-AUTH-TAC-001`—`004` 关闭、同版本 JSON/YAML 通过 `tests/auth-tac-openapi-contract.ps1`、真实 challenge 不再空 500,才允许把 `runtimeConfig.mode` 改为 `remote`。 +6. **认证 Android 发布门禁:** 非拖动等价验证、真实票据原子消费和三人 MuMu/TalkBack/外接键盘证据齐全,`tests/auth-android-accessibility-release-gate.ps1` 才能由 `ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED` 转绿。 ## 文件职责图 @@ -49,7 +51,7 @@ ### 导航阶段修改 -- `components/AppTabbar.vue`、`components/PageHeader.vue`、`components/ModulePage.vue`。 +- `components/AppTabbar.vue`、`components/PageHeader.vue`、G01/F01/M01 三个根页消费者。 - `pages/auth/*.vue`、`pages/genealogy/*.vue`、`pages/tree/*.vue`、`pages/family/*.vue`、`pages/records/*.vue`、`pages/notification/*.vue`、`pages/profile/*.vue` 中实际包含导航行为的文件。 - 与上述行为直接对应的现有聚焦合同;旧的 `fallbackUrl`、原始 URL 和直接 Uni 调用断言必须同轮删除。 @@ -111,72 +113,21 @@ if ($failed.Count -gt 0) { throw "失败合同:$($failed -join ', ')" } - 路由项:`{ path, kind, parent, parentParamMap, requiredParams, optionalParams, allowedSources, resultOperations }`;所有数组和映射均冻结。 - 路径统一以 `/pages/...` 表示;ID 参数均为字符串;`sourceKey` 是网关保留参数,不进入页面业务参数数组。 -路由注册表必须完整采用下表,不得从页面中再复制第二份父子关系: +`utils/navigation-routes.js` 已成为 52 个活动页面路由语义的唯一数据所有者;实施计划、页面、测试和映射总表都不得再维护完整父页、参数、来源或结果操作影子表。注册表路径和编号由 `pages.json` 独立交叉,具体字段只在该模块修改。 -| 路由键 | 类型 | 规范父页 | 必填参数 | 可选业务参数 | 允许来源 | -| --- | --- | --- | --- | --- | --- | -| A01 | 认证根页 | — | — | — | — | -| A04 | 普通页 | A01 | — | — | A01 | -| A05 | 普通页 | A01 | — | — | A01 | -| G01 | 根页 | — | — | `genealogyId` | — | -| G03 | 流程页 | G01 | — | `genealogyId` | G01 | -| G05 | 普通页 | G01 | `genealogyId` | — | G01,G03,G06,G09 | -| G06 | 普通页 | G01 | — | `mode` | G01,G03,G09 | -| G08 | 流程页 | G06 | `genealogyId` | `source` | G05,G06,G09 | -| G09 | 普通页 | G01 | — | `status` | G01,G06,G08 | -| G10 | 普通页 | G01 | `genealogyId` | — | G01,G05,N01,N02 | -| G11 | 流程页 | G05 | `genealogyId` | — | G05 | -| G12 | 流程页 | G05 | `genealogyId` | `startGeneration,currentGeneration` | G01,G05 | -| T01 | 普通页 | G05 | `genealogyId` | `selectedId` | G01,G05,T04,T06,T07 | -| T03 | 单实例页 | T01 | `genealogyId,personId` | — | T01,T07,R02 | -| T04 | 流程页 | T01 | `genealogyId` | `personId,mode` | T01 | -| T05 | 流程页 | T03 | `genealogyId,personId` | — | T03 | -| T06 | 流程页 | T01 | `genealogyId,personId` | — | T01 | -| T07 | 普通页 | T01 | `genealogyId` | — | T01 | -| T08 | 普通页 | T03 | `genealogyId,personId` | — | T03 | -| F01 | 根页 | — | — | `genealogyId` | — | -| F02 | 流程页 | F01 | — | — | F01 | -| F03 | 普通页 | F01 | `feedId` | — | F01 | -| F04 | 普通页 | F01 | — | — | F01 | -| F05 | 普通页 | F04 | `articleId` | — | F04 | -| F06 | 流程页 | F04 | — | `articleId,mode` | F04,F05 | -| F07 | 普通页 | F01 | — | — | F01 | -| F08 | 普通页 | F07 | `albumId` | — | F07,F09 | -| F09 | 流程页 | F08 | `albumId` | — | F08 | -| F10 | 普通页 | F01 | — | — | F01 | -| R01 | 普通页 | F01 | — | — | F01 | -| R02 | 流程页 | R01 | — | `personId,mode` | R01 | -| R03 | 普通页 | F01 | — | — | F01 | -| R04 | 流程页 | R03 | — | `giftId,mode` | R03 | -| R05 | 普通页 | F01 | — | — | F01 | -| R06 | 普通页 | R05 | `ritualId` | — | R05 | -| R07 | 流程页 | R05 | — | `ritualId,mode` | R05,R06 | -| R08 | 普通页 | R02 | `personId` | — | R02,T03 | -| R09 | 普通页 | R02 | `personId` | — | R02,T03 | -| R10 | 普通页 | F01 | — | — | F01 | -| R11 | 普通页 | F01 | — | — | F01 | -| N01 | 普通页 | G01 | — | `genealogyId` | G01,M01 | -| N02 | 普通页 | N01 | `id` | — | N01 | -| M01 | 根页 | — | — | — | — | -| M02 | 流程页 | M01 | — | — | M01 | -| M03 | 普通页 | M01 | — | — | M01 | -| M04 | 流程页 | M03 | — | — | M03 | -| M05 | 流程页 | M03 | — | — | M03 | -| M06 | 普通页 | M01 | — | — | M01 | -| M07 | 流程页 | M06 | — | — | M06 | -| M08 | 普通页 | M01 | — | — | M01 | -| M09 | 普通页 | M01 | — | — | M01 | -| M10 | 普通页 | M01 | — | — | M01 | +独立验收只保留不能由错误注册表自证的高风险边界: -直接进入后的规范父页参数默认复制父子路由同名的已声明参数;人物页回 T01 的 `personId → selectedId` 必须由 `parentParamMap` 显式声明,页面不得自行拼接回退参数。`returnTo/finishPage` 还必须显式接收目标参数,确保 G03→G05、F09→F08 等目标不在栈内时仍能构造合法 URL。 +- 根语义固定为 A01、G01、F01、M01;A01 是认证根,其余三个是业务根。 +- T03 是唯一单实例页面;人物页回 T01 的行为必须把子页 `personId` 映射为父页 `selectedId`。 +- 所有父链无环并落到四根之一;所有非根页面可从四根沿允许来源到达。 +- ID 和业务参数只接受非空字符串;`sourceKey` 是网关保留参数,旧 URL、快照和领域对象不得进入查询。 +- 目标结果操作只从注册表读取;未声明的操作由运行时立即拒绝。 -目标页允许消费的结果操作也只在注册表中定义:A01=`password-reset`;G01=`application-reviewed,generation-poems-updated`;G05=`genealogy-created,genealogy-settings-updated,generation-poems-updated,application-reviewed`;G09=`application-created`;T01=`relative-created,relationship-updated`;T03=`member-open-requested,member-updated`;F01=`feed-created`;F04=`article-created`;F05=`article-updated`;F08=`media-uploaded`;R01=`person-created,person-updated,person-deleted`;R03=`gift-created,gift-updated,gift-deleted`;R05=`ritual-created,ritual-updated,ritual-deleted`;N01/N02=`application-reviewed`;M01=`profile-updated`;M03=`password-changed,phone-changed`;M06=`feedback-submitted`。未列出的目标页 `resultOperations` 为空,任意其他字符串都必须在运行时拒绝。 - -- [ ] **步骤 1:先写失败合同** +- [x] **步骤 1:先写失败合同** `tests/navigation-routes-contract.ps1` 必须读取 `pages.json` 与 `utils/navigation-routes.js`,断言 52 个路径集合和顺序精确相等、四个根语义恰为 A01/G01/F01/M01、父页与来源均存在、参数名不重复、`parentParamMap` 两端字段都合法、`resultOperations` 在目标页内唯一且非空字符串,并禁止 `previous`、`returnUrl`、`fallbackUrl`、`targetUrl`、`state`、`count`、`saveResult`、`step`、`genealogyName` 进入业务参数。 -- [ ] **步骤 2:确认红灯原因正确** +- [x] **步骤 2:确认红灯原因正确** 运行: @@ -184,86 +135,19 @@ if ($failed.Count -gt 0) { throw "失败合同:$($failed -join ', ')" } powershell -ExecutionPolicy Bypass -File tests/navigation-routes-contract.ps1 ``` -预期:因 `utils/navigation-routes.js` 不存在而失败;不能因 PowerShell 语法或编码失败。 +首次运行记录:因 `utils/navigation-routes.js` 不存在而按预期失败,不是 PowerShell 语法或编码失败;当前 owner 已实现并转绿。 -- [ ] **步骤 3:实现最小注册表** +- [x] **步骤 3:实现最小注册表** -注册表导出结构固定如下,52 项内容严格来自上表: +实现只保留一个冻结注册表和两个严格字符串查询函数。四根安全 oracle 固定为: ```js -const defineRoute = (route) => Object.freeze({ - ...route, - parentParamMap: Object.freeze(route.parentParamMap || {}), - requiredParams: Object.freeze(route.requiredParams || []), - optionalParams: Object.freeze(route.optionalParams || []), - allowedSources: Object.freeze(route.allowedSources || []), - resultOperations: Object.freeze(route.resultOperations || []), -}); - -export const ROUTES = Object.freeze({ - A01: defineRoute({ path: "/pages/auth/a01-entry", kind: "auth-root", parent: null, resultOperations: ["password-reset"] }), - A04: defineRoute({ path: "/pages/auth/a04-register", kind: "page", parent: "A01", allowedSources: ["A01"] }), - A05: defineRoute({ path: "/pages/auth/a05-reset-password", kind: "page", parent: "A01", allowedSources: ["A01"] }), - G01: defineRoute({ path: "/pages/genealogy/g01-my-genealogies", kind: "root", parent: null, optionalParams: ["genealogyId"], resultOperations: ["application-reviewed", "generation-poems-updated"] }), - G03: defineRoute({ path: "/pages/genealogy/g03-create-genealogy", kind: "flow", parent: "G01", optionalParams: ["genealogyId"], allowedSources: ["G01"] }), - G05: defineRoute({ path: "/pages/genealogy/g05-genealogy-overview", kind: "page", parent: "G01", requiredParams: ["genealogyId"], allowedSources: ["G01", "G03", "G06", "G09"], resultOperations: ["genealogy-created", "genealogy-settings-updated", "generation-poems-updated", "application-reviewed"] }), - G06: defineRoute({ path: "/pages/genealogy/g06-search-genealogies", kind: "page", parent: "G01", optionalParams: ["mode"], allowedSources: ["G01", "G03", "G09"] }), - G08: defineRoute({ path: "/pages/genealogy/g08-join-application", kind: "flow", parent: "G06", requiredParams: ["genealogyId"], optionalParams: ["source"], allowedSources: ["G05", "G06", "G09"] }), - G09: defineRoute({ path: "/pages/genealogy/g09-my-applications", kind: "page", parent: "G01", optionalParams: ["status"], allowedSources: ["G01", "G06", "G08"], resultOperations: ["application-created"] }), - G10: defineRoute({ path: "/pages/genealogy/g10-application-review", kind: "page", parent: "G01", requiredParams: ["genealogyId"], allowedSources: ["G01", "G05", "N01", "N02"] }), - G11: defineRoute({ path: "/pages/genealogy/g11-genealogy-settings", kind: "flow", parent: "G05", requiredParams: ["genealogyId"], allowedSources: ["G05"] }), - G12: defineRoute({ path: "/pages/genealogy/g12-generation-poems", kind: "flow", parent: "G05", requiredParams: ["genealogyId"], optionalParams: ["startGeneration", "currentGeneration"], allowedSources: ["G01", "G05"] }), - T01: defineRoute({ path: "/pages/tree/t01-tree-overview", kind: "page", parent: "G05", requiredParams: ["genealogyId"], optionalParams: ["selectedId"], allowedSources: ["G01", "G05", "T04", "T06", "T07"], resultOperations: ["relative-created", "relationship-updated"] }), - T03: defineRoute({ path: "/pages/tree/t03-member-profile", kind: "single", parent: "T01", parentParamMap: { selectedId: "personId" }, requiredParams: ["genealogyId", "personId"], allowedSources: ["T01", "T07", "R02"], resultOperations: ["member-open-requested", "member-updated"] }), - T04: defineRoute({ path: "/pages/tree/t04-add-relative", kind: "flow", parent: "T01", parentParamMap: { selectedId: "personId" }, requiredParams: ["genealogyId"], optionalParams: ["personId", "mode"], allowedSources: ["T01"] }), - T05: defineRoute({ path: "/pages/tree/t05-edit-member", kind: "flow", parent: "T03", requiredParams: ["genealogyId", "personId"], allowedSources: ["T03"] }), - T06: defineRoute({ path: "/pages/tree/t06-edit-relationship", kind: "flow", parent: "T01", parentParamMap: { selectedId: "personId" }, requiredParams: ["genealogyId", "personId"], allowedSources: ["T01"] }), - T07: defineRoute({ path: "/pages/tree/t07-member-directory", kind: "page", parent: "T01", requiredParams: ["genealogyId"], allowedSources: ["T01"] }), - T08: defineRoute({ path: "/pages/tree/t08-member-states", kind: "page", parent: "T03", requiredParams: ["genealogyId", "personId"], allowedSources: ["T03"] }), - F01: defineRoute({ path: "/pages/family/f01-family-feed", kind: "root", parent: null, optionalParams: ["genealogyId"], resultOperations: ["feed-created"] }), - F02: defineRoute({ path: "/pages/family/f02-publish-feed", kind: "flow", parent: "F01", allowedSources: ["F01"] }), - F03: defineRoute({ path: "/pages/family/f03-feed-detail", kind: "page", parent: "F01", requiredParams: ["feedId"], allowedSources: ["F01"] }), - F04: defineRoute({ path: "/pages/family/f04-article-list", kind: "page", parent: "F01", allowedSources: ["F01"], resultOperations: ["article-created"] }), - F05: defineRoute({ path: "/pages/family/f05-article-detail", kind: "page", parent: "F04", requiredParams: ["articleId"], allowedSources: ["F04"], resultOperations: ["article-updated"] }), - F06: defineRoute({ path: "/pages/family/f06-article-editor", kind: "flow", parent: "F04", optionalParams: ["articleId", "mode"], allowedSources: ["F04", "F05"] }), - F07: defineRoute({ path: "/pages/family/f07-album-list", kind: "page", parent: "F01", allowedSources: ["F01"] }), - F08: defineRoute({ path: "/pages/family/f08-album-detail", kind: "page", parent: "F07", requiredParams: ["albumId"], allowedSources: ["F07", "F09"], resultOperations: ["media-uploaded"] }), - F09: defineRoute({ path: "/pages/family/f09-media-upload", kind: "flow", parent: "F08", requiredParams: ["albumId"], allowedSources: ["F08"] }), - F10: defineRoute({ path: "/pages/family/f10-video-list", kind: "page", parent: "F01", allowedSources: ["F01"] }), - R01: defineRoute({ path: "/pages/records/r01-people-list", kind: "page", parent: "F01", allowedSources: ["F01"], resultOperations: ["person-created", "person-updated", "person-deleted"] }), - R02: defineRoute({ path: "/pages/records/r02-person-detail", kind: "flow", parent: "R01", optionalParams: ["personId", "mode"], allowedSources: ["R01"] }), - R03: defineRoute({ path: "/pages/records/r03-gift-list", kind: "page", parent: "F01", allowedSources: ["F01"], resultOperations: ["gift-created", "gift-updated", "gift-deleted"] }), - R04: defineRoute({ path: "/pages/records/r04-gift-editor", kind: "flow", parent: "R03", optionalParams: ["giftId", "mode"], allowedSources: ["R03"] }), - R05: defineRoute({ path: "/pages/records/r05-ritual-list", kind: "page", parent: "F01", allowedSources: ["F01"], resultOperations: ["ritual-created", "ritual-updated", "ritual-deleted"] }), - R06: defineRoute({ path: "/pages/records/r06-ritual-detail", kind: "page", parent: "R05", requiredParams: ["ritualId"], allowedSources: ["R05"] }), - R07: defineRoute({ path: "/pages/records/r07-ritual-editor", kind: "flow", parent: "R05", optionalParams: ["ritualId", "mode"], allowedSources: ["R05", "R06"] }), - R08: defineRoute({ path: "/pages/records/r08-growth-journal", kind: "page", parent: "R02", requiredParams: ["personId"], allowedSources: ["R02", "T03"] }), - R09: defineRoute({ path: "/pages/records/r09-life-events", kind: "page", parent: "R02", requiredParams: ["personId"], allowedSources: ["R02", "T03"] }), - R10: defineRoute({ path: "/pages/records/r10-memo-list", kind: "page", parent: "F01", allowedSources: ["F01"] }), - R11: defineRoute({ path: "/pages/records/r11-merit-records", kind: "page", parent: "F01", allowedSources: ["F01"] }), - N01: defineRoute({ path: "/pages/notification/n01-message-center", kind: "page", parent: "G01", optionalParams: ["genealogyId"], allowedSources: ["G01", "M01"], resultOperations: ["application-reviewed"] }), - N02: defineRoute({ path: "/pages/notification/n02-message-detail", kind: "page", parent: "N01", requiredParams: ["id"], allowedSources: ["N01"], resultOperations: ["application-reviewed"] }), - M01: defineRoute({ path: "/pages/profile/m01-profile-home", kind: "root", parent: null, resultOperations: ["profile-updated"] }), - M02: defineRoute({ path: "/pages/profile/m02-edit-profile", kind: "flow", parent: "M01", allowedSources: ["M01"] }), - M03: defineRoute({ path: "/pages/profile/m03-security-settings", kind: "page", parent: "M01", allowedSources: ["M01"], resultOperations: ["password-changed", "phone-changed"] }), - M04: defineRoute({ path: "/pages/profile/m04-change-password", kind: "flow", parent: "M03", allowedSources: ["M03"] }), - M05: defineRoute({ path: "/pages/profile/m05-change-phone", kind: "flow", parent: "M03", allowedSources: ["M03"] }), - M06: defineRoute({ path: "/pages/profile/m06-help-center", kind: "page", parent: "M01", allowedSources: ["M01"], resultOperations: ["feedback-submitted"] }), - M07: defineRoute({ path: "/pages/profile/m07-feedback", kind: "flow", parent: "M06", allowedSources: ["M06"] }), - M08: defineRoute({ path: "/pages/profile/m08-promotion", kind: "page", parent: "M01", allowedSources: ["M01"] }), - M09: defineRoute({ path: "/pages/profile/m09-vip-orders", kind: "page", parent: "M01", allowedSources: ["M01"] }), - M10: defineRoute({ path: "/pages/profile/m10-about-settings", kind: "page", parent: "M01", allowedSources: ["M01"] }), -}); - export const ROOT_ROUTE_KEYS = Object.freeze(["A01", "G01", "F01", "M01"]); -export const getRoute = (routeKey) => ROUTES[routeKey] || null; -export const getRouteKeyByPath = (path) => - Object.keys(ROUTES).find((routeKey) => ROUTES[routeKey].path === `/${String(path).replace(/^\/+/, "")}`) || null; ``` -实现时把代码中的中文注释放在注册表形状和保留参数规则上,不为每一行重复相同注释。 +`defineRoute` 必须冻结路由项、四个数组和 `parentParamMap`;`getRoute` 只接受原始字符串和对象自身键,`getRouteKeyByPath` 只接受原始字符串路径。52 项具体内容以 `utils/navigation-routes.js` 为准,计划不再保存可漂移的代码快照。 -- [ ] **步骤 4:验证绿灯并检查旧合同迁移** +- [x] **步骤 4:验证绿灯并检查旧合同迁移** 运行路由合同、接口页面映射合同、全局响应式合同与编译审计。预期全部通过;`tests/interface-page-mapping-contract.ps1` 不再把旧代际父键布局或“阶段 1 尚未开始”作为当前真相。 @@ -276,14 +160,14 @@ export const getRouteKeyByPath = (path) => **接口:** -- 消费:任务 1 的 `ROUTES`、`ROOT_ROUTE_KEYS`、`getRoute()`、`getRouteKeyByPath()`。 -- 输出:`buildRouteUrl`、`openPage`、`replaceStep`、`goBack`、`returnTo`、`finishPage`、`goRoot`、`consumeNavigationResult`、`resolveBackAction`、`runBackGuard`。 +- 消费:任务 1 的 `ROOT_ROUTE_KEYS`、`getRoute()`、`getRouteKeyByPath()`;具体路由数据只经注册表查询函数读取。 +- 输出:`buildRouteUrl`、`openPage`、`goBack`、`returnTo`、`finishPage`、`goRoot`、`consumeNavigationResult`、`resolveBackAction`、`runBackGuard`、`handleBackPress`。 - 一次性结果唯一形状:`{ operation, entityId?, refresh }`;字段集合必须精确,`operation` 必须属于目标路由 `resultOperations`,多余字段立即抛错。 -- `buildRouteUrl` 与 `resolveBackAction` 是同步纯函数;其余导航函数返回 `Promise`,并通过同一个转场锁保证一次只调用一个 Uni 导航 API。 +- `buildRouteUrl`、`consumeNavigationResult`、`resolveBackAction` 与 `handleBackPress` 同步返回;`openPage`、`goBack`、`returnTo`、`finishPage`、`goRoot` 与 `runBackGuard` 始终返回 `Promise`,并通过同一个转场锁保证一次只调用一个 Uni 导航 API,校验失败也只使用 Promise 拒绝这一种错误通道。 -- [ ] **步骤 1:先写伪栈运行时测试** +- [x] **步骤 1:先写伪栈运行时测试** -`tests/navigation-gateway-runtime-smoke.js` 使用 `vm` 注入伪 `uni` 与 `getCurrentPages`,至少验证: +`tests/navigation-gateway-runtime-smoke.js` 以独立 ESM 实例注入伪 `uni` 与 `getCurrentPages`,至少验证: ```js assert.equal(buildRouteUrl("T03", { genealogyId: "9007199254740993", personId: "p/1" }, "T01"), @@ -293,164 +177,93 @@ assert.throws(() => buildRouteUrl("T03", { genealogyId: "1", personId: "2", targ const firstOpen = openPage("T03", { genealogyId: "1", personId: "2" }, "T01"); const repeatedOpen = openPage("T03", { genealogyId: "1", personId: "2" }, "T01"); assert.equal(firstOpen, repeatedOpen); // 同一 tick 复用在途 Promise,只发生一次 navigateTo -assert.deepEqual(consumeNavigationResult("F04"), { operation: "article-created", entityId: "a9", refresh: true }); -assert.equal(consumeNavigationResult("F04"), null); // 只消费一次 +assert.deepEqual(consumeNavigationResult("TEST_RESULT_LIST"), { operation: "fixture-created", entityId: "a9", refresh: true }); +assert.equal(consumeNavigationResult("TEST_RESULT_LIST"), null); // 只消费一次;该中性路由只存在于测试内存源码 ``` 同时构造目标在栈内、目标不在栈内、栈深 1、根页切换、非法来源、提交中和未保存确认六组场景,并补齐以下反例:当前真实页 G01 伪报 `sourceKey=T07` 必须失败;外部直接链接即使带合法 `sourceKey` 也只能按规范父页回退;同一 tick 两次打开同一目标只调用一次 `navigateTo`;`operation="anything"`、空 `entityId`、非布尔 `refresh` 和额外字段都失败。 -- [ ] **步骤 2:运行并确认红灯** +- [x] **步骤 2:运行并确认红灯** -运行 `node tests/navigation-gateway-runtime-smoke.js`。预期因 `utils/navigation.js` 不存在而失败。 +首次运行记录:`node tests/navigation-gateway-runtime-smoke.js` 因 `utils/navigation.js` 不存在而按预期失败;当前 owner 已实现,严格运行时反例已转绿。 -- [ ] **步骤 3:实现 URL 和参数校验** +- [x] **步骤 3:实现 URL 和参数校验** -实现必须遵守以下实际逻辑: +参数与结果校验只由 `utils/navigation.js` 的同一数据记录快照器拥有,计划不复制可漂移实现。运行时合同固定为: -```js -const navigationResults = new Map(); -let navigationInFlight = null; +- 只接受原型为 `Object.prototype/null` 的普通对象;只读取 own enumerable data properties,拒绝 Symbol、访问器、不可枚举字段和继承字段;校验、编码、比较与结果写入始终消费同一冻结快照。 +- 注册表声明的必填与可选参数只要实际提供,就必须是非空字符串;额外字段立即失败。栈内目标实例也只投影注册表字段后按完整必填合同校验,不能信任畸形历史页面。 +- `sourceKey=""` 是构造纯目标 URL 时唯一的省略形态;`openPage` 必须提供非空字符串来源。`null/false/0/NaN` 不能借 truthy 判断绕过验证。 +- 查询串只按注册表 `requiredParams` 后 `optionalParams` 的声明顺序逐项 `encodeURIComponent`;调用方插入顺序、字母序和运行环境是否提供 `URLSearchParams` 都不得改变结果。 +- `navigationResults` 和唯一 `navigationInFlight` 锁只存在于网关模块;所有校验错误必须在调用 Uni API 前发生。 -const assertScalarString = (name, value) => { - if (typeof value !== "string" || value.length === 0) { - throw new TypeError(`导航参数 ${name} 必须是非空字符串`); - } -}; +`buildRouteUrl` 只负责纯字符串构造;真正执行压栈导航前,`openPage` 必须读取当前栈顶路径,并断言 `sourceKey === getRouteKeyByPath(当前真实页面)`。页面不得通过省略来源或传入另一个合法来源绕过注册表。栈深为 1 时一律忽略当前 URL 查询中的 `sourceKey`,只使用注册表 `parent/parentParamMap`;`sourceKey` 不得成为外部深链的可信合同。 -export const buildRouteUrl = (routeKey, params = {}, sourceKey = "") => { - const route = getRoute(routeKey); - if (!route) throw new Error(`未知路由键:${routeKey}`); - const allowed = new Set([...route.requiredParams, ...route.optionalParams]); - Object.keys(params).forEach((name) => { - if (!allowed.has(name)) throw new Error(`${routeKey} 不接受导航参数 ${name}`); - assertScalarString(name, params[name]); - }); - route.requiredParams.forEach((name) => { - if (!Object.prototype.hasOwnProperty.call(params, name)) throw new Error(`${routeKey} 缺少导航参数 ${name}`); - }); - if (sourceKey) { - if (!route.allowedSources.includes(sourceKey)) throw new Error(`${sourceKey} 不能进入 ${routeKey}`); - } - const query = new URLSearchParams({ ...params, ...(sourceKey ? { sourceKey } : {}) }).toString(); - return query ? `${route.path}?${query}` : route.path; -}; -``` +查询串固定按注册表字段顺序逐键 `encodeURIComponent`,不依赖 Uni 运行环境可能缺失的 `URLSearchParams`,不引入依赖、不复制到页面。等价参数对象即使插入顺序不同,也必须生成同一 URL 和同一转场锁键。 -`buildRouteUrl` 只负责纯字符串构造;真正执行导航前,`openPage/replaceStep` 必须读取当前栈顶路径,并断言 `sourceKey === getRouteKeyByPath(当前真实页面)`。页面不得通过传入另一个合法来源绕过注册表。栈深为 1 时一律忽略当前 URL 查询中的 `sourceKey`,只使用注册表 `parent/parentParamMap`;`sourceKey` 不得成为外部深链的可信合同。 +转场锁固定为“同一语义键复用同一个 Promise、不同语义在转场结束前以 Promise 返回 `false`”,不能用页面各自的布尔锁。取得新锁、写入结果、调用 Uni、成功目标实例校验、同步异常回滚和异步失败回滚必须属于同一个原子协议;`success/fail` 在 Promise settle 前按独立 flight 身份释放自己的锁,迟到的 `complete` 不得清掉新 flight。成功回调发现实际栈顶不是预定实例/参数时也必须拒绝并先回滚,不能只处理 Uni 的 `fail`。 -若 Uni 运行环境没有 `URLSearchParams`,只在本文件实现一个逐键 `encodeURIComponent` 的 8—12 行小函数,不引入依赖、不复制到页面。 +- [x] **步骤 4:实现五种语义导航** -转场锁固定实现为“同一操作复用 Promise、不同操作在转场结束前返回 `false`”,不能用页面各自的布尔锁: - -```js -const runUniNavigation = (key, invoke) => { - if (navigationInFlight?.key === key) return navigationInFlight.promise; - if (navigationInFlight) return Promise.resolve(false); - let resolvePromise; - let rejectPromise; - const promise = new Promise((resolve, reject) => { - resolvePromise = resolve; - rejectPromise = reject; - }); - navigationInFlight = { key, promise }; - try { - invoke({ - success: () => resolvePromise(true), - fail: (error) => rejectPromise(new Error(error?.errMsg || "页面跳转失败")), - complete: () => { navigationInFlight = null; }, - }); - } catch (error) { - navigationInFlight = null; - rejectPromise(error); - } - return promise; -}; -``` - -- [ ] **步骤 4:实现六种语义导航** - -实现顺序固定为:先验证目标,再读取当前栈,再决定唯一 Uni 调用。核心伪代码必须逐行落实: - -```js -export const openPage = (routeKey, params = {}, sourceKey = "") => { - assertActualSource(routeKey, sourceKey); - const url = buildRouteUrl(routeKey, params, sourceKey); - if (isCurrentTarget(url)) return Promise.resolve(false); - if (getRoute(routeKey).kind === "single") return activateExistingSinglePage(routeKey, params, url); - return runUniNavigation(`push:${url}`, (callbacks) => uni.navigateTo({ url, ...callbacks })); -}; - -export const replaceStep = (routeKey, params = {}, sourceKey = "") => { - assertActualSource(routeKey, sourceKey); - const url = buildRouteUrl(routeKey, params, sourceKey); - return runUniNavigation(`replace:${url}`, (callbacks) => uni.redirectTo({ url, ...callbacks })); -}; - -export const goRoot = (routeKey, params = {}) => { - if (!ROOT_ROUTE_KEYS.includes(routeKey)) throw new Error(`${routeKey} 不是根语义`); - const url = buildRouteUrl(routeKey, params); - if (isCurrentTarget(url)) return Promise.resolve(false); - return runUniNavigation(`root:${url}`, (callbacks) => uni.reLaunch({ url, ...callbacks })); -}; -``` +实现顺序固定为:先把调用方目标参数规范化为快照并完成来源校验,再读取当前真实栈,最后决定唯一 Uni 调用。`openPage` 对普通页只允许压栈,对 single 页先检查完整既有实例;`goRoot` 只接受四根并在路由与规范参数均相同时返回 Promise 化的 `false`。具体实现只由 `utils/navigation.js` 拥有,计划不复制第二份函数源码。 `goBack()` 在栈深大于 1 时只执行受转场锁保护的 `navigateBack({ delta: 1 })`;栈深为 1 且当前是普通页时忽略 URL 中的 `sourceKey`,只按规范父页、同名参数和 `parentParamMap` 构造父页参数。某级父页缺必填参数时继续沿注册表父链向上,直到第一个可合法构造的目标;例如缺 `genealogyId` 的异常 T03 不能伪造 T01/G05,最终回 G01。当前已是 A01/G01/F01/M01 根语义时返回 `false`,由 Android 系统处理退出,不伪造跨 Tab 历史。 -`returnTo(routeKey, targetParams = {}, result = null)` 从当前页下方按路由键反向寻找最近实例;找到则精确计算 `delta`,`targetParams` 不参与匹配,只校验调用方实际提供的字段。只有目标不在栈内时才要求这些参数包含构造目标所需的全部必填项,并据此调用 `goRoot` 或内部 replace。`finishPage(routeKey, targetParams, result)` 先验证结果字段和目标页 `resultOperations`,再沿用同一规则处理目标参数。结果只在目标确定后写入;Uni 调用失败、被转场锁拒绝或目标页消费后都立即删除,不能泄漏给下一次流程。 +当前路由合同没有任何“同流程临时步骤”替换边,原 14 处 `redirectTo` 也都归入根切换、规范返回或父链回退,因此网关不公开无消费者的 `replaceStep`。`redirectTo` 只作为 `goBack/returnTo/finishPage` 的内部实现细节;未来若出现真实替换流程,必须先在唯一注册表增加专用替换边和失败合同,再同轮新增公开语义。 -`kind="single"` 的 T03 已在同一 `genealogyId` 栈下方时,`openPage` 不再压栈:网关写入 T03 允许的 `{ operation: "member-open-requested", entityId: personId, refresh: false }`,精确返回已有 T03,由其读取成功后推进页内轨迹;导航失败立即删除结果。测试必须构造 `[T01,T03,其他页]` 后再次打开 T03,断言原生栈中始终只有一个 T03。若既有 T03 的 `genealogyId` 不同,返回明确 `T03_CONTEXT_CONFLICT` 并拒绝压栈,调用方必须先用根语义切换家谱上下文。 +`returnTo(routeKey, targetParams = {})` 从当前页下方按路由键反向寻找最近实例;找到后不再按参数搜索另一个实例,但调用方显式提供的每个字段必须与这个最近实例一致,冲突立即拒绝,空对象仍允许普通返回。只有目标不在栈内时才要求参数包含构造目标所需的全部必填项,并据此调用 `goRoot` 或内部 replace;普通返回不允许携带流程结果。`finishPage(routeKey, targetParams, result)` 是完成并回传结果的唯一公开入口,调用方即使返回栈内目标也必须显式提供目标的全部必填参数;先验证结果字段和目标页 `resultOperations`,再与最近实例核对。若当前来源也是注册路由,来源与目标共同声明且实际存在的上下文字段(如 `genealogyId/albumId/personId`)必须相等,不能由调用方伪造目标参数跨上下文写结果;外部未知来源不冒充业务上下文。 -- [ ] **步骤 5:实现统一返回优先级** +结果写入与转场锁的 started/reused/busy 判定必须原子完成:只有真正取得锁的首个动作写入,目标原生 `onShow` 时已经可见;复用和忙碌调用不得覆盖。页面实例身份只由模块内 `WeakMap` 生成原始值令牌,结果 envelope 不得强引用页面/Vue 实例。栈内结果绑定被反向搜索选中的最近页面令牌和完整规范参数,缺栈结果绑定发起页令牌与待构造目标参数;只有当前真实栈顶命中该目标实例/新建目标且参数一致时才可消费,错页、同路由更远实例和其他业务上下文返回 `null` 且不删除。新完成流程取得锁后淘汰已经错过生命周期的旧结果;Uni 调用失败或同步抛错只删除当前结果,不得复活陈旧结果。 + +`kind="single"` 的 T03 已在同一 `genealogyId` 栈下方时,`openPage` 不再压栈:网关写入 T03 允许的 `{ operation: "member-open-requested", entityId: personId, refresh: false }`,精确返回已有 T03,由其读取成功后推进页内轨迹;导航失败立即删除结果。测试必须构造 `[T01,T03,其他页]` 后再次打开 T03,断言原生栈中始终只有一个 T03。若既有 T03 的 `genealogyId` 不同,返回明确 `T03_CONTEXT_CONFLICT` 并拒绝压栈;若迁移前遗留栈已包含多个 T03,则返回 `T03_STACK_CONFLICT`,不得选择其中一个后伪称栈已唯一。调用方必须先返回或用根语义清理冲突上下文。 + +- [x] **步骤 5:实现统一返回优先级** + +返回优先级固定为“关闭瞬态层 → 回退页内轨迹 → 阻止提交中离开 → 确认放弃未保存内容 → 普通返回”。该顺序只由 `resolveBackAction` 的运行时实现拥有,计划不复制第二份函数源码;同步公开入口默认接受空对象,并与异步 `runBackGuard` 共用严格普通对象快照和布尔字段校验。 + +`runBackGuard(context)` 的四个状态字段只要提供就必须是布尔值;只调用上下文中与返回动作同名的一个回调。关闭瞬态层、回退内部轨迹和阻止提交中返回完成后稳定返回 `true`;放弃确认返回 `false` 时必须留在当前页,返回 `true` 才调用 `goBack()`。页面不得另写不同优先级。 + +需要弹层、内部轨迹或未保存守卫的页面只保留一个 `requestBack` 函数。页头 `@back` 调用它,Android 返回固定委托给同步适配器;网关自己的 `navigateBack` 再次触发钩子时返回 `false` 放行,不能递归消费: ```js -export const resolveBackAction = ({ transientOpen, internalTrail, dirty, submitting }) => { - if (transientOpen) return "close-transient"; - if (internalTrail) return "pop-internal-trail"; - if (submitting) return "block-submitting"; - if (dirty) return "confirm-discard"; - return "go-back"; -}; -``` - -`runBackGuard(context)` 只调用上下文中与返回动作同名的一个回调;放弃确认返回 `false` 时必须留在当前页,返回 `true` 才调用 `goBack()`。页面不得另写不同优先级。 - -需要弹层、内部轨迹或未保存守卫的页面只保留一个 `requestBack` 函数。页头 `@back` 调用它,Android 返回固定写法如下;回调先同步返回 `true` 消费系统返回,再异步执行同一个守卫: - -```js -onBackPress(() => { - void requestBack(); - return true; -}); +onBackPress((event) => handleBackPress(event, requestBack)); ``` 根页没有浮层时不注册拦截;有浮层的根页只在浮层可见时返回 `true` 并关闭最上层。 -- [ ] **步骤 6:验证网关** +- [x] **步骤 6:验证网关** 运行 Node 冒烟、导航路由合同、全局响应式合同和编译审计。预期全部通过,且网关以外暂时仍有直接调用;任务 3 才启用零调用扫描。 -### 任务 3:迁移共享页头、底栏和模块页 +### 任务 3:迁移共享页头、底栏并退役通用母版 **文件:** - 修改:`components/AppTabbar.vue` - 修改:`components/PageHeader.vue` -- 修改:`components/ModulePage.vue` +- 修改:`pages/genealogy/g01-my-genealogies.vue` +- 修改:`pages/family/f01-family-feed.vue` +- 修改:`pages/profile/m01-profile-home.vue` - 修改:`tests/shared-interaction-accessibility-contract.ps1` - 修改:`tests/shared-component-document-flow-contract.ps1` +- 修改:`tests/root-pages-visual-contract.ps1` +- 修改:`tests/runtime-assets-contract.ps1` - 新建:`tests/navigation-source-scan-contract.ps1` +- 新建:`tests/retired-module-page-contract.ps1` +- 删除:退役通用页面组件、临时页面目录及其两个正向死合同 +- 删除:仅由退役通用页面虚构消费的两张 notification frame 资产及其清单项 **接口:** - `AppTabbar`:保留现有展示键 `genealogy/family/profile`,另给三项加入 `routeKey: G01/F01/M01`;只调用 `goRoot(item.routeKey)`,活动项无操作,删除每项的 `path`。 - `PageHeader`:普通返回调用 `goBack()`;`customBack` 仍只发出 `back` 事件给需要守卫的页面;删除 `fallbackUrl`。 -- `ModulePage`:返回调用 `goBack()`,不自行判断路由。 +- 52 个活动页面均已拥有自己的业务结构;退役通用页面与临时目录必须删除,不保留第二套状态、返回或视觉合同。 -- [ ] **步骤 1:先改失败断言** +- [x] **步骤 1:先改失败断言** -把旧合同中对 `uni.navigateBack()`、`uni.reLaunch()` 和 `fallbackUrl` 的正向断言改成反向断言,并要求三个组件导入导航网关。此时测试必须因组件尚未迁移而失败。 +把旧合同中对 `uni.navigateBack()`、`uni.reLaunch()` 和 `fallbackUrl` 的正向断言改成反向断言,要求页头与底栏导入导航网关,并先用失败合同证明无消费者的通用页面旧入口必须整体退役。 -- [ ] **步骤 2:迁移三个组件** +- [x] **步骤 2:迁移共享组件并收紧根页消费者** 共享组件只允许出现: @@ -466,19 +279,21 @@ const handleBack = () => { emit("back"); return; } - goBack(); + return goBack(); }; ``` 删除所有 `fallbackUrl` 属性、默认值、模板传递和相关测试;不保留兼容属性。 -- [ ] **步骤 3:建立最终零调用扫描,但暂不要求通过** +审查同时证明原通用页面和临时页面目录没有活动消费者;本轮已删除旧入口、专属 mixin、正向死合同与两张孤立通知资产。`ModulePageBackground` 仍由活动页面消费,family/profile/records 的同名 frame 仍由专项 mixin 消费,均不得误删。 -`tests/navigation-source-scan-contract.ps1` 扫描 `components`、`pages`、`utils` 下全部 `.vue/.js`,五种 Uni 导航调用和 `getCurrentPages` 只允许出现在 `utils/navigation.js`,而 `uni.switchTab` 因项目没有原生 tabBar 必须全项目为零。扫描同时禁止页面和组件出现业务 `/pages/...` 路径字符串,资产路径 `/static/...` 不受影响;测试与文档目录明确排除,不能用过宽白名单。当前预期列出尚未迁移的页面,不能误报测试和文档中的示例。 +- [x] **步骤 3:建立最终零调用扫描与精确迁移债务账本** + +`tests/navigation-source-scan-contract.ps1` 覆盖根文件与全部生产 `.vue/.js`,按 SFC 的 template/script/style 分段剥离 HTML、JavaScript 与 SCSS 注释,保留字符串以捕获方括号和 Reflect 取值。五种 Uni 导航方法逐项计数,`getCurrentPages` 同步计数;声明式 `navigator`、动态 Uni 属性、Uni 对象逃逸和 `switchTab` 全局禁止。页面和组件的业务 `/pages/...` 路径字符串也进入账本,资产 `/static/...` 不受影响。任务 3 完成时精确剩余 39 个文件,任务 4—9 每迁移一页必须同轮收紧,任务 10 清空账本。 - [ ] **步骤 4:验证共享批次并在 MuMu 复核** -运行三个共享聚焦合同、全局响应式合同和编译审计;在 MuMu 验证 G01/F01/M01 三个自定义 Tab、普通页头返回、带 `customBack` 的弹层页返回。不得调整模拟器。 +共享聚焦合同、退役入口合同、运行时资产合同、全局响应式合同和编译审计均已通过;MuMu 复核待执行。受“不启动、关闭或调整 MuMu”约束,本轮不以静态绿灯冒充原生视觉通过,后续只在用户现有 MuMu 环境可用时验证 G01/F01/M01 三个自定义 Tab、普通页头返回和带 `customBack` 的弹层返回。 ### 任务 4:迁移认证导航 @@ -491,8 +306,9 @@ const handleBack = () => { - 修改:`tests/a04-registration-contract.ps1` - 修改:`tests/a05-reset-password-contract.ps1` - 修改:`tests/a06-auth-status-contract.ps1` -- 修改:`tests/active-page-business-ownership-contract.ps1` -- 修改:`tests/navigation-flow-contract.ps1` +- 修改:`tests/a01-a02-ui-contract.ps1` +- 新增并作为后续导航批次唯一流程合同:`tests/navigation-flow-contract.ps1` +- 修改:`tests/navigation-source-scan-contract.ps1` **流程合同:** @@ -500,25 +316,31 @@ const handleBack = () => { | --- | --- | | A01 打开注册 | `openPage("A04", {}, "A01")` | | A01 打开重设密码 | `openPage("A05", {}, "A01")` | -| A04 页头返回或“已有账号” | `returnTo("A01", {})` | -| A05 取消或本地视觉成功态返回 | `returnTo("A01", {})`,不写结果 | +| A01 Android 返回 | 行为验证浮层打开时 `requestBack → runBackGuard` 只关闭浮层;无浮层时同步返回 `false`,交还 Android 退出根页 | +| A04 页头、Android 返回或“已有账号” | 同一 `requestBack → runBackGuard → goBack()`,正常栈弹回 A01,深链按规范父页建立 A01 | +| A05 未成功时的页头、Android 返回、“想起密码”或取消 | 同一 `requestBack → runBackGuard → goBack()`,不写结果 | +| A05 本地视觉成功终态的按钮或 Android 返回 | 共用唯一 `leaveResetSuccess → returnTo("A01", {})`,不关闭终态后重新暴露已提交表单,不写结果 | | A05 未来真实重设接口成功 | `finishPage("A01", {}, { operation: "password-reset", refresh: false })` | | A01 真实登录成功 | `goRoot("G01")` | -| A06 封存页进入登录 | `goRoot("A01")` | +| A06 封存页返回 | 恢复浮层打开时只关闭浮层,否则 `goRoot("A01")` | -当前 A01/A04 的行为验证仍是明确的接口占位,导航批次不得把“知道了”伪装成登录或注册成功。OpenAPI 已证明 `/genealogy/app/auth/register` 的 `200` 响应复用 `LoginResult`,并可返回 `LoginVo` 会话令牌;真实注册接入时必须在保存有效会话后调用 `goRoot("G01")`,不能先回 A01 再让用户重复登录。 +当前 A01/A04 的行为验证仍是明确的接口占位,导航批次不得把“知道了”伪装成登录或注册成功。2026-07-22 新线上 OpenAPI 已证明 `/genealogy/app/auth/register` 的 `200` 响应为 `RAppLoginVo → AppLoginVo.access_token`;真实注册接入时必须只读取当前字段、保存有效会话后调用 `goRoot("G01")`,不能先回 A01 再让用户重复登录,也不保留旧响应字段兼容分支。 -- [ ] **步骤 1:让认证导航合同先失败** +- [x] **步骤 1:让认证导航合同先失败** -把现有合同中的 `uni.navigateBack`、`uni.redirectTo` 正向断言改为上表语义调用,增加 A04/A05 连续进入不产生重复 A01 的静态断言。另断言 `?state=success`、本地计时器和视觉占位弹窗都不得生成 `password-reset` 导航结果。运行四个认证聚焦合同,预期因页面尚未导入导航网关而失败。 +已先建立 `tests/navigation-flow-contract.ps1` 并移除旧直接导航正向断言;第一次执行明确失败在 A01 的 `uni.navigateTo`,证明红灯来自旧实现而非测试语法。合同锁定四页无原生导航/路径旁路、Android 同步适配、完整返回 context、A04/A05 单例延迟确认和 A05 本地成功终态不得生成 `password-reset` 结果。 -- [ ] **步骤 2:只迁移现有真实导航动作** +- [x] **步骤 2:只迁移现有真实导航动作** -页面统一导入所需语义函数;A01 的安全验证占位、A04 的接口占位和 A05 的本地成功弹窗保持当前事实。A05 本地成功弹窗的“返回登录”只普通 `returnTo`,只有后续真实重设接口成功才允许生成并消费一次性结果;不新增假接口、不写未调用完成函数。A01 的行为验证/协议弹层和 A04/A05 的表单共同使用 `requestBack`;提交中先阻止离开,未保存时再确认放弃,页头与 `onBackPress` 构造同一 context 并执行同一 `runBackGuard`。 +页面已统一导入所需语义函数;A01 的安全验证占位、A04 的接口占位和 A05 的本地成功弹窗保持当前事实。A05 本地成功终态的按钮与 Android 返回共用唯一无结果 `returnTo`,只有后续真实重设接口成功才允许生成并消费一次性结果;不新增假接口、不写未调用完成函数。A01 只在行为验证浮层打开时消费系统返回;A04/A05 的脏表单用项目 `AppDialog` 和单例延迟 Promise 确认,重复返回先关闭确认框,卸载必定释放等待者。当前没有真实异步提交,未虚构 `submitting` 状态。A06 只从 `onLoad(options)` 解析状态,已删除浏览器地址与 `getCurrentPages` 旁路。 -- [ ] **步骤 3:验证认证批次** +- [x] **步骤 3:完成认证批次静态验证** -运行认证聚焦合同、导航网关 Node 冒烟、响应式合同和编译审计。在 MuMu 验证 A01→A04→页头返回、A01→A04→已有账号、A01→A05→取消、A05 本地成功→返回登录,各流程连续执行 3 次;返回后只能有一个 A01,表单取消不得显示成功。 +认证导航流程、四页聚焦合同、导航源扫描、响应式合同和编译审计均已通过;源扫描债务由 39 个文件收紧到 35 个文件。任务 4 完成当时的导航网关 Node 冒烟与 PowerShell `121/121` 是该批历史快照;项目当前全量证据以总览登记的任务 5 收口结果为准。 + +- [ ] **步骤 4:在 MuMu 复核认证流程** + +在 MuMu 验证 A01→A04→页头返回、A01→A04→已有账号、A01→A05→取消、A05 本地成功→返回登录,各流程连续执行 3 次;返回后只能有一个 A01,表单取消不得显示成功。受“不启动、关闭或调整 MuMu”约束,本轮保持待验,不能把静态绿灯写成原生交互或视觉已通过。 ### 任务 5:迁移 G 系列导航 @@ -533,41 +355,48 @@ const handleBack = () => { - 修改:`pages/genealogy/g10-application-review.vue` - 修改:`pages/genealogy/g11-genealogy-settings.vue` - 修改:`pages/genealogy/g12-generation-poems.vue` -- 修改:`tests/g01-visual-contract.ps1` -- 修改:`tests/g03-create-flow-contract.ps1` -- 修改:`tests/g08-g10-application-flow-contract.ps1` -- 修改:`tests/g11-g12-settings-poems-contract.ps1` +- 修改:`data/mock.js` +- 修改:`utils/api.js` +- 修改:`utils/genealogy-contracts.js` +- 新增:`utils/generation-poem.js` +- 修改:`utils/navigation-routes.js` +- 修改:G01、G03、G05、G06、G08—G12 的聚焦合同与运行时冒烟 - 修改:`tests/navigation-flow-contract.ps1` +- 修改:`tests/navigation-gateway-runtime-smoke.js` +- 修改:`tests/navigation-source-scan-contract.ps1` +- 新增:`tests/genealogy-fixture-runtime-smoke.js` +- 新增:`tests/generation-poem-runtime-smoke.js` +- 新增:`tests/g-series-openapi-contract.ps1` **流程合同:** - G01 卡片和快捷入口只使用 `openPage`;路径对象改为路由键对象,不能继续保存 `/pages/...` 字符串。 -- G03 内部从创建步骤切换到始祖步骤使用本页状态,不再接收 `step` 或 `redirectTo` 自己;待完善流程只传 `genealogyId`,由领域数据决定步骤。取消 `returnTo("G01", {})`;始祖完成 `finishPage("G05", { genealogyId: entityId }, { operation: "genealogy-created", entityId, refresh: true })`。 +- G03 内部从创建步骤切换到始祖步骤使用本页状态,不接收 `step` 或 `genealogyId`;当前只建立可撤销的本地预览,完成后用 `returnTo("G05", { genealogyId })` 进入明确预览态,不生成 `genealogy-created` 业务结果。真实接口形成创建与始祖闭环后,才能按服务端事实新增完成结果。 - G05 返回 `returnTo("G01", {})`,进入 F01 使用 `goRoot("F01", { genealogyId })`,其余入口使用 `openPage`。G05/G08 不再接收 `genealogyName`,页面按 `genealogyId` 从现有 fixture 或领域数据取名称。 - G06 已加入或我创建的结果 `goRoot("G01", { genealogyId })`;公开预览进入 G05;搜索申请进入 G08;审核中或被拒绝进入 G09。 -- G08 搜索申请成功 `finishPage("G09", {}, { operation: "application-created", entityId, refresh: true })`;邀请码直接加入成功 `goRoot("G01", { genealogyId })`。邀请码合同已定为“直接加入且不生成审核记录”,M08 同批删除“加入后需要管理员审核”旧文案,不写双分支兼容。 +- G08 当前搜索与邀请码都只完成本地表单校验:搜索来源无结果进入 G09 预览,邀请码来源无结果回 G01,均不得新增审核记录、选中家谱或声称加入成功。产品终点仍是“邀请码直接加入且不生成审核记录”,但必须等后端提供邀请码解析与直接加入合同后一次性替换本地预览;M08 的审核旧文案已在任务 9 删除,不写双分支兼容。 - G09 通过记录进入 G05,被拒绝记录进入 G08,搜索入口进入 G06;返回保留列表现场。 -- G10/G11/G12 保存或审核成功把一次性结果返回实际来源;取消只返回,不产生成功结果。 +- G10/G11/G12 只更新当前页面的本地审核、设置或字辈预览,不向实际来源回传成功结果;取消按统一返回守卫处理。只有真实接口成功且来源页确实需要失效刷新时,才允许在同一合同变更中注册并消费一次性结果。 -- [ ] **步骤 1:逐页写失败断言** +- [x] **步骤 1:逐页写失败断言** -`tests/navigation-flow-contract.ps1` 对上表每一类流程检查路由键、`sourceKey`、结果 `operation` 和终点;现有 G 系列聚焦合同移除直接 Uni API 断言。先运行并确认失败来自旧导航调用。 +`tests/navigation-flow-contract.ps1` 已对上表每类流程检查路由键、`sourceKey`、无伪成功结果和终点;G 系列聚焦合同已移除直接 Uni API 正向断言。失败基线明确来自旧导航调用、伪结果和页面私有合同,而不是测试语法。 -- [ ] **步骤 2:迁移 G01、G03、G05** +- [x] **步骤 2:迁移 G01、G03、G05** -先处理根页、创建流程和总览;每页完成后分别运行该页聚焦合同、响应式合同、编译审计,并在 MuMu 验证进入、返回、取消、完成和连续重复 3 次。G03 不得把同一页面的业务步骤塞回原生栈。 +根页、创建流程和总览已迁移。G03 在同一原生页面维护两步状态,并通过唯一临时预览 owner 生成词法字符串 ID;放弃删除本轮临时预览,完成只进入 G05 本地预览。G05 的名称、访问规则、来源、权限入口和公开字段由共享 fixture 与访问解析器投影,不信任路由伪造角色。 -- [ ] **步骤 3:迁移 G06、G08、G09** +- [x] **步骤 3:迁移 G06、G08、G09** -只传字符串 ID 与业务枚举;删除 `previous` 原始来源参数,改用受注册表约束的 `sourceKey`。失败和取消保留输入及当前列表,邀请码和搜索申请不得落到同一个完成终点。 +三页已只传字符串 ID 与业务枚举;`previous` 原始来源参数已经删除,来源只由真实栈与受注册表约束的 `sourceKey` 表达。共享可见性与申请资格失败关闭;邀请码与搜索申请保留不同本地终点,G09 撤回只使用 `LOCAL_WITHDRAWN`,不冒充服务端状态。 -- [ ] **步骤 4:迁移 G10、G11、G12** +- [x] **步骤 4:迁移 G10、G11、G12** -审核、设置和字辈保存结果使用注册表允许的一次性结果,并显式传入回退目标所需参数;现有 G10 拒绝原因聚焦与无障碍关系不得回归。G12 从 G01 或 G05 进入时,返回实际栈中来源;直接进入才使用规范父页 G05。G01 的切换/添加弹层,G09 的撤回确认,以及 G03、G08、G10、G11、G12 的表单都接入 `requestBack/runBackGuard`;页头、取消按钮和 `onBackPress` 使用同一 context,提交中优先于脏表单确认。 +三页已删除伪后端成功结果并明确标注本地预览;G10 保留拒绝原因聚焦与无障碍关系,G11 的访问预设由 `utils/genealogy-contracts.js` 唯一拥有且未知值失败关闭。G12 按 OpenAPI 离线导出的 26000 字符、单代 50 字符、最多 500 代、状态 `0/1` 与历史保留约束实现 Unicode 安全解析、完整 ACTIVE 序列检查和每批 50 行渲染;服务端未定义批次首代前不发送本地合并结果。G01 弹层、G09 撤回确认及 G03/G08/G10/G11/G12 表单均已接入同一返回守卫。 - [ ] **步骤 5:完成 G 系列 MuMu 矩阵** -至少验证 G01→G03→取消/完成、G01→G06→G08→G09、G05→G10/G11/G12→取消/完成、直接进入 G11/G12 后返回,以及每条流程重复进入。检查旧页面不残留、当前家谱 ID 不串、取消不刷新、完成只刷新一次。 +静态合同、纯 Node 冒烟、响应式合同和编译审计已经通过,源码导航债务收紧到 29 个文件。仍须在用户现有 MuMu 中验证 G01→G03→取消/完成、G01→G06→G08→G09、G05→G10/G11/G12→取消/本地完成、直接进入 G11/G12 后返回,以及每条流程重复进入;检查旧页面不残留、当前家谱 ID 不串、取消不刷新、本地预览不伪造服务端成功。受当前约束不得启动、关闭或调整模拟器,本步骤保持未勾选。 ### 任务 6:迁移 T 系列并实现 T03 单实例轨迹 @@ -580,22 +409,30 @@ const handleBack = () => { - 修改:`pages/tree/t06-edit-relationship.vue` - 修改:`pages/tree/t07-member-directory.vue` - 修改:`pages/tree/t08-member-states.vue` +- 修改:`data/mock.js` +- 修改:`utils/api.js` - 修改:`tests/t03-t08-member-flow-contract.ps1` - 修改:`tests/t03-t08-member-flow-runtime-smoke.js` +- 修改:`tests/t03-t08-business-specialization-contract.ps1` - 修改:`tests/t03-t08-business-specialization-runtime-smoke.js` +- 修改:`tests/t07-module-baseline-contract.ps1` +- 修改:`tests/t07-module-baseline-runtime-smoke.js` - 修改:`tests/navigation-flow-contract.ps1` +- 新建:`tests/tree-member-fixture-runtime-smoke.js` **接口:** - T03 新增页内状态:`memberTrail: string[]`、`trailIndex: number`、`loadMember(personId): Promise`、`initializeMemberTrail(initialPersonId)`、`openRelative(personId)`、`popMemberTrail()`。 - 原生页面仍只有一个 T03;亲属点击只在 `loadMember` 成功后追加字符串 `personId`,失败不改轨迹、不清空当前成员。 -- T04/T06 成功回 T01 时结果 `entityId` 是需要聚焦的成员 ID;T01 在 `onShow` 消费一次并定位。 +- T03 初始 URL 的 `personId` 是不可变宿主页路由身份,页内活动成员随轨迹变化但不改 URL;当前 T03 只消费“打开成员”请求,不消费任何写成功结果。 +- T04/T05/T06 当前都只生成明确写着“尚未提交服务器”的本地预览,提交动作留在本页且不产生导航结果;真实写接口、版本冲突和完成回流统一留到任务 18 原子启用。 +- `data/mock.js::treeMembers` 是当前唯一可变成员夹具 owner,`utils/api.js` 是唯一写入口;页面只使用 `listTreeMemberFixtures(genealogyId)`、`findTreeMemberFixture(genealogyId, personId)` 的深拷贝快照。错误家谱、未知成员与缺失必填路由身份全部失败关闭,只有 T04 明确首位成员模式允许没有 `personId`。 -- [ ] **步骤 1:先锁定重复 T03 缺陷** +- [x] **步骤 1:先锁定重复 T03 缺陷** -运行时测试先断言初始读取失败时轨迹仍为空、初始成功后轨迹恰为 `[A]`,再构造 T01→T03(A)→亲属 B→亲属 C→返回→返回→来源,断言路由变化始终停留在同一个 T03 地址、当前成员依次 A/B/C/B/A、最终才离开 T03;历史 B 失效时删除 B 后继续回 A。另构造伪栈 `[T01,T03,其他页]` 再打开同一家谱 T03,断言网关回到既有实例且原生栈始终只有一个 T03。旧实现因未初始化 A 且 `navigateTo` 同一路由而失败。 +合同先断言初始读取失败时轨迹仍为空、初始成功后轨迹恰为 `[A]`,再构造 T01→T03(A)→亲属 B→亲属 C→返回→返回→来源,锁定 URL 始终为宿主 A、页内活动成员依次 A/B/C/B/A、最终才离开 T03;历史 B 失效时删除 B 后继续回 A。导航网关 Node 冒烟另构造伪栈 `[T01,T03,其他页]` 再打开同一家谱 T03,已证明回到既有实例且原生栈始终只有一个 T03;T03 浏览器运行时脚本已同步这些断言并通过语法检查,但因没有 `127.0.0.1:9222` 调试页而未执行。 -- [ ] **步骤 2:实现 T03 页内轨迹** +- [x] **步骤 2:实现 T03 页内轨迹** 初始成员成功读取后必须先执行: @@ -619,18 +456,21 @@ const openRelative = async (nextPersonId) => { `popMemberTrail()` 先递减索引再加载历史成员;历史成员失效时移除该项并继续向前,不能生成新 T03 页面。页头与 `onBackPress` 必须构造同一 context,通过 `runBackGuard` 先消费内部轨迹;T04/T05/T06 等表单也以提交中→脏表单的统一优先级接线。 -- [ ] **步骤 3:迁移 T01、T04—T08 的终点** +- [x] **步骤 3:迁移 T01、T04—T08 的终点** -T01 打开 T03/T04/T06/T07 使用路由键;T04 保存 `finishPage("T01", { genealogyId }, { operation: "relative-created", entityId, refresh: true })`;T05 保存 `finishPage("T03", { genealogyId, personId }, { operation: "member-updated", entityId: personId, refresh: true })`;T06 保存 `finishPage("T01", { genealogyId }, { operation: "relationship-updated", entityId: personId, refresh: true })`;T07 可返回 T01 定位或打开当前单实例 T03;T08 家谱失效使用 `goRoot("G01")`,普通返回回 T03。 +T01 打开 T03/T04/T06/T07 已全部使用路由键;T07 以路由键打开当前单实例 T03;T08 家谱失效使用根语义回 G01,普通状态使用统一返回。六位共享成员 101—106 已能从 T01/T07 一致进入 T03,104 能进入 T05/T08,106 进入 T06 时仍保持 106,不再回退首位成员。T04/T06 的本地预览由用户确认放弃后无结果回 T01;T05 的活动成员可能不同于宿主页路由身份,所以确认放弃预览后必须使用 `goBack()`:有历史时精确退回原 T03(A) 并保留活动成员 B 与轨迹,深链单页时才按当前参数构造规范父页。不得以活动成员 B 强行返回 T03(B),也不得把本地预览伪造成服务端完成。 -- [ ] **步骤 4:验证 T 系列** +- [ ] **步骤 4:完成 T 系列 MuMu 矩阵** -运行 T 聚焦合同、两个运行时冒烟、响应式合同和编译审计。在 MuMu 验证 T03 A→B→C→B→A→来源、T04/T05/T06 的取消与完成、T07 选择、T08 返回和同一流程 20 次重复进入;原生栈不得随亲属浏览增长。 +T 聚焦合同、导航网关与成员夹具纯 Node 冒烟、全部 Node 语法检查、响应式合同和编译审计已经通过;最新全量结果为 PowerShell `122/122`、Node 语法 `35/35`、纯 Node `7/7`,源码导航债务收紧到 22 个页面/表单组件文件。成员夹具运行时已验证复合身份、跨谱隔离、父链与状态枚举、列表/单项深拷贝,以及 `createPerson` 写入完整 shape 后的同谱可见与跨谱不可见。依赖 `127.0.0.1:9222` 的浏览器脚本本轮只完成语法检查;仍须在用户现有 MuMu 中验证 T03 A→B→C→B→A→来源、T04/T05/T06 的取消与本地预览放弃、T07 选择、T08 返回和同一流程 20 次重复进入;原生栈不得随亲属浏览增长,三个预览均不得表现为服务端已保存。受当前约束不得启动、关闭或调整模拟器,本步骤保持未勾选。 ### 任务 7:迁移 F 系列导航 **文件:** +- 修改:`utils/navigation-routes.js` +- 修改:`data/mock.js` +- 修改:`utils/api.js` - 修改:`pages/family/f01-family-feed.vue` - 修改:`pages/family/f02-publish-feed.vue` - 修改:`pages/family/f03-feed-detail.vue` @@ -647,39 +487,58 @@ T01 打开 T03/T04/T06/T07 使用路由键;T04 保存 `finishPage("T01", { gen - 修改:`tests/f08-album-detail-contract.ps1` - 修改:`tests/f09-media-upload-contract.ps1` - 修改:`tests/f10-video-status-contract.ps1` +- 新增:`tests/family-fixture-runtime-smoke.js` +- 修改:`tests/navigation-routes-contract.ps1` +- 修改:`tests/navigation-source-scan-contract.ps1` - 修改:`tests/navigation-flow-contract.ps1` **流程合同:** -| 流程 | 取消/返回 | 完成 | -| --- | --- | --- | -| F01→F02 | F01 | `finishPage("F01", {}, { operation: "feed-created", entityId, refresh: true })` | -| F01→F03 | F01 且恢复现场 | 评论成功留在 F03,本地插入一次 | -| F01→F04→F05 | F05 回 F04,F04 回 F01 | 收藏留在 F05 | -| F04/F05→F06 | 新建回 F04,编辑回 F05 | `article-created` 或 `article-updated`,目标页刷新一次 | -| F01→F07→F08→F09 | 逐级回实际来源 | 上传完成回 F08 并刷新一次 | -| F01→F10 | F01 | 当前未开放,无伪成功 | +| 流程 | 页面身份 | 取消/返回 | 当前阶段完成语义 | +| --- | --- | --- | --- | +| F01→F02 | `genealogyId` | F01 `{ genealogyId }` | 仅生成“尚未提交服务器”的本地预览,不发布、不产出结果 | +| F01→F03 | `genealogyId + feedId` | F01 `{ genealogyId }` 且恢复现场 | 评论草稿保留在本页,不插入列表、不增加计数、不宣称发送成功 | +| F01→F04→F05 | F04 为 `genealogyId`;F05 再加 `articleId` | F05 回 F04,F04 回 F01 | 收藏禁用并明确暂未开放 | +| F04/F05→F06 | `genealogyId + mode`;编辑再加 `articleId` | 新建回 F04,编辑回精确 F05 | 仅生成未保存预览,不产出文章结果 | +| F01→F07→F08→F09 | F07 为 `genealogyId`;F08/F09 再加 `albumId` | 逐级回实际来源 | 新相册与选图只做独立本地预览,不加入正式列表、不上传、不产出结果 | +| F01→F10 | `genealogyId` | F01 `{ genealogyId }` | 当前未开放,无伪成功 | -- [ ] **步骤 1:先复现两个已知错误栈** +- [x] **步骤 1:先建立身份与伪成功失败合同** -静态合同和 MuMu 先记录 F01→F03 失效→“返回家族圈”会留下两个 F01,F04→F05→返回会留下重复 F04。把预期改为 `returnTo` 后确认旧代码失败。 +`tests/navigation-routes-contract.ps1` 已把 F02—F10 的 `genealogyId`、F03/F05/F08/F09 的复合实体身份、F06 的模式参数及 F01/F04/F05/F08 的零结果能力写成精确合同;`tests/family-fixture-runtime-smoke.js` 已把跨谱拒绝、缺参拒绝、词法字符串 ID 和列表/详情深拷贝写成运行时合同。旧实现分别因 `feed-created` 预注册和缺少共享查询 owner 正确红灯,证明失败来自待迁合同而非测试环境。 -- [ ] **步骤 2:迁移动态与谱文流程** +- [x] **步骤 2:收口唯一身份与只读内容 owner** -F02 成功态主按钮改为“返回家族圈”并调用 `finishPage`;F03 的普通、失效和错误状态都回已有 F01;F06 依据 `editorMode` 精确回 F04 或 F05,不用 `redirectTo` 伪装返回。 +F01 可从全局上下文解析家谱,但在开放任何内容和入口前必须用 `goRoot("F01", { genealogyId })` 把根页 URL 规范化;无有效成员家谱时失败关闭。F02—F10 全部必填 `genealogyId`,实体页按复合身份精确查询,缺参、未知实体和跨谱 ID 均不得回退首条数据。F01 的 R01/R03/R05/R10/R11 入口同轮把 `genealogyId` 交给注册表,R 页面内部迁移仍留在任务 8。 -- [ ] **步骤 3:迁移相册与未开放视频** +`data/mock.js` 是 F 系列只读 feed/article/album 夹具的唯一 owner,只公开按家谱列表和按复合身份详情的深拷贝查询函数;页面与 `utils/api.js` 不得读取或写入裸数组。`createFeed` 只允许真实远端配置下 POST;mock 模式必须以 `WRITE_UNAVAILABLE` 失败关闭。 -F08 的页头与 `onBackPress` 都先关闭照片预览,再返回 F07;F09 有选择或说明未保存时确认,上传中优先阻止离开,成功调用 `finishPage("F08", { albumId }, { operation: "media-uploaded", entityId: albumId, refresh: true })`;F10 使用 `returnTo("F01", {})`,直接进入时网关按规范父页建立 F01。F02/F06/F09 的表单、F03 的评论提交、F07 的新建相册弹层和 F08 的预览都让页头、取消动作与 `onBackPress` 构造同一 context 并统一进入 `runBackGuard`。 +- [x] **步骤 3:迁移动态与谱文流程** -- [ ] **步骤 4:验证 F 系列** +F01—F06 全部只使用导航网关。F02、F03、F06 的页头、Android 返回和显式取消统一处理脏草稿及校验中状态;F03 失效回 F01,F05 失效回 F04,F06 按 `mode=create/edit` 精确回 F04/F05。删除“动态已发布”“评论已发送”“谱文已保存”和收藏二态,任何页面不得调用 `finishPage`。 -运行全部 F 聚焦合同、响应式合同和编译审计;在 MuMu 逐条验证表中流程的进入、返回、取消、完成和重复进入。重点检查 F01/F04 不重复、F08 预览优先关闭、F09 取消不产生上传成功。 +- [x] **步骤 4:迁移相册与未开放视频** + +F07 新建相册只形成独立、明确标注不会保存的本地预览,不插入正式相册列表;弹层与草稿分别按优先级处理。F08 页头和 Android 返回都先关闭照片预览,再回 F07;F09 的选择、批量说明与逐图说明均计入脏状态,校验中阻止离开,本地预览后无结果回同一 `genealogyId + albumId` 的 F08。F10 使用 `returnTo("F01", { genealogyId })`。旧 `/genealogy/app/files/upload` 不得进入运行时;真实上传必须在后续接口批次按线上 resumable init/chunk/complete 取得 `ossId` 后再创建相册照片记录。 + +- [x] **步骤 5:验证 F 系列静态与纯运行时合同** + +F 聚焦合同、共享导航合同、家族夹具、响应式合同和编译审计均已通过。生产 F01—F10 的 `resultOperations` 精确为空,网关运行时逐项证明旧 `feed-created/article-created/article-updated/media-uploaded` 在任何导航发生前被拒绝且不残留结果;泛型完成结果算法改用只存在于测试内存模块的 `TEST_*` 中性路由,不把旧 F 语义伪装成夹具。`data/mock.js` 的列表/详情深拷贝、词法字符串 ID、未知实体与跨谱隔离已由 `tests/family-fixture-runtime-smoke.js` 验证,mock 写动态以 `WRITE_UNAVAILABLE` 失败关闭。 + +最新文件 fresh 全量结果为 PowerShell `122/122`、Node 语法 `36/36`、纯 Node `8/8`;源码扫描为 `MIGRATION-DEBT=13`,剩余 `13 个页面/表单组件文件`,计数为 `navigateTo 17`、`navigateBack 1`、`redirectTo 3`、`reLaunch 1`、`getCurrentPages 1`、业务页面路径字面量 `27`。依赖 `127.0.0.1:9222` 的浏览器脚本没有可连接调试页,本批只完成 `36/36` 语法检查,未冒充浏览器或原生通过。 + +- [ ] **步骤 6:完成 F 系列 MuMu 矩阵** + +在 MuMu 逐条验证表中流程的进入、返回、取消、本地预览和重复进入,重点检查 F01/F04/F07 不重复、F08 预览优先关闭、F09 始终不显示上传成功。受当前约束不得启动、关闭或调整 MuMu,静态与纯运行时绿灯不能冒充原生视觉通过。 ### 任务 8:迁移 R 系列导航 **文件:** +- 修改:`utils/navigation-routes.js` +- 修改:`data/mock.js` +- 修改:`utils/api.js` +- 修改:`pages/tree/t03-member-profile.vue` - 修改:`pages/records/r01-people-list.vue` - 修改:`pages/records/r02-person-detail.vue` - 修改:`pages/records/r03-gift-list.vue` @@ -692,29 +551,55 @@ F08 的页头与 `onBackPress` 都先关闭照片预览,再返回 F07;F09 - 修改:`pages/records/r10-memo-list.vue` - 修改:`pages/records/r11-merit-records.vue` - 修改:`tests/r-business-flow-contract.ps1` +- 修改:`tests/r01-r02-person-flow-contract.ps1` - 修改:`tests/r02-person-detail-contract.ps1` - 修改:`tests/r02-person-detail-runtime-smoke.js` +- 新建:`tests/records-fixture-runtime-smoke.js` +- 修改:`tests/navigation-routes-contract.ps1` +- 修改:`tests/navigation-gateway-runtime-smoke.js` - 修改:`tests/navigation-flow-contract.ps1` +- 修改:`tests/navigation-source-scan-contract.ps1` +- 修改:`docs/接口与页面映射总表.md` +- 修改:`docs/项目当前总览.md` -**流程合同:** +**三方审查结论与流程合同:** -- R01→R02:查看和新建均使用 `openPage`;保存或删除回 R01 并刷新一次,取消只返回。 -- R03→R04:新建、编辑、删除分别返回 `gift-created`、`gift-updated`、`gift-deleted`,失败留在 R04。 -- R05→R06/R07:详情返回 R05;编辑成功回 R05,不能用 `redirectTo` 新建列表。 -- R02/T03→R08/R09:只传 `personId`,删除把姓名当跨页身份的 `personName` 查询合同;页面自行按领域数据显示姓名。 -- R10/R11 不引入额外原生页面流程,但新增记录弹层必须先于页面返回关闭。R02/R04/R07 的保存或删除完成都调用 `finishPage(目标路由, 目标参数, 类型化结果)`;R02/R04/R07/R08/R09/R10/R11 的页头、弹层取消按钮与 `onBackPress` 共享同一 `requestBack`,提交中优先阻止离开,未保存时再确认放弃。 +- R01/R02 归属线上 `lineage/persons`;R03/R04 归属 `relative-records`,不是依赖礼仪 ID 的 ceremony gifts,`giftId` 全量退役为 `relativeId`;R05—R07 归属 `ceremonies`,`ritualId` 全量退役为 `ceremonyId`。R08 只可在同一家谱内按 `lineagePersonId` 过滤 `growth-records`;R10/R11 分别归属 `memos` 与 `merit-records`。 +- R09 必须保持硬关闭:线上没有独立人生事件接口,`growth-records.recordType` 也没有可引用枚举或类型说明;后端给出独立端点或正式字典前,不得自行发明 `LIFE_EVENT`、不得读取或写入成长记录冒充人生事。 +- R01/R03/R05/R10/R11 必填 `genealogyId`;R02 必填 `genealogyId + mode`、可选 `personId`,create 禁止 ID、view 必须有精确 ID;R04 同理使用 `relativeId`;R06 必填 `genealogyId + ceremonyId`;R07 必填 `genealogyId + mode`、可选 `ceremonyId`,create/edit 条件互斥;R08/R09 必填 `genealogyId + personId`。所有 ID 保持词法字符串,未知实体、缺参和跨谱都失败关闭。 +- 当前只允许只读正式列表与独立草稿;所有保存动作只能形成明确的“本地预览,尚未提交服务器”,不得插入正式列表、修改完成状态、增加汇总、生成实体 ID 或宣称删除。生产 R01—R11 的 `resultOperations` 精确为空,真实 API 成功接入前不得调用 `finishPage`。 +- 权限模型只说明需要登录,没有 `canCreate/canEdit/canDelete`;本地成员角色、`sponsorUserId` 和页面 fixture 都不得冒充服务端权限。无效上下文必须隐藏预览入口;本地预览入口不能写成“已保存”或“已删除”。 +- R02/R04/R07/R08/R10/R11 的页头、弹层取消按钮与 `onBackPress` 共享返回守卫;浮层优先关闭、提交中阻止离开、脏草稿确认放弃。R09 只有不可用说明与安全返回。R07 create 预览回 R05,edit 预览回原 R06;R08 返回实际 R02 或 T03 来源。 -- [ ] **步骤 1:先写结果和 ID 失败合同** +- [x] **步骤 1:先写旧参数、复合身份、零结果与假成功失败合同** -要求页面不再包含 `personName=`、`saveResult=` 或直接 Uni 导航调用,并精确断言三组列表/详情/编辑终点。旧实现必须先失败。 +反转当前保护 `saveResult`、`personName`、`giftId`、`ritualId`、直接 Uni 路径、本地 `unshift`、完成状态切换和成功文案的旧测试;网关运行时必须证明旧 person/gift/ritual 结果在导航前被拒绝且无残留。源码扫描的 R01—R07 债务同轮清零。 -- [ ] **步骤 2:按三条独立流程迁移** +- [x] **步骤 2:收紧注册表与精确身份** -依次迁移人物录、贺礼簿、礼仪活动;每条流程完成后运行自己的聚焦合同和固定验证,不把三个领域一次性机械替换。 +先只改注册表和导航消费者:所有子页携带 `genealogyId`,实体页携带精确实体 ID,create/view/edit 条件由页面失败关闭;删除 R 生产结果能力和全部原始 Uni 导航。此步不接真实写接口。 -- [ ] **步骤 3:验证 R 系列** +- [x] **步骤 3:建立只读复合身份 owner** -MuMu 分别验证新建、编辑、删除取消、删除完成、失败重试和连续重复进入;列表滚动、筛选与搜索现场应在返回后保持,完成结果只触发一次刷新。 +R01/R02 复用树成员深拷贝选择器;relative、ceremony、growth、memo、merit 建立唯一只读复合身份 owner,并由 `tests/records-fixture-runtime-smoke.js` 锁定深拷贝、词法 ID、未知实体和跨谱隔离。`utils/api.js` 的 ceremony/growth mock 只能消费该 owner,不能继续返回未隔离的 `familyContent`。 + +- [x] **步骤 4:按领域独立迁移页面** + +依次完成 R01/R02 人物、R03/R04 人情往来、R05—R07 礼仪,再完成 R08/R10/R11 的同页预览;每一域分别运行聚焦合同后才进入下一域。字段使用真实 Body 语义:人物 `name`,往来 `relativeName`,礼仪 `ceremonyTitle/ceremonyType`,成长 `recordTitle`,备忘 `memoTitle`,功德 `donorName/meritTitle`。不明确的枚举不制作选择器。 + +- [x] **步骤 5:关闭 R09 并同步接口缺口** + +R09 不显示 fixture 时间轴、不开放新增,只说明后端缺少独立接口或正式 `recordType` 字典并安全返回;将接口问题写入总表,不能以页面隐藏替代后端问题单。 + +- [x] **步骤 6:执行静态与纯运行时验证** + +运行 R 聚焦合同、导航注册表/网关/源码扫描、记录夹具 Node 冒烟、编译审计、全部 PowerShell 合同和全部 Node 语法检查;同步最新物理计数、迁移债务与明确未执行的环境验证。 + +R01—R11 已按复合家谱身份、受控成员展示投影、只读正式记录和独立本地预览完成静态迁移;R09 因线上没有独立端点或正式 `recordType` 字典保持硬关闭。三方复核未发现 P0/P1,受限成员的私密字段在数据 owner 即被删除,礼仪邀请只以 `inviteeUserId` 与同谱成员受控联接;编辑和弃稿弹层均禁止遮罩误关。最新文件 fresh 全量结果为 PowerShell `122/122`、Node 语法 `37/37`、纯 Node `9/9`;源码扫描为 `MIGRATION-DEBT=6`,剩余 `6 个页面/表单组件文件`,计数为 `navigateTo 8`、`navigateBack 1`、`redirectTo 0`、`reLaunch 0`、`getCurrentPages 1`、业务页面路径字面量 `14`。依赖 `127.0.0.1:9222` 的浏览器脚本只完成语法检查;受当前约束未触碰 MuMu,不能把静态结果冒充原生视觉验收。 + +- [ ] **步骤 7:完成 R 系列 MuMu 矩阵** + +MuMu 分别验证复合身份、跨谱失败、列表滚动、搜索现场、预览不入正式列表、弹层优先关闭、脏草稿确认、连续重复进入与 R09 硬关闭。当前明确禁止启动、关闭或调整 MuMu,因此此项保持未完成,静态绿灯不得冒充原生视觉通过。 ### 任务 9:迁移 N/M 系列与安全通知目标 @@ -752,21 +637,27 @@ export const NOTICE_TARGETS = Object.freeze({ `utils/navigation.js` 新增 `openNoticeTarget(targetType, params, sourceKey = "N02")`:未知类型、缺字段、多字段、非字符串 ID 和原始 URL 一律拒绝;G01 使用 `goRoot`,G10 使用 `openPage`。 -- [ ] **步骤 1:先锁定原始 URL 漏洞** +- [x] **步骤 1:先锁定原始 URL 漏洞** 合同要求 N01/N02 数据只含 `targetType` 与 `targetParams`,源码不得出现 `noticeDetail.value.target` 或 `/pages/...` 目标字符串。旧 N02 因持有 `target` 并直接 `navigateTo` 而失败。 -- [ ] **步骤 2:迁移 N01/N02** +- [x] **步骤 2:迁移 N01/N02** N01 打开 N02 固定使用 `openPage("N02", { id }, "N01")`;N02 默认回 N01。目标无权限、过期、字段不足或类型未知时留在 N02 明确说明,不猜测、不回落空页面。 -- [ ] **步骤 3:迁移 M 页面、邀请语义与退出** +- [x] **步骤 3:迁移 M 页面、邀请语义与退出** M01 菜单由路由字符串改成路由键;M03/M06 使用 `openPage`;M08 删除“邀请码加入后需要管理员审核”及其并行分支,只保留已经定案的“邀请码直接加入且不生成审核记录”,聚焦合同同时断言旧文案不存在。M10 确认退出时按顺序执行 `session.clear()`、关闭确认层、`goRoot("A01")`,退出取消只关闭弹窗。M02/M04/M05/M07 的表单、M08/M09 的分享或说明弹层、M10 的协议和退出确认都让页头与 `onBackPress` 共用同一 `requestBack/runBackGuard`;提交中优先于脏表单,弹层优先于页面返回。 -- [ ] **步骤 4:验证 N/M 系列** +- [x] **步骤 4:完成 N/M 静态验证** -运行 N/M 聚焦合同、两个运行时冒烟、响应式合同和编译审计;MuMu 验证 G01/M01→N01→N02→业务目标、目标失效、返回、重复进入,以及 M01→M10→取消/确认退出。确认退出后原栈被清理且只有 A01。 +N01/N02 现在只消费 `data/mock.js` 的词法消息 ID、`targetType/targetParams` 与 `unread` 字段;安全目标由 `NOTICE_TARGETS` 白名单映射,未知、缺参、多参、原始 URL 和越权目标均失败关闭。M02/M03/M05 统一消费当前用户 owner,M02/M04/M05 仅做明确未提交服务器的本地校验;M07 已在后续独立任务 25 接入真实反馈客户端。M08/M09 在缺少可实施合同时硬关闭伪邀请码和伪订单;M10 从 `manifest.json` 读取版本,并按 `session.clear()`、关闭确认层、`goRoot("A01")` 的顺序退出,账号令牌和当前家谱上下文同时清理。 + +Task9 最新静态结果为 PowerShell `122/122`、Node 语法 `39/39`、纯 Node `11/11`;导航扫描为 `MIGRATION-DEBT=1`,唯一剩余项是零生产消费者 `components/tree/TreeMemberForm.vue`,交由任务 10 原子删除。两份浏览器运行时合同已更新但因 `5173/9222` 均无可用页面只完成语法检查,未冒充浏览器通过。 + +- [ ] **步骤 5:完成 N/M 系列 MuMu 矩阵** + +MuMu 验证 G01/M01→N01→N02→业务目标、目标失效、返回、重复进入,以及 M01→M10→取消/确认退出。确认退出后原栈被清理且只有 A01。当前明确禁止启动、关闭或调整 MuMu,因此本项保持未完成。 ### 任务 10:关闭导航阶段 @@ -774,16 +665,20 @@ M01 菜单由路由字符串改成路由键;M03/M06 使用 `openPage`;M08 - 修改:`tests/navigation-source-scan-contract.ps1` - 修改:`tests/navigation-flow-contract.ps1` +- 删除:`components/tree/TreeMemberForm.vue` +- 删除:`tests/tree-member-form-document-flow-contract.ps1` +- 修改:`tests/t03-t06-all-states-visual-contract.ps1` +- 修改:`tests/responsive-layout-coverage.json` - 修改:`docs/家谱项目全量治理设计.md` - 修改:`docs/家谱项目全量治理实施计划.md` - 修改:`docs/项目当前总览.md` - 修改:`docs/接口与页面映射总表.md` -- [ ] **步骤 1:执行零调用与闭合合同** +- [x] **步骤 1:执行零调用与闭合合同** -运行 `tests/navigation-source-scan-contract.ps1`。预期 pages、components、utils 中网关外的五种 Uni 导航调用和 `getCurrentPages` 数量均为 `0`,唯一允许位置是 `utils/navigation.js`,`switchTab` 全项目为 `0`;路由注册表与 `pages.json` 为 `52/52`。 +`TreeMemberForm.vue` 已经三人确认零生产消费者并删除;旧专属合同同步删除,T04/T05/T06 活动页面接替字体、布局与文档流覆盖,响应式清单和导航债务账本同轮收紧。`tests/navigation-source-scan-contract.ps1` 证明 pages、components、utils 中网关外的五种 Uni 导航调用和 `getCurrentPages` 数量均为 `0`,唯一允许位置是 `utils/navigation.js`,`switchTab` 全项目为 `0`;路由注册表与 `pages.json` 为 `52/52`,结果为 `MIGRATION-DEBT=0`。 -- [ ] **步骤 2:运行全量合同** +- [x] **步骤 2:运行全量合同** 运行本计划“每个代码批次的固定验证”中的全量 PowerShell 循环,再运行: @@ -792,7 +687,7 @@ node tests/navigation-gateway-runtime-smoke.js node tests/password-policy-runtime-smoke.js ``` -预期全部退出码为 `0`;不能删除或放宽与新导航合同冲突的有效业务测试,而应把旧实现断言迁移到新所有者。 +最新文件 fresh 结果为 PowerShell `121/121`、Node 语法 `39/39`、纯 Node `11/11`,编译审计和响应式合同包含在全量绿灯中。合同数量从 122 变为 121 的唯一原因是删除了只保护已退役孤儿组件的专属测试,其有效视觉断言已经迁到 T04/T05/T06 现页合同;没有删除或放宽任何活动业务失败条件。 - [ ] **步骤 3:执行 NAV-MUMU-01 至 NAV-MUMU-08** @@ -925,6 +820,8 @@ PATCH /genealogy/app/v2/genealogies/{genealogyId}/lineage/relationships/{relatio `LineageGraphWindow` 以根字段 `state` 为 discriminator 使用 `oneOf`,枚举只允许 `EMPTY/POPULATED`。EMPTY 分支精确要求 `nodes/familyUnits/edges=[]`,且窗口内 `focusPersonId=null、entryPersonIds=[]、generationRange=null、returnedNodeCount=0、boundaries=[]`;POPULATED 分支要求 nodes 非空、`focusPersonId` 引用其中一个 VISIBLE 节点、`entryPersonIds` 非空且全部引用当前 nodes、`generationRange` 非空,并满足 `returnedNodeCount === nodes.length`。 +非空 `generationRange` 固定为 `{ minGeneration, maxGeneration }`,两项均为大于等于 1 的整数且关闭额外字段;大小顺序由运行时 validator 验证。四条 v2 操作统一声明 `200/400/401/403/404/422/429/5XX`,tree、overview、relationship PATCH 另声明 `409`。错误响应根层必填字符串字段固定为 `businessCode`;每个稳定业务码必须落在对应响应 `oneOf` 分支的单值 enum(OpenAPI 3.1 可用 `const`)中,不能从 description、example 或无关 metadata 搜索得出。 + `entryPersonIds` 精确等于当前返回窗口中没有 primary 入边的节点集合,不代表全谱始祖;secondary 入边不取消入口身份。入口节点 `entryReason` 只允许 `GENEALOGY_ROOT/WINDOW_CUT/DISCONNECTED_COMPONENT`,非入口固定为 `null`。每个入口在当前窗口恰有零条 primary 入边,每个非入口恰有一条 primary 入边;`GENEALOGY_ROOT` 表示全谱主森林根,`WINDOW_CUT` 表示 canonical primary 父边在窗口外,`DISCONNECTED_COMPONENT` 表示没有可达全谱根的 canonical primary 链。全谱根只由 overview 的可见根/隐私根计数和 locator 的根可见性分支表达。 人物以 `visibility` 为 discriminator 使用 `oneOf`,枚举只允许 `VISIBLE/REDACTED`。`VisibleLineagePerson` 精确包含 `id,generation,displayName,sex,avatarOssId,branchId,branchPath,order,visibility=VISIBLE,entryReason`,其 ID 是可用于 FOCUS、locator、搜索和写接口且不得以 `redacted:` 开头的稳定人物 ID,`sex` 只允许 `MALE/FEMALE/UNKNOWN`;`RedactedLineagePerson` 只允许 `id,generation,displayName,order,visibility=REDACTED,entryReason`,`displayName` 固定为“隐私成员”,opaque ID 精确使用 `redacted:{treeVersion}:{token}` 并仅能在该 `treeVersion` 内作当前图内部引用,不得用于 FOCUS、locator、搜索或写接口,也不得进入任何 `focusPersonId/targetPersonId`,并且不得返回 `sex/avatarOssId/branchId/branchPath` 或其他可推断身份字段。两类人物的 `generation` 都是大于等于 1 的整数,`order` 是非负整数;可见人物的 `avatarOssId` 只允许非空字符串或 `null`,其他实体、关系和引用 ID 必须是非空字符串。 @@ -954,11 +851,11 @@ PATCH /genealogy/app/v2/genealogies/{genealogyId}/lineage/relationships/{relatio 两个请求分支都设置 `additionalProperties: false`,并以 `minProperties` 或等价 `anyOf(required)` 约束至少一个可修改字段;`relationshipKind` 必填且只作判别与一致性校验,不能通过 PATCH 改值。测试必须覆盖单字段更新、双字段更新、未提交字段保持原值,以及只有 `relationshipKind` 的空更新稳定失败为 `422 RELATIONSHIP_PATCH_EMPTY`。 -- [ ] **步骤 1:先写严格接口合同并确认当前红灯** +- [x] **步骤 1:先写严格接口合同并确认当前红灯** -`tests/lineage-openapi-contract.ps1` 读取 `APP.openapi.json`,逐项断言上述四条固定 `/genealogy/app/v2/...` 路径、FOCUS/BOUNDARY 互斥参数、overview 必填 `treeVersion`、响应引用、两个 EMPTY/POPULATED schema、locator 根可见性 `oneOf`、`additionalProperties: false`、关系 `oneOf/discriminator`、完整枚举、稳定关系 ID、字符串 ID、nullable 头像、1/200/500 限制、`400/401/403/404/422/429/5xx`、409 和 `If-Match`;同时断言 App 的新 schema 不挂到 v1 树响应。YAML 必须存在相同 v2 路径与 schema 名。OpenAPI 静态合同只负责可表达的结构约束,跨 bucket 计数、根隐私、请求版本与响应版本一致性必须由任务 12 的运行时 validator 冒烟覆盖,不能用关键词或 schema 存在冒充。运行后预期明确报告缺少 overview、locator、relationship patch 和新 schema,而不是泛化为“接口错误”。 +`tests/lineage-openapi-contract.ps1` 已读取受保护双导出并先聚合前置缺口,再在前置项齐全时逐项断言上述四条固定 `/genealogy/app/v2/...` 路径、参数全集、overview 必填 `treeVersion`、响应 `$ref` 闭包、两个 EMPTY/POPULATED schema、locator 根可见性 `oneOf`、`additionalProperties: false`、关系 `oneOf/discriminator`、完整枚举、稳定关系 ID、字符串 ID、nullable 头像、1/200/500 限制、统一状态矩阵、根层 `businessCode`、409 和 `If-Match`;同时递归证明新 schema 不挂到 v1 树响应,并在限定的 YAML paths/components 块中对齐 operationId 和完整 component 引用闭包。当前结果为预期 `LINEAGE-OPENAPI-CONTRACT BLOCKED`,精确列出 JSON/YAML 各缺四条操作和三个固定根模型。平铺 query parameters 无法由 OpenAPI 静态证明 FOCUS/BOUNDARY 跨字段互斥,因此静态层只锁参数全集、枚举和单字段边界;互斥、条件必填、跨 bucket 计数、根隐私、请求/响应版本一致性、409/422 行为和 PATCH 字段保持由运行时 validator 与部署集成测试负责,不能用 description、example、关键词或 schema 存在冒充。YAML 门禁只证明路径、operationId 和引用闭包一致;双导出到位后仍逐字段人工复核,不能把文本解析夸大为完整 YAML 语义解析。 -- [ ] **步骤 2:把问题单写入唯一映射总表并交给后端** +- [x] **步骤 2:把问题单写入唯一映射总表并交给后端** 在 `docs/接口与页面映射总表.md` 的 T01 专节记录当前值、目标值、受影响页面、请求/响应、错误码、权限、验收步骤和上述 JSON 示例。不得新建第六份 Markdown。 @@ -1230,8 +1127,12 @@ mp-weixin 消费同一 Scene 和相机纯函数,通过平台 Canvas API 绘制 - 修改:`pages/tree/t01-tree-overview.vue` - 修改:`pages/tree/t03-member-profile.vue` - 修改:`pages/tree/t04-add-relative.vue` +- 修改:`pages/tree/t05-edit-member.vue` - 修改:`pages/tree/t06-edit-relationship.vue` - 修改:`pages/tree/t07-member-directory.vue` +- 修改:`utils/navigation-routes.js` +- 修改:`tests/navigation-flow-contract.ps1` +- 修改:`tests/t03-t08-member-flow-contract.ps1` - 修改:`tests/lineage-graph-contract-runtime-smoke.js` - 新建:`tests/t01-navigation-integration-contract.ps1` @@ -1243,12 +1144,13 @@ appApi.getLineageOverview(genealogyId, treeVersion) appApi.locateLineagePerson(genealogyId, personId, treeVersion) appApi.searchLineagePeople(genealogyId, keyword, cursor, limit) appApi.createRelative(genealogyId, personId, payload, treeVersion) +appApi.updateLineagePerson(genealogyId, memberId, payload, treeVersion) appApi.updateRelationship(genealogyId, relationshipId, payload, treeVersion) ``` - [ ] **步骤 1:先改 API 合同并确认旧适配失败** -测试禁止 `toTreeNode`、`.map(toTreeNode)`、`treeMembers`、`id || personId`、客户端 `x/y` 和数字 ID;要求六个新方法、窗口与 overview 各自的查询参数白名单、overview 必填 `treeVersion`、`If-Match` 和规范化/校验调用。REDACTED opaque ID 进入 FOCUS、locator、搜索、写接口或聚合 target 必须在客户端边界失败。 +测试禁止 `toTreeNode`、`.map(toTreeNode)`、`treeMembers`、`id || personId`、客户端 `x/y` 和数字 ID;要求七个新方法、窗口与 overview 各自的查询参数白名单、overview 必填 `treeVersion`、所有三个写方法的 `If-Match` 和规范化/校验调用。REDACTED opaque ID 进入 FOCUS、locator、搜索、写接口或聚合 target 必须在客户端边界失败。成员更新还必须以权限能力和当前 `treeVersion` 为前置条件,旧的无版本人物更新接口不能作为兼容回退。 - [ ] **步骤 2:原子替换旧 API 与 mock** @@ -1258,11 +1160,15 @@ appApi.updateRelationship(genealogyId, relationshipId, payload, treeVersion) 展开 UNLOADED 只携带当前 boundary cursor 和 treeVersion;新窗口校验成功后原子合并或替换。收到 `409 TREE_VERSION_CHANGED` 时保留当前焦点,提示数据已更新并用 locator 重新定位,不能把新节点配旧边。 -- [ ] **步骤 4:实现 T04/T06 完成回流** +- [ ] **步骤 4:实现 T04/T05/T06 完成回流** -写成功结果带新 `treeVersion` 进入领域响应,但导航一次性结果仍只传 `operation/entityId/refresh`;T01 收到后通过 locator 或新窗口响应定位 `entityId`。取消、写失败和版本冲突都不得生成成功导航结果。 +写成功结果带新 `treeVersion` 进入领域响应,但导航一次性结果仍只传 `operation/entityId/refresh`;T01 收到新增亲属或关系更新后通过 locator 或新窗口响应定位 `entityId`。取消、写失败、权限失败和版本冲突都不得生成成功导航结果。 -`tests/t01-navigation-integration-contract.ps1` 只验证 T01 与已经完成的导航网关之间的焦点回流、取消和版本冲突;本阶段只运行既有 `tests/navigation-flow-contract.ps1` 作为回归门禁,绝不修改它,避免 T01 接口阶段反向改变已关闭的导航合同。 +T05 的真实更新必须在本步骤一次性拆分宿主页身份 A 与编辑实体 B,不提前留下兼容字段:T03 从自身初始查询保存不可变 `routePersonId=A`,页内活动成员为 `activePersonId=B`;T05 路由必填参数精确改为 `genealogyId/personId/memberId`,其中 `personId=A` 只表达宿主 T03 身份,`memberId=B` 只表达被编辑成员。T03 打开 T05 时传入两者,T05 源码立即别名为 `hostPersonId` 与 `editedMemberId`;API 只能以 `editedMemberId` 写入,完成目标只能以 `hostPersonId` 匹配 T03。删除旧的“T05 `personId` 同时代表编辑对象”合同,禁止 `memberId || personId` 双读,旧 URL 因缺少 `memberId` 必须失败。 + +真实写成功后,T05 以目标 `{ genealogyId, personId: hostPersonId }` 和一次性成员更新结果返回;结果的 `entityId` 必须是 `editedMemberId`。同轮给 T03 增加对应结果操作:仅当 `entityId` 等于当前活动成员时重新读取该成员,不调用 `openRelative`,不改变 URL、轨迹或索引;实体不匹配时失败关闭。测试必须覆盖 A/B 正常回流只刷新 B、来源宿主 C 到目标 A 被拒绝且零导航/零结果、目标误传 B 被拒绝、API 实参只能是 B、写失败或 409 零结果,以及深链 T05 要求 A 与 B 相同后才能构造 T03(A)。当前 `personId=A/memberId=B` 方案复用导航网关已有同名上下文门禁;`parentParamMap` 仍只负责无历史时构造父页,不能扩张成完成结果身份映射。 + +`tests/t01-navigation-integration-contract.ps1` 验证 T01 与导航网关之间的焦点回流、取消和版本冲突;`tests/t03-t08-member-flow-contract.ps1` 验证宿主页/编辑实体双身份与 T03 刷新不改轨迹。真实写结果会改变路由允许操作和页面消费者,因此必须在同一合同迁移中同步收紧 `tests/navigation-flow-contract.ps1` 与路由注册表,删除任务 6 的“本地预览无结果”旧断言;不能让运行时接受新合同而校验器仍只允许旧合同。 - [ ] **步骤 5:验证接口与回流** @@ -1279,7 +1185,7 @@ appApi.updateRelationship(genealogyId, relationshipId, payload, treeVersion) - [ ] **步骤 1:运行全部自动化验证** -运行全部 `tests/*.ps1`、全部 lineage Node 冒烟、导航 Node 冒烟和密码策略冒烟。预期全部退出码为 `0`,Vue 覆盖仍为 `66/66`,`100% 100% no-repeat` 与 Vue 直接 `border-image-slice` 扫描仍为 `0`。 +运行全部 `tests/*.ps1`、全部 lineage Node 冒烟、导航 Node 冒烟和密码策略冒烟。预期全部退出码为 `0`,Vue 覆盖仍为 `64/64`,`100% 100% no-repeat` 与 Vue 直接 `border-image-slice` 扫描仍为 `0`。 - [ ] **步骤 2:执行 T01-MUMU-01 至 T01-MUMU-10** @@ -1313,20 +1219,371 @@ appApi.updateRelationship(genealogyId, relationshipId, payload, treeVersion) 三人分别从真实数据、几何连续性、交互、视觉完整性、接口严谨性、无障碍、性能和维护成本反向质询。只有十条几何不变量、全部性能门槛、MuMu 十组矩阵和自动化合同同时通过,才把 T01 标记完成。 +--- + +## 第三阶段:认证、TAC 与短信闭环 + +### 任务 20:建立认证 OpenAPI 硬门禁 + +- [x] **步骤 1:先写失败合同并汇总后端问题单** + +`tests/auth-tac-openapi-contract.ps1` 已对受保护 JSON/YAML 同时检查三个公开认证场景、统一 `SmsCodeSecret`、`validToken`、密码登录票据,以及验证请求的必填字段、关闭额外字段和 provider discriminator。当前精确输出为 `AUTH-TAC-OPENAPI-CONTRACT BLOCKED`:密码登录无票据;`VerificationCheckBody` 缺 provider 分支闭包;TianAi 与系统图片 payload 均未关闭额外字段;生产短信码尚未原子升级为 6 位,公共发码仍错误包含 `APP_PHONE_CHANGE`。真实 `APP_REGISTER` challenge 还返回 HTTP 500 空响应。对应问题固定为 `API-AUTH-TAC-001`—`004` 与 `API-PHONE-001/002`,不得通过放宽测试或修改受保护导出变绿。 + +### 任务 21:接入受保护 TAC 资产与严格适配器 + +- [x] **步骤 1:建立唯一场景与票据 owner** + +`utils/auth-verification.js` 精确持有 `APP_SMS_LOGIN/APP_REGISTER/APP_FORGOT_PASSWORD`、4 位短信码、require 和 verify 响应边界;`static/tac/js/jiapu-tac-adapter.js` 是 TianAi challenge/proof/verify 的唯一映射层。未知 provider/type、`required=false` 却无票据、空 token、scene 或 subject 漂移全部失败关闭。 + +- [x] **步骤 2:建立真实 renderjs 浮层** + +`components/TacVerification.vue` 加载后端提供且哈希锁定的 CSS/JS/图片,不修改四个供应商文件。浮层以严格 2xx JSON XHR 请求 challenge/verify,覆盖空体、非对象、HTTP 错误、超时、主动 abort、重复成功/取消和迟到 generation 回调;只有服务端 `passed=true + validToken` 才向页面完成。 + +- [x] **步骤 3:建立壳层无障碍预检** + +浮层已补齐 dialog 命名与说明关联、初始聚焦、Tab 圈定、Escape/Android 返回、焦点恢复、原生刷新/关闭、48px 目标和小视口内部滚动。`tests/tac-shell-accessibility-contract.ps1` 与 `tests/auth-accessibility-static-contract.ps1` 只证明静态壳层,不得冒充 TianAi 拖动本身支持 TalkBack。 + +### 任务 22:接通 A01/A04/A05 的认证客户端 + +- [x] **步骤 1:短信登录、注册和找回密码测试先行接线** + +A01 默认短信登录并以 `APP_SMS_LOGIN` 发码;A04/A05 分别使用 `APP_REGISTER/APP_FORGOT_PASSWORD`。三页当前均按“本地校验→require→TAC challenge/verify→携 `validToken` 发短信→4 位码提交”执行,手机号改变作废旧验证上下文,注册和重设提交不重复 TAC;任务 34 已把生产目标收紧为统一 6 位 `SmsCodeSecret`,后端、生成器、模板与全部消费者必须同版原子替换,当前代码不提前双读。A01 密码登录因 `API-AUTH-TAC-001` 保持可见但不可用;A04 成功保存 `AppLoginVo.access_token` 后直接 `goRoot("G01")`,不能先回 A01 再让用户重复登录;A05 成功使用同一 `AppDialog` 返回 A01。 + +- [x] **步骤 2:收紧认证传输生命周期** + +`utils/api.js` 的 `createRequestController/isRequestCancelled` 是认证与已审查业务请求共用的唯一生命周期入口;严格请求只接受 HTTP 200 和带整数 `code` 的 envelope,超时统一为 15 秒。登录、注册和 challenge 等有实体响应要求 `data`;短信发送与找回密码的 `RVoid` 没有声明 `data` 必填,`requestAuthVoid` 把省略 `data` 与 `data:null` 都归一为 `null`。返回或卸载先拒绝等待 Promise 再 abort RequestTask,迟到 native fail 不得覆盖 `REQUEST_CANCELLED`;旧认证专用公开入口已原子删除,按钮在进行中锁定并显示明确文案,不存在伪成功或本地验证码。 + +- [x] **步骤 3:完成客户端自动化验证** + +认证/TAC 聚焦合同、renderjs/adapter/API/场景纯 Node 冒烟、响应式、文档流和编译审计已通过;后续任务 24—35 纳入后,最新项目总证据为 PowerShell `126/140`、Node 语法 `47/47`、纯 Node `19/19`、活动 Vue 脚本模块 `64/64`。十四项红灯全部是显式发布门禁,不是可删除测试:T01、认证 OpenAPI、家谱工作区 OpenAPI、G03 原子创建 OpenAPI、G03 客户端激活、M06 帮助 OpenAPI、个人资料读取 OpenAPI、个人资料写入 OpenAPI、通知读取 OpenAPI、通知已读写入 OpenAPI、M10 服务端退出 OpenAPI、M04 密码凭证 OpenAPI、M05 手机号换绑 OpenAPI 与 Android 认证无障碍证据。 + +### 任务 23:关闭认证外部门禁并完成原生发布验收 + +- [ ] **步骤 1:后端关闭 `API-AUTH-TAC-001`—`004`** + +验证码中心成为 provider-neutral 唯一 owner:session 与 challenge 绑定 tenant/client/scene/canonical subject;provider evidence 只由服务端核验;票据绑定 method/assurance/audience。短信端在同一事务中完成 `ISSUED → CONSUMED` 和唯一 outbox,相同幂等键返回原结果,并发或重放最多产生一个发送任务。修复线上空 500,发布同版本 JSON/YAML 后重新运行 OpenAPI 门禁。 + +- [ ] **步骤 2:实现可访问且不降风控的替代路径** + +P0 是同一验证中心和可恢复的文字/中继 `MANUAL_REVIEW`,不是免验证。各 scene 定义独立身份或号码控制证据;案件不可改写原 subject/scene,具备去重、RBAC、限额、审计、服务时段、容量和 SLA,高风险找回/换号双人复核。坐席只提交决定;用户重新进入原流程时,由验证中心激活同一短时单次票据。中国大陆非交互风控供应商限时 POC 达标后才可成为默认自动路径;音频另做可懂度、听障覆盖和 ASR POC,不能单独上线。设备断言只有在同一 subject 的已认证会话绑定私钥、服务端 nonce、RP/App 绑定、`userVerification=required`、检查撤销且防重放时才可独立放行;设备指纹和完整性只能加权。 + +- [ ] **步骤 3:切换远端并完成 H5/Android 验收** + +后端门禁全绿后才把 `runtimeConfig.mode` 从 `mock` 切为 `remote`。验证正式 H5 域名 CORS、真实正常/过期/重放/限流/供应商故障、前后台倒计时恢复和并发幂等;在用户现有 MuMu 中由三人共同完成 A01/A04/A05 默认、错误、取消、重复进入、Android 返回、TalkBack、外接键盘与非拖动替代路径。证据写入 `tests/manual/tac-android-accessibility-evidence.json` 后运行发布门禁;浏览器截图不得替代。 + +## 第四阶段:领域上下文与独立真实接口 + +### 任务 24:建立会话与当前家谱上下文基础 + +- [x] **步骤 1:测试先行收紧词法身份与账号隔离** + +`utils/genealogy-context.js` 只接受无边界空白的非空字符串 ID,拒绝可能已经失真的 JavaScript number、重复列表、对象和控制字符;损坏存储立即清理。`utils/session.js` 在损坏令牌、退出和令牌切换时同步清理家谱上下文,同一令牌保存不误清。 + +- [x] **步骤 2:撤权与显式无权目标失败关闭** + +首次没有历史选择和失效标记时才允许建立首个可用上下文。历史选择从调用方给定的新列表消失或显式请求的家谱不可用时,`reconcileCurrentGenealogyId` 清空 ID 并持久写入 tombstone,后续重载继续失败关闭;只有用户显式选择、退出或账号切换才清除标记。G01 显示“当前家谱已不可用”并要求用户明确选择,页面不得用 `currentGenealogy || list[0]` 掩盖失效。真实权限变化的 onShow/事件刷新仍属于后续 workspace,不把纯函数闭环夸大为服务端撤权已实时生效。 + +### 任务 25:接通 M07 真实反馈客户端 + +- [x] **步骤 1:锁定 OpenAPI 与 wire payload** + +`tests/feedback-openapi-contract.ps1` 同时保护 JSON/YAML 中的 `POST /genealogy/app/feedback`、SaToken 与唯一 `FeedbackBody`。`appApi.submitFeedback` 只接受三个自有普通数据字段;`feedbackContent` trim 后非空,`feedbackType/contactInfo` 为无枚举可选字符串,空值不发送。调用方请求选项只允许 `requestController`,不能关闭认证头。remote 精确要求 HTTP 200 与整数成功 `code`;响应 schema 没有声明 `data` 必填且页面不消费返回实体,因此反馈请求不复制认证接口的 data 必需规则。mock 抛 `WRITE_UNAVAILABLE` 且不发请求。 + +- [x] **步骤 2:完成防重复、异常与可访问状态机** + +M07 以提交前规范快照锁定请求,提交中禁用类型、内容、联系方式和按钮;成功后保留已提交内容并把该快照提升为 baseline,原样禁止重提,编辑后恢复 ready,改回快照恢复成功态。成功与 uncertain 回流都再次比较当前快照,迟到输入不冒充已提交。超时、断网、意外 2xx/3xx、HTTP 408/5xx 和响应无效进入 `uncertain`,锁定同一快照且不提供原样重试;只有确定拒绝进入可重试 error。反馈类型明确选填,原生按钮有 group 名称、`aria-pressed`、44dp 目标和完整 reset,必填失败聚焦内容框;页面状态机已由纯 Node 直接执行验证,不再只有静态 token 断言。unknown 锁不跨重启持久化,避免保存反馈隐私或形成无期限锁;真正跨重启防重交给未来后端幂等合同。 + +- [ ] **步骤 3:真实服务与 MuMu 验收** + +该步骤依赖任务 23 关闭认证远端门禁。切换 remote 后验证真实登录态、成功 envelope、401、业务拒绝、超时未知结果和卸载取消;在 MuMu 检查键盘、TalkBack、长文本、错误回执和系统返回。浏览器或静态合同不得替代。 + +### 任务 26:建立家谱工作区远端硬门禁 + +- [x] **步骤 1:三人只读核对 G01/G05、线上 OpenAPI 与部署行为** + +唯一读取 owner 固定为 `/genealogy/app/genealogies/mine` 和 `/genealogy/app/genealogies/{genealogyId}/overview`;首批不同时请求语义重复的 `/{genealogyId}`。线上 `RListAppGenealogyVo/RAppGenealogyVo/AppGenealogyVo` 虽已类型化,但均无 `required`;响应 `genealogyId` 为 `integer/int64`,`roleType/status/memberStatus` 无枚举,且没有 `canView`。无令牌只读实测三条接口均为 HTTP 200、JSON `{code:401,msg,data:null}`,与文档声明的 401 string 不一致。缺少 security 注解和媒体声明 `*/*` 作为发布文档质量问题记录,不把它们单独虚报为已发生泄漏。 + +- [x] **步骤 2:先写失败合同并固定最小闭包** + +`tests/genealogy-workspace-openapi-contract.ps1` 同时读取受保护 JSON/YAML,要求 `/mine → RListAppGenealogyVo`、`/overview → RAppGenealogyVo`,两层 envelope 的 `code/data` 必填,`data` 精确指向 `AppGenealogyVo`。首批消费字段固定为必填 `genealogyId/genealogyName/canView/canManage/canEditContent/roleType`:ID 和名称为非空字符串,三项 capability 为 boolean,角色是至少两个稳定非空值的 enum。测试当前输出 `GENEALOGY-WORKSPACE-OPENAPI-CONTRACT BLOCKED`;不得删除测试或手改双导出变绿。 + +- [ ] **步骤 3:后端关闭 `API-GENEALOGY-WORKSPACE-001`—`003`** + +`001` 关闭 JSON int64 身份失真并补 typed required 闭包;`002` 让 `/mine` 以 required `canView` 或可自动证明的等价 active-only 投影成为当前账号可访问集合真相,并提供稳定角色/能力;`003` 让认证失效、对象无权/撤权、删除和服务异常在文档与部署中可稳定区分。错误可以使用规范 HTTP 状态,也可以继续 HTTP 200+业务码,但 JSON/YAML、运行时 validator 与有效 token 集成反例必须一致。后端必须从同一版本重新导出两份受保护源文件;本地不手工补 schema。 + +- [ ] **步骤 4:门禁通过后测试先行接入 G01/G05** + +新 adapter 只 pick 已声明字段并允许未知额外字段;任何 unsafe number、空 ID/名称、重复 ID、错误 data 形状、未知角色或 capability 缺失都失败关闭。G01 在 `onShow` 或统一失效事件中取消旧请求、严格读取 `/mine` 后才 reconcile;网络/5xx 保留当前现场,明确撤权才写 tombstone。G05 加载 `/overview` 前清空上一家谱数据并取消迟到响应,远端失败不回退 fixture。地点、堂号、人数和简介可选且诚实降级;若产品坚持展示线上未提供的来源、管理者、认证、始祖、支系、更新时间和激活人数,则另立合同,不从 mock 猜值。 + +- [ ] **步骤 5:真实账号与 MuMu 验收** + +用至少两个有效账号验证本人列表、跨账号 ID、撤权、删除、401、403/404 或等价业务码、5xx、超时、畸形 JSON、快速切谱与离页取消;再在用户现有 MuMu 中验证加载/空/失败/失效/重选、G01 列表与切换弹层、G05 权限入口和 Android 返回。浏览器和静态合同均不能替代。 + +### 任务 27:建立 M06 帮助内容远端硬门禁 + +- [x] **步骤 1:三人只读核对 M06、线上 OpenAPI 与部署行为** + +线上列表 `GET /genealogy/app/help-articles` 返回 `RListHelpArticleVo`,每项 `HelpArticleVo` 已同时包含分类、标题与完整正文;详情端点因此不是当前单页手风琴闭环的必要依赖。受保护双导出仍返回通用 `ListResult/RList`,线上 wrapper 与 VO 又全部没有 `required`,正文没有纯文本/富文本语义,也没有只返回已发布内容和响应顺序的保证。匿名实测列表、带分类列表和详情均返回 HTTP 200+业务 `code=401`,与线上文档的 HTTP 401 string 不一致。源码还确认 M06 只有本地问题、筛选空态和 ``,没有加载、错误、取消、重试或完整无障碍语义。 + +- [x] **步骤 2:反向质询并选定 list-only 最小闭环** + +一位评审最初要求字符串 `helpId` 加列表/详情二次读取,另一方案指出列表已带正文,M06 没有深链、写操作或跨刷新实体身份。三人最终一致选择列表唯一 owner:adapter 只投影 `helpCategory/helpTitle/helpContent`,不读取或暴露 `helpId`,因此当前 JSON int64 只作为未来详情债务。分类从列表标签按首次出现顺序派生,“全部”由客户端拥有;每次响应先分配 generation+ordinal 展示键,再做搜索/筛选,变更筛选或刷新前清空展开项,测试即可关闭 index 错绑反例。 + +- [x] **步骤 3:先写失败合同并固定最小 schema** + +`tests/help-center-openapi-contract.ps1` 同时读取受保护 JSON/YAML,要求列表 200 精确引用 `RListHelpArticleVo`、SaToken、required `code/data` 和 `HelpArticleVo[]`;每项 required `helpCategory/helpTitle/helpContent`,三项为非空字符串,正文声明 plain-text。当前输出 `HELP-CENTER-OPENAPI-CONTRACT BLOCKED`;不得手改双导出或用 live-only 字段写 adapter。 + +- [ ] **步骤 4:后端关闭 `API-M06-001`—`003`** + +`001` 提供专用列表响应与 required 最小闭包;`002` 明确分类为可直接展示标签、正文首版为纯文本、用户列表只含已发布文章且数组顺序就是展示顺序;`003` 统一 SaToken、JSON 媒体、HTTP 401 或业务 401 的文档与部署行为。后端从同一 Apifox 版本重新导出 JSON/YAML 并通过门禁;详情 `helpId` 改为字符串仅在未来消费详情时要求,不阻塞当前 M06。 + +- [ ] **步骤 5:门禁通过后测试先行接入页面** + +先写 adapter 和页面状态失败测试:allowlist 投影必须丢弃即使已失真的 `helpId`;过滤不重编号,搜索/分类/刷新先清展开,旧 generation 迟到不替换。随后实现唯一可取消列表请求、加载、服务端空、搜索无结果、失败重试、认证失效、动态分类和纯文本展开;失败不得偷偷回退本地 FAQ。分类与问题使用原生按钮,补齐 `aria-pressed/aria-expanded/aria-controls`、状态播报和 44dp 目标,不接详情、分页、封面、浏览量或富文本。 + +- [ ] **步骤 6:真实账号与 MuMu 验收** + +任务 23 关闭认证门禁后,验证有效/失效令牌、只发布与顺序、空列表、畸形行、5xx、超时、重试、快速刷新和卸载取消;再在 MuMu 检查搜索、分类、长正文、系统字号、TalkBack、焦点、Android 返回和 M06→M07。浏览器与静态合同不能替代。 + +### 任务 28:建立个人资料读取远端硬门禁 + +- [x] **步骤 1:三人只读核对 M01/M02/M03、线上 OpenAPI 与部署行为** + +唯一读取路径为 `GET /genealogy/app/auth/profile`。受保护双导出仍返回通用 `ObjectResult/RObject`;线上已改为 `RAppProfileVo → AppProfileVo`,但 envelope 与实体没有 required,手机号无 pattern,姓名/邮箱没有长度和格式边界,operation 也缺有效 security/clientid。匿名实测返回 HTTP 200+业务 `code=401`,与文档 HTTP 401 string 冲突。页面仍直接消费 `{name,phone,role}` fixture:M01 把资料错误连带整页入口隐藏并用 query 制造假错误,M02 把昵称冒充真实姓名,M03 在 setup 时固化手机号。 + +- [x] **步骤 2:反向质询并固定最小隐私投影** + +三人先比较“四项 wire 字段全 required”和“仅 phone required”。最终选择后者:合法旧账号可能没有昵称、实名或邮箱,三项未设置时唯一省略;出现则必须是非空规范值。未来 PUT 只提交 dirty fields 和省略保持原值必须由写批次另证,不能用未知写语义反向扩大 GET 门禁。首批完全丢弃 `userId/avatar/status` 等未消费字段,因此 int64 与状态字典不阻塞;M01 家谱级“创建者”删除。原始手机号只在 adapter 局部校验并立即变成掩码与读屏尾号,不能进入页面、缓存、错误、日志或路由。 + +- [x] **步骤 3:先写失败合同并固定最小 schema** + +`tests/profile-openapi-contract.ps1` 同时读取受保护 JSON/YAML,要求 GET 200 精确引用 `RAppProfileVo`、SaToken、required `code/data` 与 `AppProfileVo`;实体只 required canonical 11 位 `phone`,可选 `nickName/realName/email` 出现时分别满足 1—30、1—30、email 且 1—100。当前输出 `PROFILE-OPENAPI-CONTRACT BLOCKED`,不手改双导出或写 live-only adapter。 + +- [ ] **步骤 4:后端关闭 `API-PROFILE-READ-001`—`003`** + +`001` 提供专用响应与最小 required/type/length/format;`002` 把“属性省略”固定为三项可选资料唯一未设置形态,并确认返回 canonical 原始手机号供客户端立即掩码;`003` 统一 required clientid、SaToken/security、JSON 媒体、HTTP 401 或业务 401 的文档与部署行为。后端从同一 Apifox 版本重导 JSON/YAML,门禁通过后才开始代码批次。 + +- [ ] **步骤 5:门禁通过后测试先行接入 M01/M02/M03** + +先写 normalizer 测试,证明 optional 省略变内部空串、出现 null/空白/超长/非法邮箱失败,超大 `userId/avatar` 被丢弃,明文手机号不在输出或异常中;mock/remote 走同一模型,错误 runtime config 失败关闭。再实现严格、可取消 GET:M01 只替换身份卡且移除假 query 状态和 fixture 角色;M02 填表后才设 baseline,保存仍为本地校验;M03 只局部加载手机号。通知数量不得与真实资料混装,M05 的当前号码在 remote 发布前也迁移同一掩码 owner 或隐藏。原子更新禁止 API 的旧测试断言,并补局部错误、重试、取消、迟到响应和原生按钮无障碍。 + +- [ ] **步骤 6:真实账号与 MuMu 验收** + +任务 23 关闭认证门禁后,用有/无可选资料账号验证加载、掩码、认证失效、畸形数据、5xx、超时、重试、账号切换和取消;MuMu 检查长昵称、系统字号、TalkBack 尾号播报、焦点、M02 键盘/错误、Android 返回和 M03→M04/M05。静态审查不能替代。 + +### 任务 29:建立通知读取远端硬门禁 + +- [x] **步骤 1:三人只读核对 N01/N02/M01/G01、线上 OpenAPI 与部署行为** + +线上提供 `GET /genealogy/app/notifications` 与 `GET /genealogy/app/notifications/unread-count`;受保护双导出只有通用通知列表且缺 unread-count,线上 `RListNotificationVo/NotificationVo/RLong` 又无 required、枚举、长度、完整正文、顺序或容量保证。四个页面当前分别克隆 fixture:N01/N02 的本地已读不能同步,M01/G01 的非响应式未读数也可能长期不一致。匿名 list/count 实测都是 HTTP 200+业务 `code=401`,与线上文档声明的 HTTP 401 string 不一致;operation 只有文字鉴权,没有有效 security/clientid。 + +- [x] **步骤 2:反向质询并固定无 ID 的完整活动集合** + +“只取最新 200 条”和“无分页完整列表”不能并存,否则角标可能指向用户永远看不到的未读项。三人最终固定首版活动集合本身最多 200 条,列表无筛选完整返回且最新优先,未读数精确统计同一集合的 `UNREAD`;跨请求并发变化允许瞬时不等。读取 adapter 只公开完整标题/正文、时间、读状态和 generation+ordinal `snapshotKey`,所有服务端 ID、发送者和业务目标字段全部丢弃。N02 只读当前内存快照;无快照时提示返回消息中心,不虚构服务端过期原因。 + +- [x] **步骤 3:先写失败合同并固定最小 schema** + +`tests/notification-read-openapi-contract.ps1` 要求两条 GET 的 SaToken、required clientid、专用 `RListNotificationVo/RNotificationUnreadCount`,两层 `code/data` required。列表允许空、`maxItems` 为 1—200 之间的明确上限并在语义上保证完整活动集合最多 200;每项 required `noticeTitle/noticeContent/publishTime/readStatus`,标题 1—50、完整纯文本正文 1—1000、带时区 RFC3339、状态枚举 `READ/UNREAD`;计数为 0—200 int32 且与同一活动集合一致。当前输出 `NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED`。 + +- [ ] **步骤 4:后端关闭 `API-NOTIFICATION-READ-001`—`003`** + +`001` 提供专用响应、最小 required/type/length/enum;`002` 明确活动集合最多 200、完整返回、最新优先、完整纯文本正文和 count/list 同域;`003` 统一 SaToken、required clientid、JSON 媒体、HTTP 401 或业务 401 的文档与部署行为。后端必须从同一 Apifox 版本重新导出 JSON/YAML,不能由客户端手改受保护文件。 + +- [ ] **步骤 5:门禁通过后测试先行实现读取** + +先写 normalizer、内存快照和页面状态负例,证明 unsafe/重复服务端 ID、sender 与 biz 字段不进入页面,筛选不改变 key,旧 generation、重启、退出和账号切换不能别名。再实现严格可取消列表/计数请求:N01 摘要最多 160 字素并支持长词换行,N02 展示完整正文;M01/G01 文案改为“未读消息”,视觉角标为 `99+` 但读屏播报真实数。删除通用 G10/审核 CTA、目标按钮、查询参数假失败、fixture 未读数和所有本地伪已读,不接写端点。 + +- [ ] **步骤 6:真实账号与 MuMu 验收** + +任务 23 关闭认证后验证 0/1/99/100/200 条、最新优先、并发新消息、畸形正文/时间/状态、401、5xx、超时、取消、刷新和账号切换;MuMu 检查系统字号、TalkBack、键盘/焦点、长标题正文、错误重试、N01→N02、Android 返回和 M01/G01 角标。浏览器与源码审查不能替代。 + +### 任务 30:建立通知已读写入远端硬门禁 + +- [x] **步骤 1:三人独立核对单条与全部已读的身份和并发语义** + +线上两个 POST 都存在,但 `notificationId` 在列表和 path 中仍为 int64,JavaScript 无法保证无损;`RVoid` 没有 required,文档也未定义重复调用、超时结果未知、跨账号、不存在、read-all 截止点和并发新消息。读批次因此不能先留本地 mutation 充数,写批次也不能直接把页面 `snapshotKey` 当服务端 ID。 + +- [x] **步骤 2:反向质询并固定私有身份迁移** + +写入通过后,唯一 notification controller 才从同一列表响应私有保留 `notificationId`,页面、路由、日志和持久缓存仍只接触 `snapshotKey`。ID 固定为 1—128 位 URL-safe opaque string,列表字段与 path 使用同一个 pattern;禁止解析 int64 后 `String()`。单条和全部已读对当前账号幂等;不存在与跨账号单条 ID 统一为 404 `NOTIFICATION_NOT_AVAILABLE`,read-all 只覆盖服务端接收请求时已经存在的活动通知,之后新消息保持未读。 + +- [x] **步骤 3:先写独立失败合同** + +`tests/notification-read-state-openapi-contract.ps1` 要求两个 POST 的 SaToken、required clientid、无 request body、200 精确 `RVoid` 且整数 `code` required;`NotificationVo.notificationId` 与 path ID 同为 required 的 1—128 位 URL-safe string。当前输出 `NOTIFICATION-READ-STATE-OPENAPI-CONTRACT BLOCKED`,与读取红灯互不冒充。 + +- [ ] **步骤 4:后端关闭 `API-NOTIFICATION-STATE-001`—`003`** + +`001` 原子迁移无损 notification ID 与 required `RVoid`;`002` 固定幂等、重复/重试、read-all 截止点、并发/多端收敛;`003` 固定 401、404 `NOTIFICATION_NOT_AVAILABLE`、5xx、超时和 count/list 刷新规则。后端提供同版本双导出并以两个账号证明对象级隔离。 + +- [ ] **步骤 5:读取批次完成后单独实现写入** + +先写 controller 映射、重复点击、迟到响应、超时未知和跨 generation 负例;随后原子删除首版内部丢弃 ID 的实现及 N01/N02 所有 clone mutation。N02 打开后由 controller 标记、N01 提供全部已读;成功或结果未知都重新取得列表与计数收敛,不做本地 `count - 1`。离页取消只停止等待,不宣称服务端未执行;同一 snapshot 防重复提交。 + +- [ ] **步骤 6:真实账号与 MuMu 验收** + +验证单条重复调用、全部重复调用、超时后重读、并发新消息、多端读状态、跨账号 ID、被删除消息、401 和服务异常;MuMu 检查提交中禁用、播报、失败/未知回执、N01/N02/M01/G01 收敛与 Android 返回。没有有效账号和原生证据时保持发布红灯。 + +### 任务 31:建立 M02 个人资料写入远端硬门禁 + +- [x] **步骤 1:三人只读核对页面、本地双导出与线上 PUT** + +M02 目前把同一个 `currentUser.name` 同时写入 nickName/realName,email 固定空,保存只跑 500ms 定时器;没有真实加载、错误、认证失效、结果未知或并发冲突。受保护 PUT 使用旧 `ProfileUpdateBody`,缺 realName/email 且示例含 schema 外字段,成功为 generic RObject。线上改为 `AppProfileUpdateBody → RAppProfileVo`,却没有 required/minProperties/关闭额外字段/merge/version,200 媒体为 `*/*`,operation 无 security/clientid,只列 200/401。GET profile 门禁仍红,因此 baseline owner 也尚未可用;未调用真实 PUT。 + +- [x] **步骤 2:交叉反驳并选择唯一 merge PUT** + +三人否决“三字段全量 PUT”,因为它会覆盖其他端的未触碰字段,也无法代表 avatar/sex/birthday。PATCH 语义更正统,但在后端能够以 presence-aware DTO 和集成测试证明现有 PUT 为原子字段 merge 时,新增路由是无必要扩张;因此保留 PUT 且不发布 PATCH。省略字段保持,nickName 只允许脏的非空值,realName/email 以请求精确空串清空;null/空白非法,响应把清空字段省略。并发沿用 T01:body 返回唯一 opaque profileVersion,PUT 用 required If-Match,409 稳定码;不在 body 重复 version。 + +- [x] **步骤 3:先写失败合同锁定最小闭包** + +`tests/profile-update-openapi-contract.ps1` 要求唯一 PUT、SaToken、required clientid/If-Match、`application/json` 请求、关闭额外字段且 1—3 脏属性的 `AppProfileMergeUpdateBody`;昵称非空 1—30,实名/邮箱各以 oneOf 表达精确空串 clear 与非空规范值。200 精确 `RAppProfileVo` 并含 required profileVersion,409 精确 `RProfileVersionChanged/PROFILE_VERSION_CHANGED`,同时列 400/401/422/429/500。当前输出 `PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED`,证明失败来自合同漂移而非测试语法。 + +- [ ] **步骤 4:后端关闭 `API-PROFILE-UPDATE-001`—`004`** + +`001` 固定唯一 merge owner、dirty/omit/clear 和关闭额外字段;`002` 固定 profileVersion、If-Match、当前账号/租户原子 CAS 与 409;`003` 固定 typed canonical response、结构化字段错误、SaToken/clientid、JSON、`Cache-Control: private, no-store` 和正式 H5 CORS 允许 If-Match;`004` 固定重复设置无额外副作用、超时对账与账号隔离语义。后端从同一版本重导 JSON/YAML,不手改保护文件。 + +- [ ] **步骤 5:GET/PUT 双门禁通过后测试先行实现 M02** + +先建立 profile update normalizer、API 和状态机纯运行时负例:clean 不请求,dirty allowlist,clear 命令,非法 nickname/email/null/额外字段,严格 envelope,双击、迟到响应、session generation、canonical 回填、409、确定失败和三分支 outcome reconciliation。随后迁移页面到 loading、ready/dirty、saving、success、error、uncertain、conflict、auth-expired;保存中锁输入与返回,成功更新 baseline,失败保留草稿。原子删除 fixture 双姓名、假 timer、API 禁用旧断言、假头像动作和邮箱送达承诺,不接头像/性别/生日。 + +- [ ] **步骤 6:真实账号、CORS 与 MuMu 验收** + +用两个账号和两个会话验证单字段、多字段、清空、重复、旧 version、同字段冲突、不同字段更新、超时后 GET 对账、401/422/429/500、账号切换和迟到响应;正式 H5 origin 验证 If-Match 预检。MuMu 检查 loading、软键盘、系统字号、TalkBack、字段错误关联/聚焦、saving 返回、unknown/conflict 对话和长姓名邮箱。浏览器与静态合同不能替代。 + +### 任务 32:建立 M10 当前设备退出远端硬门禁 + +- [x] **步骤 1:三人只读核对本机退出、双导出和线上 DELETE** + +M10 现有 `session.clear → close → goRoot(A01)` 保证本机 token 与家谱上下文被清,取消无副作用,但没有远端撤销、重复保护、epoch 或状态提示。受保护双导出有 DELETE、required clientid/SaToken 和 200 RVoid,却无 scope/幂等/required/错误;线上只有 200 RVoid 与 401 string、媒体 `*/*`,且 operation 无 security/clientid。两份都不能证明当前设备还是全账号退出、旧 token 是否真正失效;未发送真实 DELETE。 + +- [x] **步骤 2:反向质询并固定本机必退、远端分级** + +三人同时否决“网络失败就不退出”和“await 后 finally 清会话并宣称远端成功”。前者把本机隐私交给网络,后者会延迟退出且 A 的迟到 finally 可能清掉新登录 B。唯一结论是 logoutCoordinator 同步捕获 A token/clientid/epoch,立即经 session owner 清理并 bump epoch,再用显式 A 创建不随 M10 卸载取消的请求并立即进入 A01;异步结果永不再 clear。远端 200 才 confirmed,网络/超时/畸形/5xx 为 unconfirmed,拒绝为 not-revoked,全部不恢复 token。 + +- [x] **步骤 3:先写失败合同锁定唯一服务端语义** + +`tests/logout-openapi-contract.ps1` 要求 DELETE 无 body、required SaToken/clientid、200 application/json RVoid 且 integer code required、401 typed `RLogoutRejected`,两者 `private, no-store`,并列 400/429/500。operation 明确只撤销当前 credential family、其他设备不受影响、active/revoked/expired 重复调用同一 200、成功后旧 token 被拒绝。当前输出 `LOGOUT-OPENAPI-CONTRACT BLOCKED`。 + +- [ ] **步骤 4:后端关闭 `API-LOGOUT-001`—`003`** + +`001` 固定当前凭证族、refresh 同步撤销、其他设备隔离和跨鉴权节点传播;`002` 固定 active/revoked/expired 同一幂等 200,非法/client mismatch typed 401;`003` 固定 SaToken/clientid 绑定、JSON、no-store、错误矩阵、正式 H5 CORS 和 bearer 日志脱敏。后端同版本重导 JSON/YAML并用两设备 token A/B 证明 A 失效、重复 A 200、B 仍可用。 + +- [ ] **步骤 5:门禁通过后测试先行实现 coordinator** + +先扩展 session owner 的单调 epoch 与所有已登记账号态清理,再写 logoutCoordinator 纯运行时测试:同步 capture/clear/request/root 顺序、无 token、request 创建抛错、双击、pending/confirmed/unconfirmed/not-revoked、M10 卸载不 abort、A 迟到不清 B、一次性消费、不持久 token。实现严格 logout API 和 A01 非阻断可播报提示;原子替换 M10、导航、NM 旧“页面直接 clear”断言。Mock/错误配置也本机退出且远端未确认。 + +- [ ] **步骤 6:真实双设备与 MuMu 验收** + +验证 active/revoked/expired/invalid/client mismatch、重复调用、跨节点传播、refresh、离线/超时/429/500、快速 B 登录、进程终止与根导航失败;MuMu 检查确认/取消、双击、系统返回、pending 与最终播报、A01 登录可用及不返回受保护页。视觉源码审查或浏览器不能替代原生流程。 + +### 任务 33:建立 M04 与统一密码凭证远端硬门禁 + +- [x] **步骤 1:三人只读核对 M03/M04、四条密码 wire 与线上 PUT** + +M04 当前只有 500ms 本地校验预览,诚实显示“不提交服务器”;没有 API、MD5 或 session 写入。受保护双导出与线上文档的登录、注册、找回、改密都把 32 个十六进制字符 MD5 当静态密码等价物。线上 M04 只有 200 RVoid/401 string、`*/*`,operation 无 security/clientid;本地虽有 SaToken/clientid,却只列 200、RVoid 无 required,双方都缺策略、错误、会话撤销、缓存和并发语义。未发送真实 PUT、未操作 MuMu。 + +- [x] **步骤 2:交叉反驳并固定唯一安全方案** + +三人否决保留 MD5 fallback、返回新 token 的方案 B 和继续保留旧 bearer 的方案 C。密码 wire 必须由登录、注册、找回、改密同批迁移到 HTTPS raw writeOnly;新密码目标为 NFC 后 15—64 Unicode code point、允许空格和 Unicode、无组成规则,由服务端执行 blocklist、限速和带盐慢哈希。M04 以当前密码重新认证,TAC 不能代替身份。唯一会话方案 A 是 200 前提升 credentialEpoch 并撤销所有设备/所有 client 的旧 access/refresh session,客户端清本机回 A01。 + +三人同时否决为了本批新增 operation-status 和持久重试 body:strict 200 与网络结果未知都会进入 A01,重新登录已是最小对账路径。但 PUT 发出后进程被杀会留下本机旧 token,因此采用最窄 marker:session owner 在 dispatch 前仅持久化 `{sessionEpoch,startedAt}`;不含 token、密码、摘要或 body,冷启动同 epoch 时在渲染账号缓存前清会话。客户端不自动重试,迟到 A 不能影响 B。 + +- [x] **步骤 3:测试先行建立共享 wire 与 M04 失败合同** + +新增 `tests/password-change-openapi-contract.ps1`。合同要求 `CurrentPasswordSecret/NewPasswordSecret` 分别成为登录/当前值与注册/找回/新值的唯一 schema owner,旧 MD5 不能继续被任何入口接受;M04 body 只含 required oldPassword/newPassword 且关闭额外字段。PUT required SaToken/clientid;200/400/401/409/422/429/500 使用 JSON/no-store,429 有 Retry-After,409/422 只消费四个稳定业务码,200 RVoid 的 integer code required。当前得到预期 `PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED`,失败精确来自受保护后端合同缺口。 + +- [ ] **步骤 4:后端关闭 `API-PASSWORD-001`—`005`** + +`001` 原子删除 login/register/reset/change 的 MD5 wire,发布 raw writeOnly 与 15—64/NFC/blocklist/慢哈希合同;`002` 固定 credentialEpoch、ALL access/refresh 跨节点撤销和 200 语义;`003` 固定 typed 400/401/409/422/429/500、并发 CAS 与明确未写边界;`004` 固定 SaToken/clientid、HTTPS server、JSON/no-store、Retry-After、日志/APM 脱敏;`005` 明确无密码账号是进入独立设置流程还是不可进入 M04。后端只接受同一版本重新导出的 JSON/YAML,禁止手改受保护文件。 + +- [ ] **步骤 5:门禁通过后原子实施四条 wire、session 与 M04** + +先用共享密码策略纯函数测试 14/15/64/65 code point、Unicode/NFC、空格、粘贴、无组成规则与 current/new 相同;再一次性移除 `calcMD5` 的四条认证消费者和 8—32 旧规则,收紧 API payload/响应。扩展 session epoch 与 credentialChangeInFlight 冷启动门禁,写 M04 ready/submitting/known-error/unknown/success 状态机;确定未写错误留页,200/401/409/unknown 清秘密和同 epoch 账号态回 A01。原子替换 NM 旧“禁止 API/尚未提交”断言,不能保留 preview/remote 双入口。 + +- [ ] **步骤 6:两设备、故障注入、无障碍与 MuMu 验收** + +验证旧/新密码、同旧密码并发只有一笔成功、所有设备 access/refresh 跨节点失效、wrong-old/policy/rate 明确未写、请求创建失败、落密前后/撤销前后断网与 5xx、进程被杀 marker、快速 B 登录和秘密不进入日志。MuMu 固定环境检查 15—64 文案、密码管理器/粘贴、软键盘、系统字号、44dp 显隐按钮、aria-pressed、字段错误关联/首错聚焦、提交播报、Android 返回及 A01 成功/unknown 持久提示;静态审查不能替代。 + +### 任务 34:建立 M05 手机号换绑与统一 OTP 远端硬门禁 + +- [x] **步骤 1:三人只读核对 M03/M05、全活动短信码、本地双导出与线上接口** + +M05 现有页面只消费脱敏 fixture,以 500ms 定时器校验新手机号和 4 位码,明确显示“当前未发送/尚未提交服务器”;没有 currentPassword、TAC consumer、API、session 或结果未知状态。本地 PUT body 为 `clientId/phone/smsCode`,线上为 `phone/smsCode`,两者均为 4 位码且缺 existing-factor、唯一约束、会话撤销和 typed 错误;线上返回完整 profile并把共享发码标为忽略权限。未发送 POST/PUT 或短信,未操作 MuMu。 + +- [x] **步骤 2:交叉反驳并固定身份、发码和会话唯一方案** + +三人否决 bearer+新号 OTP:被盗 session 加攻击者自有号码即可接管。唯一身份闭环是活动 session+最终 PUT 内 raw currentPassword 再认证+新号 `APP_PHONE_CHANGE` TAC/OTP;不强制旧号 OTP,成功事务改为持久旧号通知 outbox。无密码账号 `STEP_UP_UNAVAILABLE`,不得降级。最终 PUT 不带 challengeId:同一复合键只有一个 active OTP generation,重发废止旧码即可消歧。 + +三人先讨论复用公共 `/auth/sms/code` 加条件鉴权,随后否决:标准 OpenAPI 无法把 body scene 与 operation security 联动,optional security、文字或 vendor extension 仍让通用 validator 允许匿名,违反 validator/runtime 同步。最终采用专用 `POST /genealogy/app/auth/phone/sms/code`,SaToken required、scene 服务端固定、body 只含 phone/validToken;公共 scene enum 删除 APP_PHONE_CHANGE。底层 OTP 服务仍是单一 owner,不复制实现。 + +- [x] **步骤 3:原子固定全活动六位短信码目标** + +四位码不可上线。新增唯一 `SmsCodeSecret`:CSPRNG 生成恰好 6 位 ASCII 数字、允许前导零、writeOnly、无示例;5 分钟 TTL、60 秒重发、最多 5 次失败、单次消费,重发废止旧 generation且不重置累计失败次数。登录、注册、找回、换绑、注销及同源服务端生成器/模板、双 OpenAPI、validator、页面和测试必须同版切换,禁止 4/6 兼容窗口。当前客户端继续保持四位诚实预览,不能在后端前单边改成无法登录的六位。 + +- [x] **步骤 4:测试先行建立专用发码和最终换绑失败合同** + +新增 `tests/phone-change-openapi-contract.ps1` 并同步收紧 `tests/auth-tac-openapi-contract.ps1`。门禁要求专用 POST 与最终 PUT 都 required SaToken/clientid;`PhoneChangeSmsCodeBody` 只含 phone/validToken,`PhoneChangeBody` 只含 currentPassword/phone/smsCode;密码、新号和短信码分别引用唯一 schema。公共发码移除 APP_PHONE_CHANGE;POST/PUT 均有 JSON/no-store 错误矩阵、429 Retry-After,严格 200 RVoid,409/422/429 使用稳定 `RPhoneChangeRejected`。当前精确输出 `PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED`,且认证门禁继续标准化输出自身红灯。 + +- [ ] **步骤 5:后端关闭 `API-PHONE-001`—`005`** + +`001` 提供专用鉴权发码、固定 scene、公开/受保护反例与共享 OTP owner;`002` 原子升级全活动消费者为六位并固定 TTL/重发/尝试/单次消费;`003` 提供 raw currentPassword existing-factor、闭合 PUT 与无密码账号语义;`004` 把密码/OTP、号码唯一、CAS 换绑、credentialEpoch、ALL session 撤销和旧号 outbox 放进同一事务;`005` 固定 typed 业务码、HTTPS、JSON/no-store、Retry-After、CORS、PII/秘密日志脱敏与结果未知边界。只接受同一版本重导的双文件,不手改受保护源。 + +- [ ] **步骤 6:前置门禁通过后原子实施客户端与 MuMu/两设备验收** + +先同步迁移共享六位 validator、认证页面与测试,再新增带 bearer 的 phone-change TAC/发码 API、最终 PUT strict adapter和共享 credential marker,最后实现 M05 ready/tac/sending/code-sent/submitting/known-error/unknown 状态机;同轮删除四位、preview 和禁止 API 的旧断言,不保留双入口。验证匿名专用发码、错 scene/TAC 重放、前导零、过期/重发/限流、号码枚举与唯一、两设备并发、全部 access/refresh 撤销、outbox、请求各事务点 fault injection 和进程终止。MuMu 检查软键盘、自动填充/粘贴、系统字号、TalkBack 非拖动 TAC、原生 44dp 按钮、错误关联/首错聚焦、倒计时、Android 返回及跨根 unknown 提示。 + +### 任务 35:建立 G03 家谱与始祖原子创建远端硬门禁 + +- [x] **步骤 1:三人只读核对 G03、上下文、现有测试、本地双导出与线上接口** + +G03 当前同页两步只写内存预览,第一步缺可信 regionCode,始祖默认男性、日期限制 1800 年以后且把“一世”放进 generationName;最终 `local-created-*` 进入 fixture G05,不安装真实 context。受保护双导出和线上 `/genealogies` 都只建谱,通用 `/lineage/persons` 另建人物;创建响应、地区项和线上 App 模型均缺 required/词法 ID/错误闭包,当前 `appApi.createGenealogy` 又会在 mock 中直接篡改 fixture。审查只读获取线上 `/v3/api-docs`,未发 POST/PUT、未触碰 MuMu、Git 或受保护 OpenAPI。 + +- [x] **步骤 2:交叉反驳并固定 atomic bootstrap、访问预设和地区唯一方案** + +三人先完整设计 `ROOT_REQUIRED` 两写,再质询其必要性。当前没有跨库证据或“保存空谱以后补根”的产品需求;第一步本来也应零写,因此两写只会新增半成品 quota/ACL/搜索隐藏/过期/取消、G01 pending 卡、版本并发和第二次结果未知。最终选定现有 POST 原子创建家谱、OWNER、唯一一世始祖、READY 和幂等回执,任一失败全回滚;通用人物 POST 只服务 READY 后普通人物。 + +`GenealogyAccessPreset=MEMBER_ONLY/PUBLIC_APPLY` 是 APP 读、建、改唯一 wire,而非 G03 私有第三字段;目标版本删除 visibility/joinMode 和旧 create/update DTO。所在地只消费 required SaToken/clientid 的 `GET /genealogy/app/region/search` 所返回的 required `regionCode/label/selectable`,同版删除旧公共 `/genealogy/region/search`;不从自由文本反推 code,也不把 leaf 猜成可选或强制某行政层级。根人物 sex 使用 MALE/FEMALE/UNKNOWN且默认 UNKNOWN,生日为 date,无 1800 下限;generation=1 由服务端固定。G11 在本任务中只迁移共享字段形状,其 If-Match、版本/CAS 与结果未知另立设置写入门禁,不能借 G03 宣称可上线。 + +- [x] **步骤 3:选择无 PII 崩溃恢复并测试先行建立失败合同** + +三人比较“本地持久化 exact body 同 key 重放”和“authenticated operation-status”。前者端点更少,却会把始祖姓名、生日、生平放进普通 UniApp storage,扩大退出、换号、备份和崩溃清理面;最终选择后者。客户端只持久 `{sessionEpoch,operationKey,startedAt}`;进程内可用相同 key+冻结 snapshot 重放,冷启动查询 `PENDING/SUCCEEDED/FAILED_NO_COMMIT`,不从 `/mine` 猜同名记录。key 固定为 13 位 issuedAt+22—43 位 base64url CSPRNG,`acceptUntil=issuedAt+10 分钟`,以 server time 判定且未来超过 5 分钟为 400 `OPERATION_KEY_INVALID`;上述公式以 600/300 秒 extension 机器锁定。首次 POST 先以短控制事务唯一认领 PENDING/digest/fencing/resolveBy,`resolveBy<=claimedAt+2 分钟`并以 120 秒 extension 锁定,再由业务事务原子写 quota、谱、OWNER、唯一根、READY 和 SUCCEEDED。业务回滚后或 watchdog 超时只能用 fencing CAS 终结 FAILED_NO_COMMIT,旧 worker 不得提交;GET 只允许 operationKey/clientid、无 body且始终纯读,不能靠查询制造墓碑。 + +新增后端双导出门禁 `tests/g03-bootstrap-openapi-contract.ps1`,当前精确输出 `G03-BOOTSTRAP-OPENAPI-CONTRACT BLOCKED`;其前置 `tests/openapi-yaml-json-parity-runtime-smoke.js` 不依赖第三方包,以无损任意精度数字、合法 mapping 分隔符和完整对象结构深比较 JSON/YAML,JSON 检查因此等价覆盖 YAML 而不依赖缩进、引号或 JavaScript Number 舍入。另建客户端激活门禁 `tests/g03-bootstrap-client-release-gate.ps1`,当前精确输出 `G03-BOOTSTRAP-CLIENT-RELEASE BLOCKED`;它只保留公共 owner/旧入口删除的静态断言,并实际执行依赖注入状态机测试,以 exact marker 和 request/storage/cache/context/navigation spies 验证分支及成功顺序,不再靠 helper 的 IndexOf 或测试标题误判。已有 fixture/G11 等聚焦守卫由全量测试共同证明,不冒充为该脚本单独覆盖。后端合同转绿不能自动开放页面,客户端绿灯也不能绕过后端;G03 两门禁、家谱工作区读取门禁和 MuMu 发布验收必须共同通过。同轮把 `tests/g-series-openapi-contract.ps1` 中会永久锁死旧 `GenealogyCreateBody/GenealogyUpdateBody/visibility/joinMode` 的断言移交给新唯一 owner,既有 G-series 合同仍独立通过。 + +- [ ] **步骤 4:后端关闭 `API-G03-001`—`005`** + +`001` 以 `AppGenealogyBootstrapBody` 原子替换旧 create,在业务事务内处理 quota、家谱、OWNER、根、READY 和 SUCCEEDED 回执;`002` 提供 issuedAt+至少 128 位随机量的 Idempotency-Key、server-time 10 分钟接受窗口、5 分钟未来偏差、同 key/body 同回执、不同 digest 409、过期空 key 409 与长期成功防重记录;`003` 以短控制事务/fencing/watchdog 发布受鉴权 operation-status,GET 参数/body/响应集闭合且纯读,PENDING 为 `resolveBy/retryAfterSeconds` 并最迟 claimedAt 后 2 分钟收敛,结构化转换只允许 `ABSENT→PENDING→SUCCEEDED/FAILED_NO_COMMIT` 且两个终态不可变,FAILED 机器保证零领域写与零 quota,截止前 404 带 acceptUntil/Retry-After,截止后无记录按 key 计算 FAILED 而不写墓碑;`004` 让 APP 读/建/改统一 accessPreset并递归拒绝组合 schema 中的旧 pair/DTO;`005` 将地区 owner 固定为 `/genealogy/app/region/search` 并删除公共旧路由,固定词法 ID、SaToken/clientid、状态专属 error code/businessCode、private no-store、对象隔离和事务 fault injection。通用人物 collection POST、人物 PUT/DELETE 和 parents mutation 同版以数据库 bootstrap-root 标记保护根身份:根 PUT 可编辑白名单精确只有 `name/sex/birthDate/biography`,其余字段全部 typed 422,根不可删,typed 409/422覆盖全部绕过入口。只接受同一版本重导 JSON/YAML,禁止手改当前源文件。 + +- [ ] **步骤 5:门禁通过后测试先行实现唯一 coordinator 与页面闭环** + +先写 canonicalizer、严格响应 normalizer、operation marker/status 状态机和 context 安装的纯 Node 测试,再实现唯一 G03 coordinator。第一步 CTA 改“下一步:录入首代”且零请求;最终按钮冻结全表单后才写 marker/POST。本地校验或可证明零发出的 request-build 失败不留 marker;服务端在 claim 前返回的 400/401/403 清 marker,401 同时清会话。IDEMPOTENCY_KEY_REUSED 进入 fatal/quarantined,禁止查装 status或自动换 key,只有用户看到警告后显式放弃才清;确定未提交的 limit/expired/422 可清。429 保持同 key/body并先查 status;500/network/timeout/408/发出后取消/意外 2xx/3xx/畸形成功均按 unknown 保持。status 截止前 404 保持,400 清损坏 marker,401 走会话失效,429/500/network/cancel/unexpected/malformed 保持退避,FAILED_NO_COMMIT 才允许新 key。SUCCEEDED 唯一次序是 committed receipt → 失效或定点更新 `/mine` → 安装词法 context → 进入远端 G05;context/导航失败只重试本地闭环,不重发创建。原子删除 `createLocalGenealogyPreview`、会修改 fixture 的 mock create、禁止 appApi 的旧断言及 `local-created-*` 成功路径,不保留 preview/remote 双 owner;G01 onShow 和 G05 overview 必须与工作区门禁同一真实 ID 收敛。 + +- [ ] **步骤 6:表单无障碍、事务反例和 MuMu 发布验收** + +地区选择覆盖加载/搜索/无结果/失败/迟到/失效;输入具有 label、aria-invalid/describedby、首错聚焦,访问预设使用 radio,所有动作至少 44dp,弹层使用 AppDialog 焦点圈定/恢复,提交中冻结输入与返回并播报 PENDING/unknown/fatal/committed。后端在 control claim、quota、谱、OWNER、根、receipt、终态 CAS 每个写点做 fault injection,验证零残留、同 key 并发唯一、不同 body 409、跨账号/tenant/client status 非泄漏 404、截止前 404 不换 key、截止后 GET 纯计算 FAILED 且数据库零写、迟到 POST 永久拒绝、watchdog 后旧 worker 因 fencing 无法提交;另覆盖 collection/PUT/DELETE/parents 的根身份绕过,并证明根 PUT 仅 `name/sex/birthDate/biography` 四项可编辑、其余字段一律 422。MuMu 固定覆盖 320/360/412 宽度、系统字号、TalkBack、软键盘、日期、双击、慢网/断网/杀进程、Android 返回、context/导航失败;浏览器或源码检查不能代替原生结论。 + ## 后续独立阶段边界 -T01 完成后才依次为以下阶段重新执行三人只读审查、五方案比较、中文设计和中文实施计划;本计划不提前写其业务代码: +T01、认证、家谱工作区、M06、个人资料读写、通知、服务端退出和密码凭证当前都被外部合同红灯阻塞,不在本地空等。其余阶段仍逐个重新执行三人只读审查、方案比较、失败测试和最小实施;不得把两个业务域混入同一批: -1. 短信验证码完整状态机:发送资格、发送中、倒计时、失败、限流、前后台恢复、到期和重复发送。 -2. 跨页面领域数据持久化:会话、当前家谱、列表现场、写后失效、账号切换和权限变化;不把导航一次性结果升级成领域仓库。 -3. 全局文字层级与无障碍第二轮:字号层级、系统字号放大、焦点顺序、读屏语义、对比度和 44dp 触控目标。 +1. 家谱工作区:任务 26 已完成三人审查和 OpenAPI 红灯;线上 JSON `int64` 响应身份、模型 required、可访问能力与错误语义未关闭前禁止猜测实现。门禁通过后才处理远端列表现场、权限刷新和账号隔离;写后失效属于后续写接口批次。 +2. M06:任务 27 已完成 list-only 三人审查和 OpenAPI 红灯;门禁通过后才写严格 adapter、异步状态和无障碍手风琴,不接详情 ID。 +3. 个人资料读取:任务 28 已完成 M01/M02/M03 三人审查和 OpenAPI 红灯;门禁通过后才接掩码 adapter,PUT 保存、头像和换绑仍各自独立。 +4. 通知:任务 29/30 已把读取和已读写入拆开建立红灯;读取通过后先实现无 ID 内存快照,写入再私有迁移字符串 ID,未知业务目标始终内容态。 +5. M02 写入:任务 31 已建立唯一 merge PUT 红灯;GET/PUT 双门禁通过后才实现 dirty command、版本 CAS、结果未知对账与账号隔离,头像保持独立。 +6. M10 退出:任务 32 已建立当前凭证族与幂等撤销红灯;通过后才实现 session epoch、logoutCoordinator 和 A01 提示,本机清理始终优先。 +7. M04 密码凭证:任务 33 已建立 raw wire、15—64 策略、ALL 会话撤销与崩溃 marker 红灯;必须与登录/注册/找回同批迁移,M04 不单页双轨。 +8. M05 手机号换绑:任务 34 已建立专用受保护发码、全活动六位 OTP、currentPassword、ALL 会话撤销与旧号 outbox 红灯;必须等认证/M04/profile 前置合同共同通过后原子实施。 +9. G03 原子创建:任务 35 已建立 atomic bootstrap、无 PII operation-status、统一 accessPreset、可信地区和词法 ID 红灯;必须等本门禁与工作区读取门禁通过后再实现 context/G05/G01 完整回流。 +10. G、F、R、N/M 其余真实接口:逐页核对页面、业务闭环与线上/双导出三方,缺失合同先形成后端问题单;当前从 G/F/R 选择不依赖既有红灯的下一域。 +11. 全局文字层级与无障碍第二轮:字号层级、系统字号放大、焦点顺序、读屏语义、对比度和 44dp 触控目标。 +12. 发布收口:真实环境配置、隐私与权限声明、错误观测、构建签名、升级/回滚、MuMu 全流程回归和三人终审。 不得在同一实现批次混合以上阶段,也不得借后续阶段返工已经通过且无回归证据的响应式页面。 ## 当前计划完成条件 - 阶段 0 的五份权威中文文档仍是唯一长期入口,没有新建旧版、备份或平行计划。 -- 导航与 T01 的所有者、旧路径删除条件、测试、MuMu 矩阵和后端接口缺口均已精确写明。 -- 当前只完成设计与实施计划,没有提前修改业务代码或手工改写 OpenAPI 导出。 +- 导航、T01、认证/TAC、家谱工作区、G03 原子创建、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 密码凭证与 M05 手机号换绑的所有者、旧路径删除条件、测试、MuMu 矩阵和后端接口缺口均已精确写明。 +- 导航任务 1—10 已按测试先行完成两个共享所有者、共享组件、退役入口、认证、G、T、F、R、N/M 系列静态迁移与零债务门禁;没有手工改写 OpenAPI 导出,MuMu 原生矩阵仍按明确边界待执行。 - 三人对业务正确性、全局一致性、回归风险、维护成本和 MuMu 可验证性形成一致结论。 -- 下一次实施从任务 1 的失败测试开始,不从页面机械替换开始。 +- T01、认证、家谱工作区、G03 原子创建、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 密码凭证与 M05 手机号换绑 OpenAPI 门禁已经建立并取得预期红灯;TAC 客户端任务 20—22 已完成,任务 23 等待后端合同与 MuMu 原生证据。领域上下文任务 24、M07 客户端任务 25、工作区任务 26、M06 任务 27、profile 读取任务 28、通知任务 29/30、profile 写入任务 31、logout 任务 32、password 任务 33、phone 任务 34 与 G03 任务 35 已完成各自可本地完成的部分;等待后端期间转向 G/F/R 下一域,不把两个阶段混在同一变更中。 diff --git a/docs/家谱项目全量治理设计.md b/docs/家谱项目全量治理设计.md index 44a319d..8d543fb 100644 --- a/docs/家谱项目全量治理设计.md +++ b/docs/家谱项目全量治理设计.md @@ -1,7 +1,7 @@ # 家谱项目全量治理设计 > 日期:2026-07-22 -> 状态:阶段 0 已完成;导航栈语义与 T01 大规模世系树设计已经三人终审,尚未实施业务代码 +> 状态:阶段 0 已完成;导航任务 1—10 的静态实施和零债务门禁已经完成;A01/A04/A05 TAC 客户端与 M07 反馈客户端已经完成;MuMu 原生矩阵待执行;T01、认证、家谱工作区、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 密码凭证与 M05 手机号换绑后端合同均处于硬门禁红灯 > 适用范围:当前 UniApp 家谱项目、根目录 OpenAPI 文档、全部活动页面、共享组件、测试、正式资产与后续真实接口接入 ## 一、背景 @@ -509,7 +509,7 @@ MuMu 验证中 ### 20.1 当前证据 -只读扫描得到以下统一口径: +任务 3 实施前的只读扫描得到以下统一口径: - 52 个活动页面中共有 `navigateTo 59`、`navigateBack 11`、`redirectTo 14`、`reLaunch 7`,合计 91 次直接调用。 - 活动共享组件另有 `navigateBack 2`、`reLaunch 2`,活动页面与组件共 95 次。 @@ -517,6 +517,8 @@ MuMu 验证中 - MuMu 已复现 F01→F03→“返回家族圈”后留下两个 F01,A01→A04→“登录”后留下两个 A01,T03 可连续叠出三个同路由页面。 - 栈深为 1 时直接进入 F03,当前 `PageHeader` 会回到固定 G01,而不是业务父页 F01。 +任务 3 已把共享页头和自定义底栏的直接调用清零,并删除无活动消费者的通用页面旧入口;任务 4—8 依次迁移认证、G、T、F、R,任务 9 完成 N/M、安全通知目标、账号表单返回守卫与退出会话清理,任务 10 删除最后一个零生产消费者的 `TreeMemberForm.vue` 及其旧专属合同。当前 SFC 分段词法扫描的迁移债务已经清零:pages/components 中 `navigateTo`、`navigateBack`、`redirectTo`、`reLaunch`、`getCurrentPages` 与业务页面路径字面量均为零,声明式 navigator、动态 Uni 属性、Uni 对象逃逸与 `switchTab` 也为零;验证结果为 `MIGRATION-DEBT=0`。路由与页面栈合同分别只由 `utils/navigation-routes.js` 和 `utils/navigation.js` 持有,后续业务批次不得恢复页面私有路径或栈判断。 + 因此问题不是某几个按钮写错,而是项目没有统一表达“打开页面、替换步骤、返回来源、完成流程、切换根页和直接进入回退”的语义合同。 ### 20.2 八类问题的五方案终选 @@ -537,7 +539,7 @@ MuMu 验证中 ### 20.3 唯一所有者 - `utils/navigation-routes.js` 是 52 个活动路由的唯一语义注册表,拥有路由键、路径、页面类型、规范父页、父页参数映射、根页、必填参数、可选参数、允许来源和目标页允许消费的结果操作枚举。 -- `utils/navigation.js` 是项目唯一允许调用 `uni.navigateTo`、`uni.redirectTo`、`uni.reLaunch`、`uni.switchTab` 和 `uni.navigateBack` 的业务模块。 +- `utils/navigation.js` 是项目唯一允许调用 `uni.navigateTo`、`uni.redirectTo`、`uni.reLaunch` 和 `uni.navigateBack` 的业务模块;项目没有原生 tabBar,`uni.switchTab` 在该模块内外都禁止。 - 页面和组件只调用语义方法,不拼接页面路径,不保存 fallback URL,不接收后端原始跳转 URL。 - 注册表中的路径集合必须与 `pages.json` 的 52 个活动路由精确相等;缺失、重复和陈旧条目均使合同失败。 @@ -549,27 +551,27 @@ MuMu 验证中 openPage(routeKey, params, sourceKey) 打开普通子页;sourceKey 必须等于当前真实页面。当前已经是同一路由且关键参数相同则不重复入栈。 -replaceStep(routeKey, params, sourceKey) - 只替换同一流程中的临时步骤;sourceKey 同样必须来自当前真实页面,禁止用于“返回列表”。 - goBack() 栈内有上一页时 navigateBack;没有时按当前路由的规范父页逐级回退。 -returnTo(routeKey, targetParams = {}, result = null) - 按路由键寻找最近实例并精确返回;targetParams 只用于目标不在栈内时构造合法回退 URL。 +returnTo(routeKey, targetParams = {}) + 按路由键寻找最近实例并精确返回;显式 targetParams 必须与最近实例一致,目标不在栈内时才用于构造合法回退 URL;普通返回不产生流程结果。 finishPage(routeKey, targetParams, result) - 校验目标参数和类型化结果后调用 returnTo;结果只能包含 operation、entityId 和 refresh。 + 是完成并回传结果的唯一公开入口;调用方必须显式提供目标全部必填参数,当前来源页与目标页共同声明且实际存在的上下文字段必须相等;校验参数和类型化结果后精确返回,结果只能包含 operation、entityId 和 refresh。 goRoot(routeKey, params) 只接受 A01、G01、F01、M01 四个根语义;使用 reLaunch 清理旧流程。 + +handleBackPress(event, requestBack) + 同步适配 UniApp 的 onBackPress:网关自己的 navigateBack 回调来源返回 false 放行,其余来源同步返回 true,并异步执行页面唯一 requestBack,避免递归拦截。 ``` -六个公开语义方法中,除纯判断外的导航动作都返回 `Promise`,并共享一个在途转场锁:同一目标的重复调用复用同一个 Promise,其他并发转场明确返回忙碌结果。导航失败或被锁拒绝时必须回滚刚写入的一次性结果。 +五个公开导航语义方法都返回 `Promise`,并共享一个在途转场锁:相同参数和相同结果语义的重复调用复用同一个 Promise,其他并发转场明确返回忙碌结果。一次性结果写入必须与“取得新锁”原子发生;复用或忙碌调用不得改写结果,导航失败必须回滚。每次转场用独立 flight 身份释放锁,`success/fail` 必须在 Promise settle 前释放自己的 flight;迟到的 `complete` 只能清理原 flight,不能清掉已经开始的新转场。所有调用方导航参数、流程结果和返回守卫上下文只接受普通对象的 own enumerable data properties;Symbol、访问器、不可枚举字段和原型继承字段一律拒绝,校验后只使用同一次读取形成的冻结快照,禁止重复 getter 读取或校验后别名篡改。`redirectTo` 只允许由网关内部的规范父链回退和目标缺栈返回使用;当前没有可公开授权的“替换步骤”边,因此不暴露无消费者的替换方法。 -一次性结果只存在于当前 JavaScript 进程,目标页消费一次后立即删除。字段键精确为 `operation、entityId(可选)、refresh`;`operation` 必须属于目标路由注册的 `resultOperations`,`entityId` 若存在必须为非空字符串,`refresh` 必须是布尔值。它不是领域数据持久化,不允许加入 `treeVersion、genealogyId、focusId、payload`,也不允许保存完整对象、表单内容、列表快照或接口响应。 +一次性结果只存在于当前 JavaScript 进程。网关用模块内 `WeakMap` 为页面实例分配原始值身份令牌,结果 envelope 只保存令牌,不强引用或保存页面/Vue 实例。栈内返回时结果绑定反向搜索选中的最近目标令牌和该实例的规范业务参数;缺栈重建时绑定发起页令牌与完整目标参数。只有当前真实栈顶正是目标实例(或缺栈重建出的非发起页)且业务参数完全一致时才能消费,错页、同路由的更远实例和其他家谱上下文只能得到 `null`,也不得删除正确结果。结果必须在原生目标页 `onShow` 发生前随取得转场锁原子写入,消费一次后立即删除;新完成流程取得锁时淘汰已经错过目标生命周期的未消费旧结果,失败时不复活陈旧结果。字段键精确为 `operation、entityId(可选)、refresh`;`operation` 必须属于目标路由注册的 `resultOperations`,`entityId` 若存在必须为非空字符串,`refresh` 必须是布尔值。它不是领域数据持久化,不允许加入 `treeVersion、genealogyId、focusId、payload`,也不允许保存完整对象、表单内容、列表快照或接口响应。 -`sourceKey` 只证明当前 JavaScript 进程内的一次真实导航:`openPage/replaceStep` 必须核对它与 `getCurrentPages()` 的当前真实路由一致。栈深为 1 或外部直接进入时,查询串里的 `sourceKey` 一律视为不可信并忽略,只按注册表的 `parent/parentParamMap` 建立回退目标;外部链接不能伪造来源合同。 +`sourceKey` 只证明当前 JavaScript 进程内的一次真实导航:`openPage` 必须核对它与 `getCurrentPages()` 的当前真实路由一致。栈深为 1 或外部直接进入时,查询串里的 `sourceKey` 一律视为不可信并忽略,只按注册表的 `parent/parentParamMap` 建立回退目标;外部链接不能伪造来源合同。 ### 20.5 根页与页头 @@ -599,20 +601,22 @@ goRoot(routeKey, params) - A04 取消或“已有账号”返回 A01;注册并建立会话后 `goRoot(G01)`。 - A05 取消返回 A01;只有未来真实重设接口成功才写入 `password-reset` 结果并回 A01。本地 `state=success`、计时器或视觉占位成功态只能普通返回,不得生成业务成功结果。 - A01 登录成功 `goRoot(G01)`;会话失效和退出成功也只允许 `goRoot(A01)`。 -- F02、F03 完成或返回精确回 F01;F06 新建回 F04,编辑回 F05;F09 上传完成回 F08。 +- 当前 F02、F06、F07、F09 只形成明确标注“尚未提交服务器”的独立本地预览;F03 评论草稿不插入评论列表、不清空、不增加计数,F05 收藏明确禁用,F10 明确未开放。F01—F10 均不产生写成功结果;F02/F03 返回精确 F01,F06 新建回 F04、编辑回精确 F05,F09 无结果回同一 `genealogyId + albumId` 的 F08。真实写接口返回服务器 ID 后才能同轮启用完成结果。 +- `data/mock.js` 是 F 系列 feed/article/album 只读夹具的唯一 owner,只公开按 `genealogyId` 列表和按复合身份详情的深拷贝查询;跨谱、未知实体或缺失身份失败关闭。`utils/api.js::createFeed` 在 mock 模式以 `WRITE_UNAVAILABLE` 拒绝,不得修改列表冒充发布成功。 - R02 回 R01;R04 回 R03;R06、R07 回 R05。 -- T04 保存后回 T01 并定位新成员;T05、T08 回当前 T03;T06 保存后回 T01 并刷新原焦点;T07 选择成员后回 T01 定位或进入 T03。 +- 当前 T04/T05/T06 只生成“尚未提交服务器”的本地预览,不产生写成功结果;用户确认放弃预览后,T04/T06 无结果回 T01,T05 使用 `goBack()` 精确回原 T03 实例,T08 普通返回 T03,T07 可进入 T03。新增亲属、编辑成员和编辑关系的完成定位只能在真实版本化写接口同轮启用。 +- 当前阶段 `data/mock.js::treeMembers` 是唯一可变成员夹具 owner,`utils/api.js` 是唯一写入口;T01/T03—T08 只能通过 `listTreeMemberFixtures(genealogyId)`、`findTreeMemberFixture(genealogyId, personId)` 取得含亲属数组深拷贝的快照。成员身份必须由家谱与成员复合定位,缺失、未知或跨家谱身份失败关闭;只有 T04 的明确首位成员模式允许没有 `personId`。Task 18 接入真实新图合同后必须原子删除该临时 owner、选择器和旧 `id/parentId` 模型,不保留双读适配层。 - N02 默认回 N01;通知业务目标失效、无权限或字段不足时留在 N02 的明确状态,不猜测页面。 -当前 OpenAPI 已确认 `POST /genealogy/app/auth/register` 的成功响应复用 `LoginResult`,其 `LoginVo` 可返回 `token/accessToken/tokenValue`。因此 A04 的正式终点不是“注册后再登录”,而是保存有效会话后直接 `goRoot(G01)`;在真实注册与行为验证尚未接入前,不得把当前占位反馈冒充注册完成。 +2026-07-22 新线上 OpenAPI 已确认密码登录、短信登录和注册统一返回 `RAppLoginVo`,其 `data` 引用 `AppLoginVo`,唯一会话字段为 `access_token`。因此 A04 的正式终点不是“注册后再登录”,而是保存有效会话后直接 `goRoot(G01)`;受保护旧快照的响应形状不再作为兼容读取路径,在真实注册与行为验证尚未接入前也不得把当前占位反馈冒充注册完成。 ### 20.7 T03 单实例成员轨迹 -- T03 原生页面实例只保留一个。初始成员成功读取后先把轨迹初始化为恰好 `[initialPersonId]`;初始读取失败不建立轨迹。点击亲属只更新页面内 `personId` 和成员轨迹,不再 `navigateTo` 同一路由。 +- T03 原生页面实例只保留一个。初始路由 `personId` 是不可变的宿主页路由身份,初始成员成功读取后先把轨迹初始化为恰好 `[initialPersonId]`;初始读取失败不建立轨迹。点击亲属只更新页面内活动成员 `personId` 和成员轨迹,不改 URL,也不再 `navigateTo` 同一路由。 - 新成员资料成功读取后才写入轨迹;读取失败保留原成员和原轨迹。 - 返回先逐项弹出成员轨迹,轨迹结束后才返回 T01、T07、R02 或实际来源。 -- T05、T08 返回时刷新当前轨迹项,不新增 T03。 -- 如果 T03 已位于原生栈下方,再次打开同一 `genealogyId` 的 T03 时必须回到该实例,并用目标页允许的类型化一次性操作请求加载目标成员;不得创建第二个 T03。若栈中 T03 属于不同 `genealogyId`,网关返回 `T03_CONTEXT_CONFLICT` 并拒绝压栈,调用方必须先通过根语义切换家谱上下文。 +- 当前 T05 本地预览以 `goBack()` 返回并保留原 T03 宿主页身份、活动成员和轨迹,T08 普通返回也不新增 T03。未来真实成员更新结果只允许刷新与 `entityId` 相同的当前活动成员,不得切换轨迹或改写宿主页身份。 +- 如果 T03 已位于原生栈下方,再次打开同一 `genealogyId` 的 T03 时必须回到该实例,并用目标页允许的类型化一次性操作请求加载目标成员;不得创建第二个 T03。若栈中 T03 属于不同 `genealogyId`,网关返回 `T03_CONTEXT_CONFLICT` 并拒绝压栈;若迁移前遗留栈已有多个 T03,则返回 `T03_STACK_CONFLICT`,不得只激活最近实例后宣称栈已唯一。调用方必须先返回或通过根语义清理冲突上下文。 - 从直接链接进入 T03 且没有来源页时,规范父页为带当前成员定位参数的 T01;缺少 `genealogyId` 时继续回退 G01。 ### 20.8 通知和外部进入 @@ -805,6 +809,10 @@ PATCH /genealogy/app/v2/genealogies/{genealogyId}/lineage/relationships/{relatio - 现有人物搜索只需稳定返回字符串 `personId`;新增人物、编辑人物和编辑关系接口必须加入版本并发合同。当前 OpenAPI 没有 T06 所需的关系修改入口,因此新增上述关系 PATCH。不可变 `relationshipKind` 只允许 `PARTNER/PARENT_CHILD`,必须与服务端既有关系一致;请求使用以该字段为 discriminator 的 `oneOf`:PARTNER 分支只更新 `relationType/status`,PARENT_CHILD 分支只更新 `relationType/parentRole`。每个分支除 `relationshipKind` 外至少包含一个可修改字段,省略字段保持原值;只有 discriminator 的空更新返回 `422 RELATIONSHIP_PATCH_EMPTY`。参与人和 `relationshipKind` 不可在 PATCH 中偷换,也不以客户端删除再新增模拟修改。 - 旧 v1 `/genealogy/app/genealogies/{genealogyId}/lineage/tree` 保持原合同供既有消费者使用;当前 App 只接入上述四条明确的 `/genealogy/app/v2/...` 路径,不双读、不做运行时版本探测。 +四条操作统一声明 `200/400/401/403/404/422/429/5XX`;tree、overview 和 relationship PATCH 还必须声明 `409`。稳定业务码字段唯一固定为错误响应根层必填字符串 `businessCode`,对应 HTTP 响应使用 `oneOf` 分支中的单值 enum;OpenAPI 3.1 也可使用 `const`。description、example 或无关 metadata 中出现同名文本都不构成合同。`generationRange` 固定为关闭额外字段的 `{ minGeneration, maxGeneration }`,两项均为大于等于 1 的整数;`minGeneration <= maxGeneration` 由运行时校验。 + +接口门禁分为两层,不能互相冒充:`tests/lineage-openapi-contract.ps1` 的静态层只验证 OpenAPI 能结构化表达的路径、参数全集与单字段边界、响应引用闭包、discriminator/oneOf、精确字段集、枚举、字符串 ID、错误响应、`If-Match`、JSON/YAML 引用一致性以及 v1 隔离。平铺 query Parameter Objects 即使在 OpenAPI 3.1 中也不能证明 FOCUS/BOUNDARY 的跨参数互斥;入口集合、引用、环、bucket 总和、隐私根、locator 分段首尾、cursor/version 绑定、409/422 真实行为及 PATCH 省略字段保持原值同样必须由运行时 validator 与部署集成测试验证,禁止用 description、example 或关键词命中制造假绿。支持 schema 的文件名不由客户端另造,固定根模型之外沿真实 `$ref` 闭包验证结构;YAML 静态层只证明限定块内的路径、operationId 和完整 component 引用闭包与 JSON 一致,字段语义仍必须在后端同版本导出后逐项复核。 + ### 21.10 明确删除的旧路径 同一迁移中删除:页面内置运行时 members、递归 `children/spouses`、单一 `parentId`、`fatherId/motherId` 与 `parentId` 双读、`person.id || person.personId`、API 或 fixture 的 `x/y`、数字 int64 ID、默认“族人/主支”语义兜底、顶层 `.map(toTreeNode)`、全量 CSS Grid track、多 `` 拼线、Canvas 线叠 DOM 节点、选中态改尺寸和全局 `NODE_HALF_HEIGHT`。 @@ -830,14 +838,202 @@ PATCH /genealogy/app/v2/genealogies/{genealogyId}/lineage/relationships/{relatio 性能门槛:单窗口不超过 500 人;`dataReady` 定义为规范图完成校验的时间点,`interactive` 定义为首帧绘制结束且命中索引可用,二者间隔不超过 800ms;输入到反馈 p95 不超过 100ms;持续手势帧耗时 p95 不超过 32ms;不连续出现两帧超过 100ms;20 次聚合跳转与搜索后驻留内存相对稳定态增长不超过 15%。每项 MuMu 时延指标至少采样 30 次并按 nearest-rank 计算 p95;手势帧由 renderjs 的 `requestAnimationFrame` 记录,输入延迟从视图层触摸时间戳量到下一完成帧;内存先预热 3 轮,再在相同空闲点比较 20 轮。不把全 App 冷启动混进 T01 门槛。未达到门槛时缩小窗口或进入列表,不能提高上限掩盖问题。 -## 二十二、当前精确执行顺序 +## 二十二、认证、TAC 与可访问安全设计 + +### 22.1 唯一所有者 + +- `utils/auth-verification.js` 唯一拥有 `APP_SMS_LOGIN/APP_REGISTER/APP_FORGOT_PASSWORD` 三个认证场景、4 位短信码和 require/verify 响应边界。 +- `components/TacVerification.vue` 唯一拥有 TAC 浮层、renderjs 加载、可见生命周期、焦点和返回行为;页面不得直接操作供应商全局对象。 +- `static/tac/js/jiapu-tac-adapter.js` 唯一拥有 TianAi challenge、proof 与 verify payload 映射。后端提供的 `tac.css/tac.min.js/icon.png/dun.jpeg` 保持原字节,由专项哈希合同保护。 +- `utils/api.js` 唯一拥有已审查请求的 HTTP 200 严格 envelope、15 秒超时、RequestTask 中止和错误分类,并拥有认证密码摘要、`validToken` 发码、`AppLoginVo.access_token` 会话写入及反馈 wire payload。页面不复制 header、地址、响应兼容、token 读取或请求控制器。 + +### 22.2 页面与状态机 + +A01 默认短信登录;密码登录因为服务端请求体无法消费 TAC 票据而保持可见但不可用,微信登录也不得以视觉入口冒充已接通。A01/A04/A05 的发码顺序固定为:本地字段和协议校验 → `/captcha/require` → 严格 TAC challenge/verify → 取得服务端 `validToken` → `/genealogy/app/auth/sms/code`。验证码精确 4 位,注册或重设提交只消费短信码,不再重复 TAC。手机号变化、刷新 challenge、切换验证方法、返回或卸载都使旧上下文失效;超时、空响应、非 JSON、重复回调和迟到回调失败关闭并保留表单。短信发送与找回密码返回 `RVoid`,其 schema 未把 `data` 列为必填;客户端仍强制 HTTP 200 和整数成功 `code`,但把省略 `data` 与 `data:null` 都归一为 `null`,不把这一合法空响应误判为失败。有实体的 challenge、登录和注册响应继续要求 `data`。 + +短信状态覆盖可发送、验证中、发送中、60 秒倒计时、失败重试、到期、手机号变更和重复点击。客户端倒计时不是服务端限流证据;最终仍须用真实服务验证手机号/IP/设备/租户限流、前后台恢复、过期和并发重放。注册成功只读取 `RAppLoginVo → AppLoginVo.access_token`,保存会话后直接进入 G01;找回成功返回 A01,不自动登录。 + +### 22.3 后端验证中心合同 + +`VerificationCenter` 是唯一授权 owner,供应商只提供 evidence,不直接签发或消费短信票据。`/captcha/require` 建立绑定 tenant、client、scene、规范化 subject 和风险策略版本的 `verificationSessionId`;`required=false` 也直接签发供指定 audience 使用的一次性 grant,不能让短信端点出现无票据分支。同一 session 只允许一个活动 challenge;刷新或切换方法使旧 challenge 失效但不清零累计失败。 + +`/captcha/verify` 的请求以 evidence/provider 为 discriminator 使用 `oneOf`,根和每个 payload 都 `additionalProperties:false`。provider、type、scene 和 subject 以服务端 challenge 记录为真,客户端字段不能改变绑定。只有供应商 evidence 与本地风险策略共同通过才签发 opaque `validToken`;票据绑定 session、tenant、client、scene、subject、challenge、method、assurance、audience 和过期时间。`/sms/code` 在同一事务中完成 `ISSUED → CONSUMED` 与唯一短信 outbox 创建;同一幂等键返回原结果,不同键或并发重放不能产生第二条短信任务。 + +当前后端门禁固定为:`API-AUTH-TAC-001` 补齐密码登录票据;`API-AUTH-TAC-002` 修复真实 challenge 空 500 与错误 envelope;`API-AUTH-TAC-003` 关闭验证 schema 并建立 discriminator;`API-AUTH-TAC-004` 落实 session、多方法、`required=false` 票据和原子消费。`AUTH-TAC-OPENAPI-CONTRACT BLOCKED` 解除前,`runtimeConfig.mode` 必须保持 `mock`。 + +### 22.4 不降风控的可访问路径 + +TianAi 指针滑块不能因外层 dialog 可聚焦就被宣称为 TalkBack 或键盘可完成;不得生成固定键盘轨迹,也不得检测辅助技术后免验证。不存在 `accessibility=true`、`skipCaptcha`、供应商故障放行或客服直接发短信等旁路。 + +三人交叉质询后的统一 P0 是 provider-neutral 验证中心与可恢复的文字/中继 `MANUAL_REVIEW`。文字渠道只是可访问通信媒介,各 scene 仍有独立身份或号码控制证据。案件继承且不可改写原 session 的 subject/scene,具备去重、RBAC、主体/IP/设备/审核员限额、完整审计、服务时段、容量和 SLA;高风险找回或换号双人复核。坐席只提交决定,验证中心才可签票;异步案件和批准授权在合理期限内可恢复,用户重新进入原流程时才激活短时 token,避免在通知前过期,也不得要求残障证明。 + +中国大陆非交互风控供应商只进入限时 POC;必须在真实 UniApp Android WebView 中证明 TalkBack、外接键盘、Switch Access、弱网、超时、异常/重放票据、误杀和攻击拦截指标,达标后才可成为默认自动路径,不能预先宣称符合无障碍标准。音频验证码只作为另一个 POC 候选,必须验证可懂度、听障覆盖与 ASR 对抗,不能单独上线或成为唯一替代。设备断言只有在同一 subject 的已认证会话绑定硬件保护私钥、服务端 nonce、RP/App 绑定、`userVerification=required`、短时单次、防重放并检查撤销时才可独立放行;普通设备指纹、完整性或仅 user-presence 只能作为风险信号,注册和未绑定设备不得使用。 + +客户端现有壳层只完成 dialog 命名、说明关联、初始聚焦、Tab 圈定、Escape/Android 返回、焦点恢复、原生刷新/关闭、48px 目标和小视口滚动。最终必须在 MuMu 用 TalkBack、外接键盘与非拖动路径完成 A01/A04/A05;证据缺失时 `ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED` 必须保持红灯。 + +## 二十三、领域上下文与反馈提交设计 + +`utils/genealogy-context.js` 是当前家谱词法 ID 与失效 tombstone 的唯一持久 owner,`utils/session.js` 是账号令牌边界。只在首次且没有历史上下文或失效标记时自动选择首个可用家谱;调用方给定的新列表不再包含历史 ID,或显式目标不可用时,必须清理 ID、写入 tombstone 并让用户明确重选,后续重载不能用列表第一项继续渲染。令牌损坏、退出和账号切换都同步清理 ID 与标记;导航一次性结果不得承担领域持久化。实际后台撤权仍需 workspace 在 onShow 或失效事件中重新取得服务端列表,当前基础不能被表述为实时权限刷新已完成。 + +M07 的唯一接口 owner 是 `appApi.submitFeedback`。请求只允许 `feedbackContent/feedbackType/contactInfo` 三个普通字符串数据字段;内容必填,类型和联系方式可选且无客户端枚举映射;请求选项不能覆盖认证要求。remote 只接受 `POST /genealogy/app/feedback` 的 HTTP 200 与整数成功 `code`,反馈响应未声明 `data` 必填且页面不消费实体;mock 必须抛 `WRITE_UNAVAILABLE`。页面提交中锁定表单并捕获规范快照;成功和 uncertain 回流都再次核对当前快照,保留回执或未知结果并阻止原样重复写,迟到输入保持为尚未提交的新内容。超时、断网、意外 2xx/3xx、HTTP 408/5xx 或响应失真进入 `uncertain`,只有确定拒绝允许重试。本地不持久保存反馈原文或哈希 tombstone;跨重启 at-most-once 必须由未来服务端幂等键或状态查询合同解决,不能以隐私数据、无期限锁或伪保证替代。 + +### 23.1 家谱工作区只读合同 + +家谱工作区第一批唯一选择 `GET /genealogy/app/genealogies/mine` 持有当前账号可访问集合,选择 `GET /genealogy/app/genealogies/{genealogyId}/overview` 持有 G05 展示;语义重复的 `GET /{genealogyId}` 不同时接入。页面不把 fixture 与远端事实混用,也不在两个详情响应之间择优补字段。 + +线上 `AppGenealogyVo.genealogyId` 当前是 JSON `integer/int64`。最大合法 int64 经 JavaScript JSON 解析后会不可逆失真,事后 `String()` 无法恢复,因此响应身份必须改为非空词法字符串;URL path 在 wire 上本是文本,手写客户端可保持词法字符串,不机械把 path 声明本身当成同一阻塞。仅拒绝 unsafe number 可以失败关闭,却会使 OpenAPI 合法用户永久不可用,只能用于关闭功能开关的预研,不能作为正式兼容方案。 + +首批最小实体只消费 `genealogyId/genealogyName/canView/canManage/canEditContent/roleType`。两层响应 envelope 的 `code/data` 与这些实体字段必须进入 `required`;ID、名称为非空字符串,三项 capability 为布尔值,`roleType` 提供至少两个稳定非空枚举供 G01 分组和标签。其他字段保持可选:地点、堂号、人数、简介等有值才展示;未知额外响应字段允许忽略。若产品坚持保留当前 fixture 的来源、管理者、认证、始祖、上级谱、支系、更新时间和激活人数,则后端必须另补合同;首批不为复刻 mock 强制 22 个字段全部必填。 + +`canView` 是进入 `availableIds` 的授权投影,不能从 `roleType/status/memberStatus` 猜值;后端也可以改为能够由自动化证明“`/mine` 只返回当前仍可查看 ACTIVE 成员”的等价合同。G01 仅在新列表成功校验后 reconcile;网络、超时、5xx 和畸形响应保留旧现场并显示错误,明确无权、撤权或不存在才写 tombstone。G05 每次加载先清空旧数据、取消迟到请求,只消费 `/overview`;能力缺失按最低权限处理,不回退本地角色。 + +当前线上 OpenAPI 没有有效 security 声明且媒体写作 `*/*`,但无令牌实测三条读取均被拒绝、实际 Content-Type 为 JSON,因此两项是必须修复的发布文档质量问题,不单独声称已经公开泄漏。真正发布门禁还包括有效令牌下的无凭证、跨账号、撤权、删除、401/403/404/5xx、畸形 JSON 与取消反例。当前部署使用 HTTP 200+业务 `code=401`;后端可以保留业务码承载或改为规范 HTTP 状态,但文档、运行时 validator 与部署行为必须一致。 + +`tests/genealogy-workspace-openapi-contract.ps1` 只读取受保护的同版本 JSON/YAML,固定上述响应身份、typed envelope、required 与 capability/role 边界;当前旧双导出与线上文档都不能通过。门禁关闭前不建立 G01/G05 专属字段 adapter,不让客户端成为第二份猜测合同。问题固定为 `API-GENEALOGY-WORKSPACE-001` 无损身份与最小 schema 闭包、`API-GENEALOGY-WORKSPACE-002` 可访问集合与能力投影、`API-GENEALOGY-WORKSPACE-003` 错误语义及对象级授权行为。 + +### 23.2 M06 帮助内容只读合同 + +M06 第一批唯一选择 `GET /genealogy/app/help-articles` 持有完整 FAQ 列表。线上 `HelpArticleVo` 已同时包含 `helpCategory/helpTitle/helpContent`,所以单页手风琴不再调用 `/{helpId}`;详情端点只保留为未来文章深链或其他 surface 的候选,不进入当前页面。这样当前页面不读取、比较、缓存、序列化或回传 `helpId`,线上 JSON `integer/int64` 虽仍是未来详情债务,却不会参与 M06 身份或行为。 + +成功响应必须由 `RListHelpArticleVo` 唯一拥有,并把 `code/data` 设为 required;`data` 是允许为空的 `HelpArticleVo[]`。每行只要求 required、非空的 `helpCategory/helpTitle/helpContent`:分类是可直接展示的标签,正文首版明确为纯文本,数组只含当前可发布文章且顺序就是展示顺序。分类 enum、详情 ID、封面、浏览量、状态和排序字段都不由客户端消费;“全部”是唯一客户端分类值。富文本、Markdown、图片和文章详情只有在另立格式与内容安全合同时才可进入。 + +adapter 必须使用 allowlist 投影为 `{category,title,content}`,不得 spread 原对象。每次合法响应先按原始顺序分配仅限当前 generation 的 ordinal 展示键,再进行搜索和分类;筛选后的 index 绝不能成为 key。搜索、分类、刷新或原子替换列表前清空展开项;唯一列表 controller 与 generation 共同拒绝取消后的迟到响应。重复标题或正文不会产生 key 冲突,也不能据此生成持久身份。 + +页面状态固定为加载、正常、服务端空列表、搜索/筛选无结果、失败可重试和认证失效;失败不得回退成本地五条内容并冒充线上成功。搜索只匹配标题与纯文本正文,分类从返回标签按首次出现顺序动态去重。分类与标题使用原生按钮语义并满足至少 44dp 触控目标;分类补 `aria-pressed`,问题补 `aria-expanded/aria-controls`,加载、计数、失败和展开状态提供适当播报。最终系统字号、TalkBack、焦点和视觉只能在 MuMu 验收。 + +当前受保护 JSON/YAML 仍把列表写成通用 `ListResult/RList`;线上虽已出现 `RListHelpArticleVo/HelpArticleVo`,两者仍无 `required`,正文格式、仅发布与排序保证也未定义。匿名实测列表、带分类列表和详情均为 HTTP 200+`{code:401,data:null}`,而线上文档声明 HTTP 401 string 且 operation 没有有效 security。后端可以选择规范 HTTP 401 或稳定业务 401,但文档、部署和 validator 必须一致。`tests/help-center-openapi-contract.ps1` 当前输出 `HELP-CENTER-OPENAPI-CONTRACT BLOCKED`;问题固定为 `API-M06-001` 专用响应与最小 required 闭包、`API-M06-002` 纯文本/发布范围/顺序语义、`API-M06-003` 认证与错误承载一致性。门禁通过前不写 live-only adapter。 + +### 23.3 个人资料只读合同 + +`GET /genealogy/app/auth/profile` 是 M01 身份卡、M02 表单初值和 M03 绑定手机号展示的唯一 wire owner。首批不消费 `userId/avatar/status/tenantId/userNo/sex/birthday/registerSource/loginIp/loginDate/clientKey/deviceType`;数字 ID 与状态字典因此不构成本批门禁。M01 当前显示的“创建者”是家谱级角色,不属于账号 profile,必须删除而不是向后端索要错误字段。 + +成功响应固定为 `RAppProfileVo → AppProfileVo`,envelope 的 `code/data` required。wire 实体仅把 `phone` 设为 required,并要求 canonical `^1[3-9]\d{9}$`;`nickName/realName/email` 可选,省略是唯一“未设置”形态,出现则拒绝 null、空串和边界空白,姓名为 1—30 字符,邮箱为有效格式且 1—100 字符。把四项全部 required 会让没有实名或邮箱的合法旧账号拖垮 M01/M03;省略可选字段后由 adapter 统一为空串,不是旧字段兼容或双模型。GET 只定义读取;未来 PUT 必须另证省略字段是否保持原值并只发送 dirty fields,不能在本批预判。 + +唯一 profile normalizer 在 mock 与 remote 中输出固定 `{maskedPhone,phoneAccessibleLabel,nickName,realName,email}`。原始手机号只在函数局部完成格式校验和掩码,页面、缓存、错误详情、日志与路由不得出现明文;可选字段只有真正缺席才规范成空串,出现但非法时整份响应失败。所有其他响应字段使用 allowlist 丢弃。`appApi.getProfile` 必须经 `resolveRuntimeMode()` 区分 mock/remote;非法配置失败关闭,remote 走严格 envelope、15 秒超时和 request controller,不以 `hasRemoteConfig() ? remote : fixture` 把错误配置伪装成本地数据。三个页面可以各自读取同一 adapter,但不建立跨会话缓存。 + +M01 在 `onShow` 刷新并以 controller+generation 拒绝迟到结果;资料 loading/error 只替换身份卡,服务菜单和底栏不因普通读取失败而消失。旧 `query.state=error` 与“重新查看即 ready”是假状态,接线时删除;通知未读数在通知域接通前不得与真实 profile 混装为线上事实。M02 首次加载成功后再填充三个字段并重设 baseline,加载不能制造脏表单;普通 GET 失败可重试,编辑期间不后台刷新覆盖输入,保存仍明确是本地校验,不能冒充 PUT。M03 只让手机号行局部加载/失败,密码入口不依赖 profile;M05 的当前手机号展示在真正 remote 前也必须消费同一掩码 owner 或隐藏,不能保留 fixture。 + +账号失效交给会话 owner,普通 network/timeout/5xx/畸形响应只产生可重试读取失败,没有写请求的 uncertain 状态。实施测试必须证明 optional 缺席、出现非法值、明文不泄漏、超大 `userId/avatar` 丢弃、错误配置、取消、迟到回调、M01/M03 局部失败和 M02 baseline。交互同时改为原生按钮,补加载播报、M02 字段错误关联与聚焦、44dp 目标、长昵称换行和装饰图隐藏;最终系统字号、TalkBack、纹理对比和返回流程仍只由 MuMu 验收。 + +当前受保护双导出仍引用通用 `ObjectResult/RObject`;线上 `RAppProfileVo/AppProfileVo` 没有 required、phone pattern 或姓名/邮箱边界,GET operation 也缺有效 security/clientid。匿名真实响应是 HTTP 200+业务 `code=401`,文档却列 HTTP 401 string。`tests/profile-openapi-contract.ps1` 当前输出 `PROFILE-OPENAPI-CONTRACT BLOCKED`;问题固定为 `API-PROFILE-READ-001` 专用响应与最小字段闭包、`API-PROFILE-READ-002` 可选字段与隐私投影、`API-PROFILE-READ-003` 认证/媒体/错误一致性。门禁通过前不写 live-only adapter。 + +### 23.4 通知读取与已读状态合同 + +通知必须分成读取批次和写入批次,不能为了让页面看起来完整而在同一个本地 clone 中伪造读写闭环。读取只由不带 `readStatus` 筛选的 `GET /genealogy/app/notifications` 与 `GET /genealogy/app/notifications/unread-count` 持有;前者返回当前账号完整活动通知集合,集合最多 200 条、按最新优先,后者精确统计同一集合中 `readStatus=UNREAD` 的条目。两个独立请求之间若有新消息或多端读状态变化,短暂不相等是合法并发结果,页面分别展示成功响应并在下一次刷新收敛。 + +读取成功的最小 wire 形状为 `RListNotificationVo → NotificationVo[]` 与 `RNotificationUnreadCount → int32`。两层 `code/data` required;计数范围为 0—200。通知首批只 required `noticeTitle/noticeContent/publishTime/readStatus`:标题 1—50,正文 1—1000、完整且为 plain text,时间为带时区的 RFC3339,状态精确枚举 `READ/UNREAD`。客户端不解释 HTML、Markdown、服务端 URL 或 `bizType`,也不从标题、类型或摘要猜业务目标。N01 只将摘要显示限制在最多 160 个 Unicode 字素,N02 必须持有同一响应中的未截断正文。 + +读取 adapter 首版公开模型固定为 `{snapshotKey,title,content,publishedAt,unread}`。`snapshotKey` 使用成功响应 generation 与映射前 ordinal 组成的词法 key;裸 ordinal、数组筛选后 index、服务端 int64 ID 和内容哈希都不能成为页面身份。唯一内存快照不可变且不落盘;成功刷新原子替换 generation,退出或账号切换立即清空。N02 在进入时解析并持有该 generation 的完整条目,应用重启、旧 generation、直接构造或未知 key 统一显示“请返回消息中心重新打开”,不能虚构“服务端已删除/已撤回”。 + +读取批次显式丢弃 `notificationId/genealogyId/senderUserId/senderPhone/bizId/bizType/noticeType` 等服务端字段;N01 的通用审核 CTA、空态 G10 按钮和 N02 目标按钮一并删除。只有后端以后提供闭合的 `bizType → route key+必填词法参数+权限/失效语义` 字典,并为每种目标给出越权、删除和跨谱反例,才能另立目标分流批次;任何原始 URL 都不得执行。M01 与 G01 共同消费未读数 owner,文案只称“未读消息”,可见数大于 99 时显示 `99+`,可访问名称仍包含真实数量;加载或错误只影响各自消息入口,不锁死根页其他功能。 + +写入批次由 `POST /genealogy/app/notifications/{notificationId}/read` 与 `POST /genealogy/app/notifications/read-all` 唯一持有。此时唯一 notification controller 可以从同一个严格列表响应私有保留 `notificationId`,但页面模型、路由、日志和持久缓存仍只见 `snapshotKey`;ID 必须在列表实体与 path 中同为 1—128 位 URL-safe opaque string,禁止 int64 经 JavaScript 解析后再转字符串。该迁移必须原子删除首版“完全丢弃 ID”的内部实现和 N01/N02/M01/G01 所有 fixture 计数与本地 clone 写入口,不保留双 owner。 + +单条已读和全部已读对当前账号必须幂等,重复调用成功且无重复副作用。认证失效返回稳定 401;不存在与跨账号单条 ID 统一为 404 `NOTIFICATION_NOT_AVAILABLE`,避免泄露实体存在性。read-all 的截止点是服务端接收该请求时当前账号已经存在的活动通知,截止点之后并发到达的消息保持未读;成功后客户端重取列表和未读数,不用本地递减猜结果。超时、断网、HTTP 408/5xx 或畸形响应属于结果未知,依靠幂等重试或重新读取收敛;明确 4xx 才是确定拒绝。 + +当前受保护双导出把列表返回写成通用 `ListResult/RList`,完全缺少 unread-count 路径与专用通知模型;线上虽出现专用 VO 和四条操作,但 wrapper/VO 无 required,`readStatus` 无枚举、内容无长度/纯文本/完整性、列表无容量和顺序,ID 仍是 int64,operation 也缺有效 security/clientid。匿名 list/count 实测均为 HTTP 200+业务 `code=401`,与文档 HTTP 401 string 冲突。`tests/notification-read-openapi-contract.ps1` 和 `tests/notification-read-state-openapi-contract.ps1` 当前分别输出 `NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED` 与 `NOTIFICATION-READ-STATE-OPENAPI-CONTRACT BLOCKED`;问题固定为 `API-NOTIFICATION-READ-001`—`003` 和 `API-NOTIFICATION-STATE-001`—`003`。双门禁通过前不写 live-only adapter,也不把本地已读行为称为成功。 + +### 23.5 M02 个人资料写入合同 + +M02 继续只使用 `PUT /genealogy/app/auth/profile`,不同时发布 PATCH 或第二写入口。由于页面只拥有 `nickName/realName/email`,PUT 必须在 operation 与专用 `AppProfileMergeUpdateBody` 中明确是原子 dirty-only merge,而不是整个 profile replacement:只允许出现 1—3 个真正改变的可编辑字段,出现字段更新、省略字段保持,额外字段关闭;同一 payload 重复设置相同值不能产生重复通知或其他业务副作用。后端若无法证明 presence-aware merge,就必须先替换现有操作,客户端不能从“字段 optional”推断安全。 + +`nickName` 出现时必须是去边界空白的 1—30 字符,空串和 null 均非法;这允许没有昵称的旧账号只修改其他字段,但不允许把已有昵称删除。`realName/email` 的精确空串只在 request command 中表示清空,省略仍表示保持;非空值分别满足 1—30 和 email 格式/1—100。null、纯空白、边界空白都非法,服务端不以隐式 trim 把空白偷偷解释成 clear。GET 和成功响应中,清空后的可选字段继续以属性省略表达,不能把请求命令形状泄漏回读取模型。 + +并发只使用一个 opaque `profileVersion` owner。`AppProfileVo.profileVersion` required,值为 1—128 位 URL-safe string;PUT required `If-Match` 携带同一 token,请求 body 不重复版本。后端以当前 principal+tenant+version 原子 compare-and-set,成功 200 返回完整 canonical `RAppProfileVo` 与新版本;旧版本返回 409,唯一稳定码固定为 `PROFILE_VERSION_CHANGED`。本项目 T01 已使用 body version+If-Match+409 模式,资料写入沿用同一并发语义;H5 CORS 必须允许 `If-Match`。 + +客户端先依赖 23.3 的 GET 取得 canonical 初值和版本,完成异步回填后建立 baseline。`normalizeProfileUpdate` 只从普通自有数据属性提取脏字段,禁止 getter、symbol、prototype、avatar/sex/birthday 和额外属性;clean 不发请求。提交开始时冻结三个输入并捕获规范快照、版本与 session generation;成功只接受严格 HTTP 200 typed envelope,以返回模型原子回填表单和 baseline。确定的 400/401/409/422 保留草稿并进入对应状态,409 不静默覆盖。 + +超时、断网、408/5xx、取消或畸形成功响应都是 outcome unknown。客户端不得自动重复 PUT,而先重新 GET:若本次所有脏字段均等于提交值,视为已提交;仍等于旧 baseline 才可由用户重试;出现第三值或版本无法归因则进入 conflict,并保留当前草稿供用户明确选择重新载入或基于最新值继续。页面卸载、退出和账号切换中止等待并清空未持久草稿;RequestTask 取消不证明服务端没有落库,旧 session generation 的迟到响应永远不能污染新账号。 + +M02 当前把 `currentUser.name` 同时填入昵称和真实姓名,是必须删除的 PII 伪造;500ms 定时器只形成本地预览,不是接口状态。真正实施时状态至少包括 loading、ready/dirty、saving、success、error、uncertain、conflict、auth-expired;保存期间锁定输入和返回,成功重置 dirty,失败保留草稿。头像假按钮、相册权限和 int64 avatar 不混入本批;“邮箱用于接收通知”在验证/送达合同缺失时删除。原生输入和按钮补齐 `aria-invalid/aria-describedby`、错误关联、首错聚焦、busy/status 播报、email 键盘类型与 44dp 目标,最终只由 MuMu 验收。 + +受保护双导出仍使用旧 `ProfileUpdateBody`,缺 realName/email,示例还出现 schema 外 `regionCode/addressDetail`,成功为 generic `RObject`。线上改为 `AppProfileUpdateBody → RAppProfileVo`,但 body 无 required/minProperties/关闭额外字段/merge/version,响应无 required,operation 无 security/clientid,媒体还是 `*/*` 且只列 200/401。`tests/profile-update-openapi-contract.ps1` 当前输出 `PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED`;问题固定为 `API-PROFILE-UPDATE-001` 唯一 merge owner 与字段命令、`002` 版本并发和 409、`003` typed 响应/认证/错误/CORS、`004` 超时对账与账号隔离。读取和写入门禁都通过前不接 M02 真实保存。 + +### 23.6 M10 当前设备退出合同 + +`DELETE /genealogy/app/auth/logout` 唯一语义是撤销 Authorization bearer 所属的当前设备 credential family,包括同一登录会话的 refresh 能力(如果未来存在);同账号其他设备保持有效。“退出全部设备”必须是另一条接口、另一层确认和另一份权限合同。200 只在撤销已经传播到所有鉴权节点、旧 access token 不能再访问任何受保护接口且旧 refresh 不能换取新 token 时返回;已经在鉴权前通过的并发业务请求无法回滚,产品文案不得承诺取消所有进行中操作。 + +DELETE 没有 body,required SaToken 和与 token client 绑定的非空 clientid。为形成唯一幂等成功出口,凡能验证为该 client 历史签发的 active、revoked 或 expired credential,重复调用都返回相同的 HTTP 200 `RVoid`,不产生额外副作用;伪造、格式非法和 client 不匹配才返回 401 `RLogoutRejected`,稳定码只允许 `TOKEN_INVALID/TOKEN_CLIENT_MISMATCH`,它们不是撤销成功。200/401 均为 application/json 且 `Cache-Control: private, no-store`,RVoid 的 integer code required,同时声明 400/429/500。 + +客户端唯一 owner 是服务级 `logoutCoordinator`,而不是 M10、A01 和 session 各存一份。一次同步临界段按顺序:捕获 A 的 token/clientid 和当前 session epoch;通过 session owner 立即 bump epoch 并且只清一次 token、家谱上下文与所有已登记账号缓存;使用显式 A 快照创建不绑定页面 controller 的 DELETE RequestTask;发布非敏感 attempt 并立即 `goRoot(A01)`。该同步段没有 await,创建请求同步失败也不恢复 token。M10 页面永远拿不到 token,请求不会因 M10 卸载或 reLaunch 主动 abort。 + +异步 callback、catch 和 finally 只能按 attemptId 更新 coordinator,绝不能再次 `session.clear()`;否则 A 的迟到响应会抹掉随后登录的 B。coordinator 的公开内存态固定为 `{attemptId,logoutEpoch,status}`,status 为 `pending/confirmed/unconfirmed/not-revoked`,不含 token、header、响应 payload 或服务端消息。最多允许用同一 A 快照做一次同进程短时有界重试;不写 storage、日志或持久队列,也不从 session 重新取 token。 + +A01 仅在 session 仍为空、epoch 与 logoutEpoch 相等时订阅并原子消费一次状态;B 登录或 epoch 改变后丢弃 A 的迟到提示。进程被杀允许丢失提示,但本地退出已经持久完成。唯一主句始终是“已从本机退出”:pending 补“正在结束服务器会话”,严格 200 为 confirmed;network/timeout/408/429/5xx、畸形响应或 generic 401 为 unconfirmed;typed 401/400/403 为 not-revoked。所有分支都留在 A01、不恢复 token、不返回 M10、不阻塞重新登录;状态必须可播报且不能只靠短 toast 或颜色。 + +M10 确认前的取消和 Android 返回只关闭确认层,零请求、零清理;确认后立刻退出可关闭弹层态并防双击,根导航失败时显示“本机已退出”遮罩且禁止返回受保护页面。当前文案“本机保存的密码不会被保留”没有任何密码存储 owner 证据,实施时改为“仅退出此设备,其他设备不受影响”。Mock 或错误 runtime config 也必须完成本机退出,并准确标记远端未确认,不能为了发请求把用户困在本地会话。 + +当前 M10 只做 `session.clear → close → goRoot(A01)`,本机边界正确但没有远端撤销、重复保护、epoch 或跨根提示;旧静态测试也锁定这条本地序列。受保护双导出有 DELETE、SaToken、clientid 和 200 `RVoid`,但缺范围、幂等、required 和错误;线上只有 200/401 `*/*`,又缺 operation security/clientid。`tests/logout-openapi-contract.ps1` 当前输出 `LOGOUT-OPENAPI-CONTRACT BLOCKED`;问题固定为 `API-LOGOUT-001` 当前凭证族与撤销传播、`API-LOGOUT-002` 幂等成功/拒绝模型、`API-LOGOUT-003` 安全/媒体/no-store/部署反例。门禁通过前不新增 logout API 代码,也不把本机退出写成服务端注销成功。 + +### 23.7 M04 登录态改密与统一密码凭证合同 + +当前本地双导出要求 `oldPassword/newPassword` 为 32 个十六进制字符的 MD5,线上 `AppPasswordChangeBody` 也只把它放宽为大小写十六进制;登录、注册和找回同样接受静态摘要。该摘要不是一次性 proof,而是后端登录入口直接接受的密码等价物,一旦从日志、代理、调试记录或终端泄漏即可重放;同时服务端看不到原始长度与 blocklist 命中,无法成为生产策略 owner。新合同不得长期双读 raw/MD5,也不得只改 M04 留下其他入口。A01 登录、A04 注册、A05 找回和 M04 改密必须同一批删除全部 MD5 wire fallback;只在认证 HTTPS 通道提交 raw `writeOnly` 密码,服务端使用独立盐与 Argon2id,无法使用时才采用合规 scrypt/PBKDF2。 + +密码 schema 只有两个 owner:`CurrentPasswordSecret` 原样、不 trim,允许 1—64 Unicode code point 以兼容已有账号;`NewPasswordSecret` 在明确 NFC 规则后为 15—64 Unicode code point,允许空格、Unicode、粘贴和密码管理器,不设置字母/数字/符号组成规则。注册、找回与改密新值共用后者;登录与改密当前值共用前者。服务端在写入前执行常见/泄露密码 blocklist、账号级限速、当前密码重新认证和新旧不同;客户端只镜像即时提示,不能成为可绕过的权威。TAC 是反机器人证据,不代替当前密码;用户要求的登录/注册/找回 TAC 继续由认证合同持有,正常 M04 不另建一套 TAC。 + +M04 的唯一会话方案是 ALL,而不是返回并安装新 token,也不是保留旧 bearer。严格 HTTP 200 `RVoid` 只能在新 verifier 已持久化、账号 `credentialEpoch` 已原子递增,并且包括请求者在内的所有设备、所有 client 的既有 access/refresh/renewal 会话已跨鉴权节点失效后返回。方案 B 会要求 typed 新 token 和响应丢失恢复协议,当前 RVoid 无法承载;方案 C 会让可能被盗的旧 token 在改密后继续有效,均被否决。两个使用同一旧密码的并发请求必须通过服务端 CAS 至多一笔成功,另一笔返回 typed 409 `CREDENTIAL_VERSION_CONFLICT`。 + +PUT required SaToken、非空 clientid、`application/json` 和关闭额外字段的 `PasswordChangeBody`;body 只含 oldPassword/newPassword,confirm 永不出端。200/400/401/409/422/429/500 都必须是 JSON 且 `Cache-Control: private, no-store`,429 带 `Retry-After`。409/422 的 `RPasswordChangeRejected.businessCode` 只允许 `CREDENTIAL_VERSION_CONFLICT/CURRENT_PASSWORD_INCORRECT/NEW_PASSWORD_SAME_AS_CURRENT/PASSWORD_POLICY_VIOLATION`;服务端和网关日志、APM、分析、崩溃记录及错误消息不得含密码、摘要或请求体。 + +客户端在 dispatch 前由 session owner 持久化唯一非敏感 marker `{sessionEpoch,startedAt}`,不保存 token、密码、摘要、body、重试键或 operation id。明确未写的 400/422/429 原子清 marker 并留页;严格 200、401、409 均清同 epoch 全部账号态并回 A01。network、timeout、取消、畸形 2xx 或 5xx 在发出后属于结果未知,同样清秘密与本机会话、回 A01 并只提示“请尝试使用新密码重新登录”,绝不自动重试或宣称失败。冷启动发现同 epoch marker 时,在任何受保护缓存渲染前先清会话;新登录 bump epoch,旧 marker 和 A 的迟到响应不得清掉 B。无需新增 operation-status,重新登录就是最小对账路径。 + +页面实施时状态至少是 ready/submitting/known-error/unknown/success:提交中冻结三个输入、显隐控制、按钮和返回;确定字段错误聚焦并关联 `aria-invalid/aria-describedby`,显隐改为可键盘操作的原生按钮与 `aria-pressed`,目标至少 44dp,持续状态可播报。成功与 unknown 的跨根提示不能只靠短 toast。M03“建议定期更新”改为风险触发建议;M04 允许自动填充、粘贴和密码管理器。`tests/password-change-openapi-contract.ps1` 当前输出 `PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED`;`API-PASSWORD-001`—`005`、密码登录 TAC、双设备/并发/故障注入和 MuMu 原生矩阵通过前,不修改 M04 的诚实本地预览。 + +### 23.8 M05 手机号换绑与统一短信码合同 + +M05 当前只展示脱敏 fixture,以 500ms 定时器验证新手机号和 4 位验证码并明确“不提交服务器”;没有当前密码、TAC、远端请求、会话迁移或结果未知状态。受保护双导出的 PUT body 是 `clientId/phone/smsCode`,线上则只有 `phone/smsCode` 并返回完整 profile;两者都缺 existing-factor 再认证、号码唯一性、会话撤销和 typed 错误。共享发码接口在线上还明确忽略权限。活动 bearer 加攻击者控制的新号验证码因此足以构成账号接管,不能接线。 + +三人比较了“共享发码 operation 按 scene 条件鉴权”和“专用受保护 operation”。标准 OpenAPI 3.x 无法把 operation-level security 与 body 中的 `sceneCode` 条件绑定;`security: [{}, {SaToken: []}]`、文字说明或 vendor extension 都不能让通用 validator、网关和 SDK 自动拒绝匿名 `APP_PHONE_CHANGE`,会产生 validator 允许而 runtime 拒绝的双合同。唯一方案是 `POST /genealogy/app/auth/phone/sms/code`:required SaToken 与非空 clientid,闭合 body 只有 `phone/validToken`,scene 在服务端固定,公共 `/auth/sms/code` 的 enum 删除 `APP_PHONE_CHANGE`。两个 operation 只分协议边界,底层仍调用同一 OTP 生成、存储和限流 owner,不复制策略。 + +最终 PUT 的身份闭环是活动 session+raw `currentPassword`+新号 OTP。TAC 只防自动化,不能替代既有因子;新号 OTP 只证明新号码控制权。不强制旧号 OTP,因为旧号丢失是正常换绑原因且不会增加独立因子;成功事务改为持久化旧号安全通知 outbox。所有账号若不保证有密码,稳定返回 `STEP_UP_UNAVAILABLE` 并进入独立找回,M05 不得降级为 bearer+新号 OTP。最终 body 精确为 `currentPassword/phone/smsCode`,不含 currentPhone、clientId、validToken、challengeId 或供应商字段;它依赖 M04 先完成 raw-password/慢哈希迁移,当前 MD5 wire 不得复用。 + +短信码唯一 schema owner 是 `SmsCodeSecret`:CSPRNG 生成恰好 6 位 ASCII 十进制字符串,保留前导零,TTL 5 分钟,60 秒重发冷却,最多 5 次失败,单次消费;重发原子废止旧码且不重置累计失败计数。同一 `(account,session/credentialEpoch,tenant,client,scene,newPhone)` 只有一个 active generation,服务端内部 generation 足以消歧,因此最终 PUT 不新增 challengeId/attemptId。A01/A04/A05/M05、注销等活动消费者、短信模板、生成器、OpenAPI、validator、页面和测试必须同版本从 4 位原子迁移为 6 位,禁止兼容双长度。 + +服务端在同一事务内验证当前密码与 active OTP、执行 `(tenant,canonicalPhone)` 唯一约束、消费 OTP、更新手机号、递增 credentialEpoch、撤销包括当前在内的全部 access/refresh session,并持久化旧号通知 outbox;严格 200 只返回 `RVoid`。两个并发换绑至多一笔成功,另一笔按 credential CAS 无写入。发码 POST 的结果未知不清登录态,但按服务端冷却避免立即轰炸;最终 PUT dispatch 前复用无秘密 `{sessionEpoch,startedAt}` credential marker,200、401、409、network/timeout/5xx/畸形响应或进程终止均清同 epoch 本机会话回 A01且不自动重试。 + +M05 页面实施时状态至少为 ready/tac/sending/code-sent/submitting/known-error/unknown:新号变化作废 TAC 与 OTP,发码后锁定号码,提交中冻结全部字段和返回。当前密码支持粘贴、密码管理器和带 `aria-pressed` 的显隐按钮;手机号与 OTP 使用能保留前导零的文本/电话输入和 numeric inputmode,错误具备 `aria-invalid/aria-describedby` 与首错聚焦,发送按钮为至少 44dp 的原生按钮,倒计时和持续状态可播报但不每秒打断 TalkBack。`tests/phone-change-openapi-contract.ps1` 当前输出 `PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED`;`API-PHONE-001`—`005`、M04/认证前置门禁、两设备/OTP/故障注入和 MuMu 矩阵完成前不修改诚实预览。 + +### 23.9 G03 家谱与始祖原子创建合同 + +G03 当前在一个页面实例中先收集家谱资料,再录入始祖,两个 320ms 定时器只生成 `local-created-*` 预览;没有真实 API、可信 `regionCode`、上下文安装或 G01/G05 远端回流。受保护双导出的 `GenealogyCreateBody` 和线上 `AppGenealogyCreateBody` 都只创建家谱;通用 `/lineage/persons` 又允许客户端提交代数、父母和账号字段。若按旧接口先建谱再建根,会制造非产品需求的 `ROOT_REQUIRED` 半成品及两次结果未知。三人先讨论了两写恢复,再反向检查是否存在跨库或“保存空谱”的硬约束;当前没有任何证据,因此一致选择原子 bootstrap。只有后端以后证明事务边界不可跨越时才重新立 saga,而不是把补偿复杂度预埋客户端。 + +唯一写 owner 仍是 `POST /genealogy/app/genealogies`,但旧 `GenealogyCreateBody/AppGenealogyCreateBody` 必须被 `AppGenealogyBootstrapBody` 原子替换,不保留双收。根对象关闭额外字段,只允许 `genealogyName/surname/ancestralHall/regionCode/accessPreset/rootPerson`,前五项除堂号外均必填;`rootPerson` 关闭额外字段,只允许 `name/sex/birthDate/biography` 且 name/sex 必填。服务端固定根人物 `generation=1` 和唯一首根,不接收 `generationName/personNo/appUserId/fatherId/motherId/status`;`sex` 只允许 `MALE/FEMALE/UNKNOWN`,页面默认 UNKNOWN;生日是本地日 `format: date`,不保留页面武断的 1800 年下限。文本按 Unicode code point、NFC 和无边界空白统一验证,长度沿用页面现有 4/24/12/20/200 上限。 + +严格 200 前必须在一个事务内完成配额竞争检查、家谱、OWNER 成员关系、唯一一世始祖、READY 状态和幂等成功回执;任一步失败全部回滚,通用人物 POST 只服务 READY 家谱中的普通人物,不能承担 bootstrap。数据库 `bootstrap-root` 标记是根身份唯一权威,不能从 generation=1 猜测:普通人物 POST 即使提交一世也不能新建/替换根;人物 PUT 对根的可编辑白名单精确且只有 `name/sex/birthDate/biography`,operation 以 `x-bootstrap-root-editable-fields` 登记这四项并以 `x-bootstrap-root-noneditable-policy=REJECT_422_BOOTSTRAP_ROOT_IMMUTABLE` 登记拒绝策略;任何 status/personStatus、账号绑定、世代、父母、根标记或其他字段即使出现在通用 body 中也必须以 typed 422 拒绝,不能用“值未变化”掩盖越权字段。人物 DELETE 不能删除根,parents mutation 不能给根新增或重挂父母。未 READY 固定 typed 409 `GENEALOGY_NOT_READY`,触碰白名单外根字段或根身份固定 typed 422 `BOOTSTRAP_ROOT_IMMUTABLE`。成功 `GenealogyBootstrapResult` 必填词法字符串 `genealogyId/rootPersonId`、`setupState=READY`、`roleType=OWNER` 和 `canView=true`;两个 ID 从 JSON 到 storage、context 和 URL 都不得经过 JavaScript Number。同姓、同名和同地区均合法,页面重复提醒只能是建议,后端不得把名称当唯一键。 + +访问规则的唯一 wire owner 是共享 `GenealogyAccessPreset`,枚举只有 `MEMBER_ONLY/PUBLIC_APPLY`。这不是 G03 私有别名:同一后端版本必须让 APP 家谱读取 `AppGenealogyVo`、bootstrap 创建和 G11 设置更新都引用它,并删除 `visibility/joinMode`、旧 create/update DTO 和 `utils/genealogy-contracts.js` 的远端数字映射;客户端不得长期同时接受新 enum 与旧 pair。设置请求同步收紧为闭合、至少一个脏字段的 `AppGenealogySettingsUpdateBody`,只含 `genealogyName/intro/accessPreset`。本任务只统一 G11 的字段合同,不代表设置写入已经可上线;G11 的 `If-Match`、版本/CAS、权限刷新和结果未知仍须在独立批次建立门禁。在后端合同尚未落地的当前本地预览阶段,现有 fixture 映射暂不改写,也不能冒充生产 wire。 + +所在地只能从 required SaToken、非空 clientid 的 `GET /genealogy/app/region/search` typed `RegionSelectVo` 选择;同版删除旧公共 `/genealogy/region/search`,不能让 APP 在两条 owner 间漂移。`regionCode/label/selectable` 必填,code 使用共享词法 `GenealogyRegionCode`;`leaf` 只表示树导航,不能被客户端猜成“可提交”。页面展示 label、只提交 code,最终 POST 在业务事务中再次验证仍可选;不强制县、乡或 leaf,避免无依据排除省市级、历史地域或要求过细隐私位置。搜索加载、无结果、失败、迟到响应和失效选项分别表达,自由文本不得反推 code。 + +POST required SaToken、非空 clientid 和 `Idempotency-Key`。`GenealogyBootstrapOperationKey` 精确为 `gcb.{13位毫秒时间}.{22—43位 base64url 随机量}`,随机量必须来自至少 128 位 CSPRNG;`issuedAt` 从 key 提取,`acceptUntil=issuedAt+10 分钟`,一律以服务端时间判定,issuedAt 晚于 serverNow 5 分钟以上固定 400 `OPERATION_KEY_INVALID`。schema 同时用 `x-issued-at-source=KEY_EPOCH_MILLISECONDS`、`x-accept-window-seconds=600`、`x-max-future-skew-seconds=300` 机器锁定公式。仍在窗口内的首次请求先用短控制事务按 `(account,tenant,client,path,key)` 唯一 CAS 认领 `PENDING`、canonical digest、fencing lease 和 `resolveBy`;`resolveBy<=claimedAt+2 分钟`,并以 `x-resolve-from=CLAIMED_AT/x-resolve-sla-seconds=120` 锁定。相同作用域、key 与 canonical body 的已存在操作即使超过 acceptUntil 也可重放同一结果,不同 digest 固定 409 `IDEMPOTENCY_KEY_REUSED`;截止后仍不存在的 key 固定 409 `OPERATION_KEY_EXPIRED`,不得启动工作。 + +控制事务认领后,业务事务才原子执行配额竞争检查、家谱、OWNER、唯一一世始祖、READY 和 `SUCCEEDED` 回执;任一写点失败全部回滚,再以 fencing token CAS 成不可变 `FAILED_NO_COMMIT`。超出 lease/resolveBy 的 watchdog 也只能用同一 CAS 终结,旧 worker 失去 fencing 后不得提交,保证 PENDING 最迟 2 分钟内收敛。严格 200 只表示业务事务与成功回执均已提交;SUCCEEDED 防重记录至少覆盖实体生命周期,FAILED_NO_COMMIT 记录至少保留 30 天。 + +为避免把始祖姓名、生日和生平写入 UniApp 普通持久存储,崩溃恢复选择受鉴权 `GET /genealogy/app/genealogy-bootstrap-operations/{operationKey}`,不持久化完整 body。状态响应必须用带显式 mapping 的 discriminator `oneOf` 精确区分 `PENDING{resolveBy,retryAfterSeconds}`、`SUCCEEDED{result}`、`FAILED_NO_COMMIT`,三个 status 均是单值 string;PENDING 的 `retryAfterSeconds` 为 1—30,避免给所有 200 错加 Retry-After。`x-state-transitions` 只登记 `ABSENT→PENDING` 与 `PENDING→SUCCEEDED/FAILED_NO_COMMIT`,两个终态无出边,`x-terminal-immutable=true`;FAILED schema 同时固定 `x-domain-effects=NONE` 与 `x-quota-consumed=false`,机器保证家谱、OWNER、始祖和配额均未提交。SUCCEEDED 返回与 POST 相同严格回执。GET 只允许 operationKey path 与 clientid header、不得有 request body,是纯读且绝不创建墓碑或推进状态:acceptUntil 前无记录返回 typed 404 `BOOTSTRAP_OPERATION_NOT_AVAILABLE`,同时返回服务端 acceptUntil 与 Retry-After;截止后无记录则按 key 时间可计算地返回 FAILED_NO_COMMIT 而不写库,迟到 POST 仍永久拒绝。跨 account、tenant 或 client 查询统一 404且不泄漏存在性,禁止额外 403 分叉。操作记录不进 `/mine`、不占业务配额,也不返回请求 body 或人物 PII;GET 零写入和 FAILED 零领域提交最终仍须数据库观测测试证明,OpenAPI 描述与扩展不能冒充实现证据。 + +客户端唯一 coordinator 在最终按钮校验全部字段后才冻结 canonical snapshot、生成 key,并在 dispatch 前持久化 `{sessionEpoch,operationKey,startedAt}`;第一步 CTA 只写“下一步:录入首代”,绝不发网络请求。客户端本地校验失败不写 marker;请求对象创建失败且能证明零发出时清 marker。服务端在 claim 前返回的 400/401/403 均保证无 operation,清 marker,其中 401 还必须清会话并回登录,403 失败关闭。409 `IDEMPOTENCY_KEY_REUSED` 视为合同/篡改冲突,marker 转入 fatal/quarantined,不查询或安装该 key 的 status、不自动换 key;只有用户看到明确警告并显式放弃后才清 marker、重新开始。创建上限、过期 key 和 422 是确定未提交,可清 marker后修正。429 保持 marker、同 key/body,先查 status 再按 Retry-After 重试;500、网络、超时、408、发出后的取消、任意意外 2xx/3xx、畸形 200 或进程终止均是 unknown,保持 marker并查 status,绝不换 key。status 的截止前 404 保持 key,400 表示本地 marker 已损坏并安全清除,401 执行会话失效,429/500、网络、取消、意外状态和畸形 200 均保持并退避;只有 FAILED_NO_COMMIT 或可证明未认领的确定失败才清 marker。 + +当前进程 unknown 可用相同 key 与内存 snapshot 重放;进程重启只查状态,不从 `/mine` 按名字猜测,也不自动生成新 key。SUCCEEDED 的唯一次序固定为:持久化 committed receipt → 失效或定点更新 `/mine` 缓存 → 安装 genealogy context → 进入 G05;context 或导航失败只重试本地安装/导航,绝不重发创建。退出、换账号和 epoch 变化必须隔离旧操作;任何日志、路由、marker、遥测和崩溃记录都不得包含表单正文。 + +页面实施必须同步删除伪提交 owner 与 mock fixture mutation,覆盖地区加载、确定拒绝、fatal/quarantined、PENDING、结果未知、已提交待进入、上下文失败和导航失败。文本标签与 input 建立关系;访问预设使用真正 radio/`aria-checked`;按钮至少 44dp,提交中冻结字段和返回;错误具备 `aria-invalid/aria-describedby`、首错聚焦和持续状态播报;重复提醒与成功/放弃层复用具备焦点圈定和恢复的 `AppDialog`。当前源码仍有 clickable view、54rpx 目标、23rpx 选项、无关联错误和 UTC 日期上限,这些只登记为实施项;没有 MuMu 证据前不得宣称视觉或 TalkBack 上线。后端门禁先执行无第三方依赖的 `tests/openapi-yaml-json-parity-runtime-smoke.js`,以无损任意精度数字、严格 YAML mapping 分隔符和完整对象结构深比较 JSON/YAML,确保不安全整数差一也不能假绿,再由 `tests/g03-bootstrap-openapi-contract.ps1` 检查唯一 JSON 语义、组合 schema 内旧字段、同 scope 参数重复、根 PUT 精确白名单和结构化不可变终态;客户端门禁 `tests/g03-bootstrap-client-release-gate.ps1` 实际执行依赖注入的 marker/status 状态机测试,不再用 helper 名称顺序冒充行为证据。两项只是 G03 自身门禁;真实开放还必须同时通过家谱工作区读取门禁、聚焦测试全量回归与 MuMu 原生交互/无障碍验收,任一红灯都不得开放真实创建。 + +## 二十四、当前精确执行顺序 后续不再按页面样式迁移重做,而按以下独立阶段执行: -1. 导航栈语义统一:先测试和共享所有者,再按认证、G、T、F、R、N/M 小批迁移,每批 MuMu 闭环。 -2. T01 大规模世系树:先向后端提交新图合同,等待 Apifox 更新并验证,再按图合同、布局、Scene、Canvas、页面交互分批实施。 -3. 短信验证码完整状态机。 -4. 跨页面领域数据持久化。 -5. 全局文字层级与无障碍第二轮。 +1. 导航栈语义统一:静态任务 1—10 已完成,MuMu 流程矩阵待执行。 +2. T01 大规模世系树:设计与 OpenAPI 红灯已完成;等待后端新图合同后按规范化、布局、Scene、Canvas 和页面交互分批实施。 +3. TAC 认证:客户端与静态/纯运行时门禁已完成;等待后端关闭 `API-AUTH-TAC-001`—`004`,随后执行真实环境与 Android 发布门禁。 +4. 领域上下文基础与 M07 真实反馈客户端已完成;家谱工作区三人审查与 OpenAPI 红灯已完成,等待后端关闭 `API-GENEALOGY-WORKSPACE-001`—`003` 后再接 G01/G05。 +5. G03 原子 bootstrap 三人审查与 OpenAPI 红灯已完成;等待后端关闭 `API-G03-001`—`005` 后,再实现严格 coordinator、无 PII 状态恢复、地区选择、context/G05/G01 闭环和独立 MuMu 矩阵。 +6. M06 帮助三人审查与 list-only OpenAPI 红灯已完成;等待后端关闭 `API-M06-001`—`003` 后再接严格 adapter。等待期间继续下一个无依赖只读域,不与验证码、T01 或支付混合。 +7. M01/M02/M03 个人资料读取三人审查与 OpenAPI 红灯已完成;等待后端关闭 `API-PROFILE-READ-001`—`003` 后再接掩码 adapter,不与资料写入混合。 +8. 通知读取和已读写入已分别完成三人审查与 OpenAPI 红灯;后端先关闭读取合同后实现无 ID 内存快照,再在独立写批次私有迁移字符串 ID、删除伪本地读状态并验证并发收敛。 +9. M02 资料写入三人审查与 OpenAPI 红灯已完成;等待读取与 `API-PROFILE-UPDATE-001`—`004` 同时关闭后,再分 normalizer、API 和页面状态实现。 +10. M10 当前设备退出三人审查与 OpenAPI 红灯已完成;等待后端关闭 `API-LOGOUT-001`—`003` 后,再实现 session epoch、logoutCoordinator 和 A01 一次性状态。 +11. M04 登录态改密与四条密码 wire 三人审查、OpenAPI 红灯已完成;等待后端关闭 `API-PASSWORD-001`—`005` 后,原子迁移共享策略、MD5 消费者、session marker、页面状态机与全设备撤销。 +12. M05 手机号换绑与全活动 OTP 三人审查、OpenAPI 红灯已完成;等待后端关闭 `API-PHONE-001`—`005` 且 M04/认证前置门禁通过后,再原子迁移六位码、专用受保护发码、最终 PUT 与 credential marker。等待期间转向 G/F/R,不混入本批。 +13. 全局文字层级与无障碍第二轮。 +14. 生产配置、隐私权限、可观测性、构建发布、升级回滚与 MuMu 全流程终审。 -T01 必需的线性目录和 44dp 控件随 T01 一起完成;不借此提前改造全项目文字体系。任何阶段完成前都不开始下一阶段的业务代码。 +T01 必需的线性目录和 44dp 控件随 T01 一起完成;认证浮层的壳层无障碍不扩张为全项目已通过。外部门禁阻塞时继续无依赖批次,但任何真实成功、接口兼容或视觉通过都必须有对应证据。 diff --git a/docs/接口与页面映射总表.md b/docs/接口与页面映射总表.md index 1859d09..c26e08a 100644 --- a/docs/接口与页面映射总表.md +++ b/docs/接口与页面映射总表.md @@ -1,20 +1,20 @@ # 接口与页面映射总表 > 更新日期:2026-07-22 -> 阶段状态:阶段 0 已完成;导航栈与 T01 专项设计已完成三人终审,业务代码尚未实施 -> 接口状态:已完成 A04 注册响应和 T01 世系图专项核对;其余 OpenAPI 操作仍待逐项审查 +> 阶段状态:阶段 0、导航任务 1—10、A01/A04/A05 TAC 客户端、领域上下文基础与 M07 反馈客户端已完成;家谱工作区、G03 原子创建、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 密码凭证与 M05 手机号换绑三人审查及 OpenAPI 红灯已完成;T01、TAC、工作区、G03、帮助、profile、通知、logout、password 与 phone-change 后端接口门禁当前红灯;MuMu 原生矩阵待执行 +> 接口状态:已完成 A 系列认证/TAC、G 系列第一轮、G01/G05 工作区专项与 G03 原子创建专项、F 系列当前写接口边界、M06 帮助、M07 反馈、M01/M02/M03 个人资料读取、M02 资料写入、N01/N02/M01/G01 通知读写、M10 当前设备退出、M04/四条密码 wire、M05/全活动 OTP wire、T01 世系图及 2026-07-22 新线上 OpenAPI 差异核对;其余操作仍待逐项审查 ## 一、权威边界 本文件是活动页面、业务目标、进入方式、返回或完成目标、适用状态与接口归属关系的唯一总表。`pages.json` 是活动路由的唯一注册清单;两者必须同轮更新并保持精确一致。 -接口的唯一源头是后端维护的 Apifox 项目。根目录 `APP.openapi.json` 用于自动分析,`APP.openapi.yaml` 用于人工阅读与跨工具导入。阶段 0 只保护了两份用户导出;当前仅对导航依赖、A04 注册响应和 T01 世系图做了专项核对,不能把专项结论扩张为 153 个操作均已审查。 +接口的唯一编辑源是后端维护的 Apifox 项目。根目录 `APP.openapi.json` 用于离线自动分析,`APP.openapi.yaml` 用于人工阅读与跨工具导入;部署地址的 `/v3/api-docs` 只提供当前线上实现证据。阶段 0 保护的双导出是 OpenAPI 3.0.1、112 路径、153 操作的旧快照;2026-07-22 21:52 只读核对 `https://backend-api.ddxcjp.cn/v3/api-docs` 得到 OpenAPI 3.1.0、722 路径、858 操作、507 模型。发现差异时必须由后端生成同版本双导出,不能手工覆盖受保护文件,也不能把专项结论扩张为全部线上操作均已审查。 当前边界如下: - `pages.json` 注册 `52` 条活动路由;A02 已并入 A01,A06 保留源码但不属于活动路由。 -- 全项目响应式迁移与统一扫描已经完成,实际 `66/66` 个 Vue 文件均在覆盖清单中。 -- A 系列及 G01—G10 已由用户在 MuMu 中人工确认;G11、G12 以及 T、F、R、N、M 页面已由上一位代理按用户授权在 MuMu 中逐页、逐状态审核并修复。 +- 全项目响应式迁移与统一扫描已经完成;退役通用页面和零消费者旧表单删除后,实际 `64/64` 个 Vue 文件均在覆盖清单中。 +- 任务 5 实施前,A 系列及 G01—G10 曾由用户在 MuMu 中人工确认,G11、G12 以及 T、F、R、N、M 页面也曾逐页、逐状态审核并修复;这只是历史视觉基线,不覆盖任务 5—6 后的 G/T 动作、文案、权限和布局变更。任务 4—6 的当前代码仍须按实施计划重新完成 MuMu 流程矩阵。 - 上述视觉结论是当前继续工作的基线,不等于真实接口、持久化、系统权限、真实短信、微信、支付或跨页数据闭环已经完成。 - 后续发现明确、可复现的样式、交互或业务问题时可以重新打开页面;不得因为旧结论写着“通过”就忽略证据。 @@ -23,30 +23,30 @@ ### 2.1 账户、启动与登录 - APP 不设游客模式。首次打开、无有效凭证或凭证过期时进入 A01;有效登录态进入 G01。 -- A01 是唯一活动登录页,承载密码登录、短信登录、注册、找回密码、协议入口和微信登录入口。登录成功统一到 G01,不直接恢复上次浏览的深层页面。 -- A04 注册成功的目标是建立登录态后进入 G01。当前 OpenAPI 已确认 `POST /genealogy/app/auth/register` 的 `200` 响应复用 `LoginResult`,其 `LoginVo` 可返回 `token/accessToken/tokenValue`;正式接入应保存有效会话后直接进入 G01,不让用户重复登录。行为验证与短信发送时机仍在后续短信状态机阶段核对。 +- A01 是唯一活动登录页,承载短信登录、受后端合同阻塞的密码登录入口、注册、找回密码、协议入口和尚未接入的微信登录入口。当前默认短信登录;密码登录在后端能够强制消费 TAC 票据前保持可见但不可用。登录成功统一到 G01,不直接恢复上次浏览的深层页面。 +- A04 注册成功的目标是建立登录态后进入 G01。2026-07-22 新线上 OpenAPI 已确认 `POST /genealogy/app/auth/register` 与两种登录操作统一返回 `RAppLoginVo`,`data` 引用 `AppLoginVo`,会话字段为 `access_token`;`utils/api.js` 只读取这一当前字段。正式接入应保存有效会话后直接进入 G01,不让用户重复登录,也不为受保护旧快照保留并行兼容路径。行为验证与短信发送时机仍在后续短信状态机阶段核对。 - A05 重设成功后不自动登录:返回 A01,保留合规的手机号信息,切换密码方式、聚焦密码框并让用户使用新密码登录。 -- A04、A05、M04 共用 `utils/validation.js` 的唯一密码策略:密码长度 `8–32` 位且必须同时包含字母和数字;M04 的新密码还不得与旧密码相同。页面不得各自复制或放宽该规则。 +- A04、A05、M04 当前预览仍共用 `utils/validation.js` 的 `8–32+字母数字` 旧策略;Task33 已证明该规则只在客户端且无法作为生产 owner。门禁通过后必须与 A01/A04/A05/M04 的 MD5 wire 同批原子迁移为服务端权威的 15—64 Unicode code point、NFC、允许空格与 Unicode、无组成规则,并同步替换本地唯一 owner、页面文案和测试;不得只改 M04 或保留双轨。 - A01 未勾选协议时在协议区域就近高亮并显示错误,不弹原生提示、不跳页;用户勾选后立即清除错误状态。 -- A01、A04、A05 最终共用真实行为验证能力,但触发时机必须按业务区分:A01 在本地校验通过后、登录请求前触发;A05 在请求发送短信验证码前触发,最终重设提交不重复验证;A04 当前临时在完整注册表单提交后触发,后端提供短信注册接口时必须迁移到发送短信前,并删除旧触发路径。关闭或验证失败时保留表单内容,不得形成两套并行验证路径。 -- 短信验证码需要完整状态机:可发送、发送中、倒计时、发送失败、限流、前后台恢复和到期。该状态机在导航栈统一之后单独评估,不在阶段 0 实施。 +- A01、A04、A05 已接入同一个 `TacVerification` 和 `static/tac/`,不能把本地滑动成功冒充服务端验证。A01 短信登录使用 `APP_SMS_LOGIN`,A04 使用 `APP_REGISTER`,A05 使用 `APP_FORGOT_PASSWORD`;三者都先从 `/captcha/require` 取得严格 provider/type 合同,再由 `/captcha/challenge` 和 `/captcha/verify` 换取 `validToken`,携票调用 `POST /genealogy/app/auth/sms/code`。验证只发生在发短信前,注册或重设提交不重复验证;手机号改变会作废旧上下文,关闭、失败或离页保留表单但不得继续发送。密码登录体尚无票据字段或其他强制绑定证据,因此密码登录入口保持不可用。 +- A01/A04/A05 已实现可发送、发送中、60 秒倒计时、失败重试、手机号变更失效、验证码到期和重复发送保护;短信码精确为 4 位。认证 HTTP 只接受 HTTP 200 严格 envelope,统一 15 秒超时,Android 返回或页面卸载会中止 RequestTask,迟到回调不得改变已离开页面。真实限流、前后台剩余时间恢复和多设备重放仍需后端集成与 MuMu 证明。 - 凭证过期先回 A01,再显示项目自定义的单按钮信息弹窗并聚焦登录表单;主动退出清除凭证,但可以保留用户上次选择的密码或短信登录方式,不保存密码。 ### 2.2 家谱上下文与加入、创建 - 一个账号允许创建或加入多个家谱。G01 列表按“我创建的”“我加入的”“加入申请”分组,顶部只表示当前选中项。 -- G01 下方列表卡只切换顶部当前项,不直接进入详情;顶部可用家谱卡才进入 G05。默认选择顺序为:本机保存的上次有效 ID、第一个可操作家谱、列表第一项。只保存 ID,不复制整份家谱数据。 +- G01 顶部当前家谱卡用于打开切换层;下方可用家谱卡直接进入 G05。只有导航成功后才同步顶部选中项和本机当前家谱 ID,失败或并发点击不得把页面与持久上下文串到不同家谱。首次且没有历史选择或失效标记时可确定使用首个可用家谱;调用方给定的新列表不再包含历史 ID,或显式目标不可用时,必须清空、持久标记并要求用户重选,跨重载也禁止静默回退到另一家谱。只保存词法字符串 ID,不复制整份家谱数据;真实撤权检测等待 workspace 接入 onShow/事件刷新。 - 审核中记录只展示进度和“撤回申请”,顶部卡不可进入家谱;被拒绝记录展示原因和“修改后重新提交”,进入 G08 而不是 G05;已退出或被移除记录展示原因和“重新申请”,不得访问原家谱内容。 -- 只有可用家谱能够成为全局家谱上下文;审核中、被拒绝、已退出、被移除或待录入始祖的记录不得覆盖最近一个可用上下文。 +- 只有 READY 且当前账号 `canView=true` 的家谱能够成为全局家谱上下文;审核中、被拒绝、已退出或被移除的记录不得覆盖最近一个可用上下文。G03 新合同不再产生“待录入始祖”业务状态;历史半成品如真实存在应由后端迁移/隔离,不能继续成为客户端正式状态。 - 没有可用家谱时,相关页面不得展示上一个失效家谱的缓存内容,应引导用户搜索家谱、使用邀请码或创建家谱。 - G01 空态的主次顺序为搜索家谱、邀请码加入、创建家谱;非空列表保留“添加家谱”底部弹层。所有者可见世系、成员、字辈诗、申请审核四个快捷入口,普通成员不显示申请审核。 -- G06 同时承载搜索与邀请码定位。搜索申请进入审核;邀请码目标是直接加入,不应生成审核记录。邀请码验证和直接加入目前仍是待核对的接口依赖。 +- G06 同时承载搜索与邀请码定位。产品目标仍是“邀请码直接加入且不生成审核记录”,但当前 OpenAPI 没有邀请码校验或直接加入操作,因此本地流程只展示目标并进入 G08 填写确认,完成后无结果返回 G01,不能选中家谱或声称已经加入。搜索申请进入审核;真实邀请码终点必须等后端合同补齐后替换本地预览。 - G06 结果至少需要谱名、姓氏、地区、堂号、所属上级谱、当前支系、管理者或认证信息、成员规模和最近更新时间,以区分同名家谱和支系;未加入、已加入、审核中、被拒绝、已退出或移除、我创建的六种关系各自只出现一个明确动作。 - G08 当前用真实姓名、与已知长辈的文字关系和补充说明表达申请;用户可见示例统一使用“某某某堂侄”等通用占位,不出现具体姓名。后端提供结构化参照成员或关系字段后,应以新合同完整替换文字关系旧路径。 -- G03 当前只创建独立家谱。创建完成但始祖未录入时,G01 必须保留“待录入始祖”记录;完成始祖后进入 G05,由用户主动进入 T01,不自动越过家谱总览。 +- G03 当前在同一页面实例内依次完成“创建家谱”和“录入始祖”,不接收 `step` 或 `genealogyId` 路由参数;门禁前本地成功无业务结果进入 G05 明确预览态。生产目标不允许创建中断成空谱:第一步零网络写,最终按钮一次原子创建家谱、OWNER 与唯一始祖;进程终止后按 operationKey 查询服务端操作状态,不恢复已删除的路由步骤合同,也不让 G01 承担半成品恢复卡。 - G05 同一路由区分公开预览与成员视图。公开预览不得闪现成员隐私或管理入口;所有者和普通成员采用最小权限模型,最终权限以接口合同为准。 - G05 首屏按身份确认、来源确认、可信度确认三层组织信息;世系是次级入口,不自动抢占首次进入流程。 -- G11 只维护当前可解释的名称、公开范围和访问说明;公开范围只有“仅成员可见”和“公开可申请”两个当前枚举,不虚构“转让管理员”等后端尚未确认的能力。G12 字辈保存必须保证代次连续;发现历史缺口时停止向后推导并要求明确处理,不能静默错位。 +- G11 只维护当前可解释的名称、访问预设和家谱简介;门禁前本地 fixture 的“仅成员可见”“公开可申请”仍由 `utils/genealogy-contracts.js` 映射旧 `visibility/joinMode` 并对未知组合失败关闭。任务 35 已选定后端同版迁移为 APP 读、建、改唯一 `GenealogyAccessPreset`,落地时必须删除旧 pair 与客户端数字映射,不双读;不虚构“转让管理员”等能力。G12 本地批量预览固定按完整序列处理,支持最多 500 代、单代 50 字符和接口声明的分隔符;发现 ACTIVE 世代缺口时停止保存,不能静默错位。 ### 2.3 页面状态与请求结果 @@ -61,13 +61,13 @@ ### 2.4 导航、弹层与流程终点 -- 当前三个业务根页面是 G01“家谱”、F01“家族”和 M01“我的”,A01 是认证根页;书面栈语义已经在导航设计中收口,业务代码仍须按测试先行和 MuMu 矩阵实施验证。 +- 当前三个业务根页面是 G01“家谱”、F01“家族”和 M01“我的”,A01 是认证根页;导航栈语义统一已经完成,路由注册表、导航网关、共享页头、自定义底栏、认证、G、T、F、R、N/M 系列活动页均按测试先行落地,退役通用页面、临时页面目录和最后一个零消费者旧表单组件均已删除。源码导航扫描为 `MIGRATION-DEBT=0`;认证至 N/M 的 MuMu 原生流程复核仍待执行。 - 返回、取消、完成和重复进入必须分别验证。页面完成后不得把已经结束的旧流程继续留在栈中,也不得用 `navigateBack` 猜测一个可能不存在的返回目标。 - 普通底部弹层可由遮罩或 Android 返回键关闭;存在未保存输入时先确认是否放弃。确认弹窗的返回键等同取消;任何取消都不得被记录为成功。 - 弹窗高度只允许使用视口 `max-height` 和内部滚动;普通页面内容高度由内容决定,不为单一设备压缩字号、行高或控件尺寸。 - 用户可见反馈使用项目自定义组件,不新增原生 UniApp Toast、Modal、Loading 或 ActionSheet 作为正式体验。 - 轻提示、底部弹层、居中确认、结果说明和危险操作按决策成本分级。不可逆操作必须说明后果并二次确认;普通操作不滥用确认。 -- 产品合同已经定案为“邀请码直接加入且不生成审核记录”:只有邀请码校验成功才执行直接加入。M08 当前“邀请码加入后需要管理员审核”的旧文案属于待删除实现债务,导航任务 9 必须同步删除,不能保留审核与直接加入两套分支;邀请码校验和直接加入接口仍按对应业务阶段向后端核对。 +- 产品合同已经定案为“邀请码直接加入且不生成审核记录”:只有未来真实邀请码校验与直接加入接口成功才允许建立成员关系。当前 G06/G08 只做本地流程预览并明确未提交服务器;M08 的旧审核分支、硬编码邀请码、复制和海报伪能力已经删除,在真实邀请码签发与校验合同落地前保持不可用。 ### 2.5 三个根页面与主要流程 @@ -93,20 +93,20 @@ M01 我的 ### 2.6 当前本地数据与路由参数边界 -当前 `52` 条活动页面仍使用页面内本地状态、fixture 或 mock 数据;活动页面没有 `appApi` 消费者。`utils/api.js` 的存在不能被解释为已经接入真实接口。以下只记录页面源码当前主动读取的查询参数,供阶段 1 导航栈与接口审查核对;上游传入但页面未读取的参数属于待审债务,不能写成有效合同。 +当前 `52` 条活动页面除 A01/A04/A05 的认证调用外,仍使用页面内本地状态、fixture 或 mock 数据;其他活动页面没有 `appApi` 消费者。认证调用已经对准真实端点,但 `runtimeConfig.mode` 固定为 `mock` 并失败关闭,不能解释为真实后端已经联通。以下只记录页面源码当前主动读取的查询参数;上游传入但页面未读取的参数属于待审债务,不能写成有效合同。 | 页面 | 当前主动读取的查询参数 | | --- | --- | | A01、A04、A05 | 无 | | G01 | `genealogyId`、`state` | -| G03 | `step`、`genealogyId` | -| G05 | `genealogyId`、`mode`、`role`、`state`、`genealogyName` | +| G03 | 无 | +| G05 | `genealogyId`、`state` | | G06 | `mode`、`state` | -| G08 | `genealogyId`、`source`、`previous`、`state`、`genealogyName` | +| G08 | `genealogyId`、`source`、`state` | | G09 | `state`、`status` | | G10 | `genealogyId`、`state` | | G11 | `genealogyId`、`state` | -| G12 | `genealogyId`、`startGeneration`、`currentGeneration`、`state` | +| G12 | `genealogyId`、`state` | | T01 | `genealogyId`、`state`、`selectedId` | | T03 | `genealogyId`、`personId`、`state` | | T04 | `genealogyId`、`personId`、`mode`、`state` | @@ -114,30 +114,32 @@ M01 我的 | T07 | `genealogyId`、`state` | | T08 | `genealogyId`、`personId`、`state` | | F01 | `genealogyId`、`state` | -| F02 | `state` | -| F03 | `feedId`、`commentResult`、`state` | -| F04 | `count`、`state` | -| F05 | `articleId`、`state` | -| F06 | `articleId`、`mode`、`state` | -| F07 | `count`、`state` | -| F08、F09 | `albumId`、`state` | -| F10 | 无 | -| R01 | `state` | -| R02 | `mode`、`personId`、`state` | -| R03 | `count`、`state` | -| R04 | `giftId`、`mode`、`saveResult`、`state` | -| R05 | `count`、`state` | -| R06 | `ritualId`、`state` | -| R07 | `ritualId`、`mode`、`saveResult` | -| R08、R09 | `personName`、`state` | -| R10、R11 | `state` | +| F02 | `genealogyId`、`state` | +| F03 | `genealogyId`、`feedId`、`state` | +| F04 | `genealogyId`、`state` | +| F05 | `genealogyId`、`articleId`、`state` | +| F06 | `genealogyId`、`articleId`、`mode`、`state` | +| F07 | `genealogyId`、`state` | +| F08、F09 | `genealogyId`、`albumId`、`state` | +| F10 | `genealogyId` | +| R01 | `genealogyId`、`state` | +| R02 | `genealogyId`、`mode`、`personId`、`state` | +| R03 | `genealogyId`、`state` | +| R04 | `genealogyId`、`mode`、`relativeId`、`state` | +| R05 | `genealogyId`、`state` | +| R06 | `genealogyId`、`ceremonyId`、`state` | +| R07 | `genealogyId`、`mode`、`ceremonyId`、`state` | +| R08、R09 | `genealogyId`、`personId`、`state` | +| R10、R11 | `genealogyId`、`state` | | N01 | `genealogyId`、`state` | | N02 | `id`、`state` | | M01 | `state` | | M02—M08、M10 | 无 | | M09 | `state` | -`state`、`count`、`saveResult` 等参数目前主要用于本地状态审查和压力测试,不代表后端请求字段。导航注册表已经决定删除 G03 的 `step`、G05/G08 的 `genealogyName` 和 G08 的 `previous`:页内步骤留在页面状态,名称按 `genealogyId` 从现有 fixture 或后续领域数据取得,来源只由真实栈与受验证 `sourceKey` 表达。其余参数仍须在对应业务阶段判断为真实输入、页内状态、领域数据或删除项,不得把调试参数固化成接口合同。 +`state` 参数目前只用于直接加载页面时的本地状态审查,不代表后端请求字段;导航网关不注册也不会生成这个展示钩子。R 系列的 `count/saveResult/personName/giftId/ritualId` 已全部退役,人物名称和实体资料只能由受校验的复合身份从唯一只读 owner 取得。G03 的 `step/genealogyId`、G05 的 `mode/role/genealogyName`、G08 的 `previous/genealogyName` 以及 G12 的 `startGeneration/currentGeneration` 已从页面和注册表删除:页内步骤留在页面状态,名称按词法 `genealogyId` 从唯一 fixture 或后续领域数据取得,来源只由真实栈与受验证 `sourceKey` 表达。T03 初始 `personId` 是不可变宿主页路由身份,亲属浏览只改变页内活动成员和轨迹;因此当前 T05 本地预览必须以 `goBack()` 回到原实例,不得用活动成员重写 T03 URL。其余参数仍须在对应业务阶段判断为真实输入、页内状态、领域数据或删除项,不得把调试参数固化成接口合同。 + +T01/T03—T08 当前共用 `data/mock.js` 的唯一成员夹具 owner:列表查询必须传 `genealogyId`,单成员查询必须同时传 `genealogyId/personId`,返回值与嵌套亲属均为快照。错误家谱下的已知成员、未知成员或缺失必填身份不得回退到 1001、首位成员或“待核实成员”;T04 只有 `mode=first` 且 `personId` 为空时可以建立首位成员草稿。该夹具只支撑当前本地设计流程,不代表后端字段已经完整;任务 18 接入真实接口时必须删除临时 owner 与选择器,而不是并存第二份成员合同。 ### 2.7 全局非功能门槛 @@ -147,66 +149,135 @@ M01 我的 - 同一详情连续进入和退出 `20` 次,并快速切换根页面、重复开关弹层;不得出现白屏、串状态、重复堆栈、残留遮罩或逐次变慢。 - Android 性能以约 `4GB` 内存的中低端设备为底线,验证启动、键盘、长列表、图片解码、页面切换和系统返回手势。 - 页面离开时清理本页创建的定时器、监听器、上传任务和动画状态;连续使用不得积累重复请求或实例。 -- 当前尚未实施的五个独立阶段依次为:导航栈语义统一、T01 大规模世系树、短信验证码完整状态机、跨页面领域数据持久化、全局文字层级和无障碍第二轮。不得一次混合实施。 +- 当前导航阶段已完成任务 1—10 的共享基础、组件、退役入口、认证、G、T、F、R、N/M 系列静态迁移和零债务门禁;A01/A04/A05 的 TAC 客户端、领域上下文与 M07 反馈客户端也已完成,MuMu 原生矩阵尚未执行。T01、认证与家谱工作区后端门禁并行等待外部合同关闭;跨页面领域数据持久化以工作区门禁为前置,本地先按无依赖业务域、全局文字层级和无障碍第二轮逐批推进,不得一次混合实施。 + +### 2.8 G 系列第一轮接口差距账本 + +本轮已逐页核对 G01、G03、G05、G06、G08—G12 与受保护双导出,并对照新线上 OpenAPI 做了第一轮差异检查;随后任务 26 又对 G01/G05 做了三人反向质询、部署探测与失败门禁,但没有在 schema owner 未定时把页面接到 `appApi`。所有 G 页面仍是本地交互预览,页面显示角色也只是 fixture 的最小权限投影,不代表服务端授权。以下问题在真实接入前必须由接口适配合同或后端同版本新导出关闭: + +- 公共边界:受保护旧双导出的家谱详情、列表和申请列表多为通用 `Object/ListResult`;新线上虽改为 `RListAppGenealogyVo/RAppGenealogyVo/RListGenealogyJoinApplyVo` 等类型化响应,但三个工作区模型均无 `required`。`AppGenealogyVo.genealogyId` 仍是 JSON `integer/int64`,合法最大值在 JavaScript 中不可逆失真;`roleType/status/memberStatus` 无 enum,且没有可直接用于 current context 的 `canView`。首批工作区只要求 `genealogyId/genealogyName/canView/canManage/canEditContent/roleType` 的最小闭包,不要求 22 个字段全部必填。`GET /genealogy/app/genealogies/quota` 虽已返回 `GenealogyQuotaVo`,它属于后续创建/加入写流程,不混入 G01/G05 只读批次。申请、用户、字辈等其他 int64 只在各自实际消费批次逐项关闭,不能用解析后 `String()` 冒充无损。 +- G03:任务 35 已完成三人专项审查。受保护旧双导出的 `GenealogyCreateBody` 与线上 `AppGenealogyCreateBody` 都只建谱,页面又缺可信 `regionCode`,通用人物 POST 不能保证唯一始祖或两写原子性。生产目标固定为最终按钮一次 `AppGenealogyBootstrapBody` 创建谱+OWNER+一世始祖+READY,使用 `/genealogy/app/region/search`、`GenealogyRegionCode`、`RegionSelectVo.selectable`、统一 `GenealogyAccessPreset`、词法 ID、Idempotency-Key 与无 PII operation-status;当前后端 `G03-BOOTSTRAP-OPENAPI-CONTRACT BLOCKED` 和客户端 `G03-BOOTSTRAP-CLIENT-RELEASE BLOCKED` 均为独立发布红灯,见 5.12。 +- G01/G05 只读工作区:`/mine` 是唯一可访问集合 owner,`/{genealogyId}/overview` 是 G05 唯一读取 owner,不同时调用语义重复的 `/{genealogyId}`。G01 必须在 `onShow` 或失效事件中取消旧请求并成功取得严格列表后才 reconcile;网络/5xx 不写撤权 tombstone。G05 在新请求前清旧数据并取消迟到响应,远端失败不回退 fixture。线上没有当前首屏用于确认来源和可信度的 `source/manager/certification/ancestorName/parentName/branchName/updatedAt/activeCount`;首批可以诚实隐藏或降级这些可选展示,若产品坚持保留则另补后端合同,不能让 fixture 成为真实详情。 +- G06:新线上公开搜索已经返回类型化家谱对象,但仍没有显式 `canApply`、六类关系字典、邀请码校验、邀请码目标解析或直接加入端点;未来必须由服务端返回可信的 `canJoinByInvite` 或等价结果,搜索申请资格不能代替邀请码资格,也不能信任 `source=invite` 查询参数。 +- G08:`GenealogyJoinApplyBody` 提供 `applicantName/phone/relationDesc/applyReason/inviterUserId`,当前页面的 `realName/relation/message` 仍需显式映射、手机号来源和必填规则确认;接口没有邀请码票据字段。搜索来源本地完成仅进入 G09 预览,邀请码来源本地完成仅回 G01,二者都不建立成员关系。 +- G09/G10:新线上我的申请和待审核列表已返回类型化申请行,但申请状态仍缺正式 enum/字典;真实撤回和审核必须携带服务端词法 `applyId`。线上审核体为 `{status,auditRemark}`,`status` 必填且匹配 `[12]`(1 通过、2 拒绝),`auditRemark` 最长 500;`utils/api.js` 已同步严格校验,旧 `{approved}` 路径已删除。当前本地 `LOCAL_WITHDRAWN` 和审核预览仍不得当成服务端状态。 +- G11:受保护旧双导出的 `GenealogyUpdateBody` 与线上 `AppGenealogyUpdateBody` 都有 `genealogyName/intro/visibility/joinMode`,页面已删除不存在的 `accessNote`。`utils/genealogy-contracts.js` 当前只为 fixture 暂时映射 `2/0` 与 `1/1`;任务 35 已把远端 owner 收紧为闭合 `AppGenealogySettingsUpdateBody` 的 `genealogyName/intro/accessPreset`,并要求 `AppGenealogyVo` 同用枚举。新合同落地时原子删除旧 DTO、pair、映射和对应旧测试,不保留邀请码 mode 的暗中兼容,见 5.12。 +- G12:旧双导出的批量体为 `{poemText,disableMissing}`;输入总长上限 26000 字符、单代最多 50 字符、单批最多 500 代,停用遗漏后续记录时不得删除历史。新线上 `GenerationPoemBatchBody` 还暴露 `genealogyId`,说明为“由路径参数写入,客户端不得自行指定”,并丢失了旧导出中的单代/单批说明,必须由后端同版本导出确认哪组约束仍有效。客户端解析、状态 `0/1`、词法 `poemId` 保留、重复世代失败关闭、完整 ACTIVE 序列检查和 50 条分批渲染已由共享 owner 与 Node 冒烟锁定;页面运行时合同还覆盖 `disableMissing` 保留/停用/取消恢复及 500×50 个补充平面字符的最大合法包络。旧导出文字列“空格”等分隔符但示例含换行,当前解析器把 Unicode whitespace 视作分隔符;Tab/NBSP 是否属于正式合同仍须后端澄清。普通列表只应给可查看者返回正常状态,维护列表只给内容编辑者返回正常与停用记录。接口仍没有 `startGeneration`,且未明确批次首项对应哪一世;真实接入必须以服务端 preview items 为差异真相,在后端澄清批次起点前不得发送本地合并结果。 + +### 2.9 新线上 OpenAPI 差异与运行边界 + +- 新运行基址为 `https://backend-api.ddxcjp.cn`,已经由 `utils/config.js` 唯一持有且无尾斜杠;当前仍保持 `mode: 'mock'`,不会让尚未接完的页面误打真实服务。线上文档的 `servers` 却仍生成 `http://backend-api.ddxcjp.cn`;客户端配置只能使用 HTTPS,后端需修正文档声明,不能让 H5 产生混合内容风险。 +- 本地 112 条路径中 109 条仍在线;旧 `/genealogy/app/files/reference`、`/genealogy/app/files/upload`、`/genealogy/pc/files/upload` 三条当前不在线,线上另有 613 条路径。任何上传与文件引用实现都必须先按新线上合同重新审查。 +- 线上把本地若干 `Genealogy*Body/View` 改为 `AppGenealogy*Body/Vo`,部分字段说明、响应包装和 operationId 也已漂移;现有离线测试只能证明受保护双导出内部的 G 系列快照,没有证明线上与旧快照一致。 +- 行为验证服务已能返回 `validToken`,短信发送体也强制接收它;但密码登录体没有票据字段。A01 密码登录、注册、忘记密码三条流程共用 TAC 是产品硬要求,后端必须明确各自 `sceneCode`、provider/captchaType、票据一次性消费与过期/重放/限流规则,以及密码登录如何强制校验。 +- 新线上文档的 `722` 条路径中没有任何 `/genealogy/app/v2/`,`507` 个 schema 中没有 `LineageGraphWindow/LineageOverview/LineageLocator` 或 `schemaVersion/treeVersion/familyUnits/edges/rootVisibility/ancestorPathSegments/affected*Ids` 等辨识字段,`LINEAGE_QUERY_INVALID/LINEAGE_FOCUS_NOT_AVAILABLE/TREE_VERSION_CHANGED/RELATIONSHIP_PATCH_EMPTY` 也全部不存在。`tests/lineage-openapi-contract.ps1` 已对受保护 JSON/YAML 建立聚合红灯并证明四条目标操作和三个固定根模型同时缺失,因此不能解除 `API-T01-001` 门禁,也不能开始任务 12。 +- 已验证 `http://localhost:5173` 对 `/captcha/challenge` 的预检允许 `POST`、`content-type`、`clientid` 和 credentials;正式 H5 域名、App 原生请求、错误码与限流仍须分别验证,不能用本次预检替代上线验收。 + +### 2.10 认证与 TAC 后端缺口账本 + +认证客户端批次已经完成,但 `runtimeConfig.mode` 继续固定为 `mock`;下列问题关闭、后端提供同版本双导出并完成真实联调前,客户端不得切换远端或宣称登录注册可上线: + +- `API-AUTH-TAC-001`:密码登录体尚无票据字段。后端必须让 `POST /genealogy/app/auth/login` 强制消费与短信发送相同安全语义、绑定 `APP_PASSWORD_LOGIN + tenant + client + canonical phone` 的短时单次票据;在此之前 A01 密码登录入口保持不可用,不能仅由客户端先展示滑块。 +- `API-AUTH-TAC-002`:2026-07-22 对真实 `APP_REGISTER` 请求只读联调时,`POST /captcha/challenge` 返回 HTTP 500 且响应体为空。后端必须修复并提供成功、无效场景、过期、限流和服务不可用的稳定错误 envelope;不得用客户端重试掩盖空 500。 +- `API-AUTH-TAC-003`:当前 `VerificationCheckBody` 未将 `providerCode/captchaType/payload` 全部声明为必填,根对象与 TianAi/SystemImage payload 也未关闭额外字段,且缺少以 evidence/provider 为 discriminator 的 `oneOf`。后端必须建立关闭额外字段的严格分支;客户端提交的 provider/type 不能替代 challenge 的服务端所有权。`tests/auth-tac-openapi-contract.ps1` 当前聚合六项结构缺口并输出 `AUTH-TAC-OPENAPI-CONTRACT BLOCKED`。 +- `API-AUTH-TAC-004`:验证码中心必须是唯一 owner。`/captcha/require` 返回服务端绑定 `tenant/client/scene/canonical subject/riskPolicyVersion` 的 `verificationSessionId`、允许方法和短时有效期;`required=false` 也必须直接返回可供短信端点原子消费的一次性 `validToken`。同一 session 只允许一个活动 challenge,刷新或切换方法立即作废旧题但不清零失败计数;`/captcha/verify` 只在服务端验证 evidence 并结合本地风险策略通过后签发票据,票据继续绑定 method/assurance/audience。`/sms/code` 必须在同一事务中完成 `ISSUED → CONSUMED` 与唯一短信 outbox 创建:相同幂等键返回原结果,并发或不同键重放不能创建第二个任务;跨手机号、场景、租户或客户端全部失败。 +- 无障碍不得成为降风控布尔开关:禁止 `accessibility=true`、`skipCaptcha`、检测 TalkBack 后放行、供应商故障时直接发短信,以及由客服绕过验证中心触发短信。P0 先落 provider-neutral 验证中心和支持屏幕阅读器、文字聊天/中继的可审计人工兜底;文字渠道只是通信媒介,各 `sceneCode` 仍须定义独立身份或号码控制证据。案件继承且不得改写原 session 的 subject/scene,具有去重、RBAC、主体/IP/设备/审核员限额、审计、服务时段、容量与 SLA,高风险找回或换号双人复核;坐席只提交决定,验证中心才可签票。异步案件和批准授权可在合理期限内恢复,用户重新进入原流程时才激活短时 `validToken`,避免通知前过期;不得要求用户证明残障。中国大陆非交互风控供应商只进入限时 POC,真实 UniApp Android WebView 必须证明 TalkBack、外接键盘、Switch Access、弱网、错误票据、误杀和攻击拦截门槛,达标后才可成为默认自动路径,不能预先宣称无障碍合规。只有此前在同一 subject 的已认证会话绑定私钥、服务端 nonce、RP/App 绑定、`userVerification=required`、短时单次且检查撤销的设备断言才可独立放行;普通设备指纹、完整性检测或仅 user-presence 只能加权,注册和未绑定设备不能使用。音频验证码必须另经可懂度、听障覆盖和 ASR 对抗 POC,不能单独上线或充当唯一替代。 +- 客户端当前只完成浮层壳层的对话语义、焦点进入/圈定/恢复、Escape/Android 返回、原生刷新/关闭、48px 目标与小视口滚动;第三方 TianAi 仍以指针拖动为主,不能据此宣称 TalkBack 可完成。`tests/auth-android-accessibility-release-gate.ps1` 在缺少非拖动等价路径与三人 MuMu 证据时固定输出 `ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED`。 + +### 2.11 F 系列线上写合同与上传阻塞 + +本节只记录 2026-07-22 从 `https://backend-api.ddxcjp.cn/v3/api-docs` 只读核对到的线上事实,不改写受保护的 `APP.openapi.yaml/json`,也不代表 F 页面已经接入写接口: + +- 动态:`AppFamilyFeedBody.feedContent` 必填且 `minLength=1`,其余字段为 `feedType`、`mediaOssIds`、`sortOrder int64`、`status`。`AppFamilyFeedCommentBody.commentContent` 必填,但文档边界为 `minLength=0/maxLength=1000`,另有可选 `parentCommentId int64`;空字符串虽然被模型允许,产品页仍可采用更严格的非空校验,但适配器不能把页面规则误写成服务端约束。 +- 谱文与相册:`AppArticleBody.articleTitle/articleContent` 必填且均为 `minLength=1`,`categoryId/coverOssId/sortOrder` 为 int64;`AppAlbumBody.albumName` 必填且 `minLength=1`,`coverOssId/sortOrder` 为 int64。所有 int64 标识在客户端边界继续以词法字符串保存,只有合同已经消除歧义的请求适配器才可编码。 +- 上传初始化:`SysOssResumableInitBo` 的 `uploadId/fileName/fileMd5/totalSize/totalChunks/chunkSize` 六项必填;`fileMd5` 匹配 `^[a-fA-F0-9]{32}$`,三个大小或分片数均要求正整数。初始化响应的即时命中分支 `SysOssResumableInitVo.ossId` 是 int64。 +- 分片与完成:chunk 要求 query `uploadId/chunkIndex/chunkMd5` 和 multipart `file`;complete 的 `SysOssResumableCompleteBo` 要求 `uploadId/fileName/fileMd5/totalSize/totalChunks`,MD5 与正整数边界同初始化。 +- 硬阻塞:complete 返回的 `SysOssUploadVo.ossId` 被声明为 string,而创建照片的 `AppAlbumPhotoBody.ossId` 必填且声明为 int64;两条成功路径对同一对象存储标识给出冲突类型。后端统一类型或明确无损转换责任并重新导出同版本文档前,客户端不得自行 `Number()`、不得提交照片创建,也不得显示上传成功。F09 因此只能保留明确的本地预览。 + +### 2.12 R 系列线上接口与静态迁移边界 + +三位评审者已经同时从接口字段、业务闭环、异常交互和视觉风险审查 R01—R11。以下是 2026-07-22 线上 OpenAPI 证据与 Task8 静态批次边界,不表示页面已经调用真实接口: + +- 人物:R01/R02 对应 `/genealogy/app/genealogies/{genealogyId}/lineage/persons` 与详情路径;搜索分页另有 `/page`,接收 `keyword/generation/personStatus` 和必填 `pageQuery`,返回 `TableDataInfoAppLineagePersonVo`。`AppLineagePersonBody` 只要求 `name`,页面旧 `role/legacy` 不是可靠请求字段;人物状态与写权限也没有正式字典或能力位。Task8 只复用树成员只读 owner,真实人物写入留到独立接口批次与 T01 v2 原子变更一并治理。 +- 人情往来:R03/R04 对应 `relative-records`,不是强制依赖 `ceremonyId` 的 ceremony gifts。请求只要求 `relativeName`,另有 `relationName/eventName/eventTime/giftAmount/recordContent/mediaOssIds`;模型没有收礼/送礼方向及金额币种语义,后端补齐或产品明确单向定义前不能声称完整礼账闭环。 +- 礼仪:R05—R07 对应 `ceremonies`,请求要求 `ceremonyTitle/ceremonyType`;`ceremonyType/status` 无正式枚举。邀请列表只有 `inviteeUserId/inviteStatus` 等字段,页面若展示姓名必须与同谱成员选项按用户 ID 受控联接,且“受邀人”不能直接写成“已参与者”。 +- 成长:R08 对应 `growth-records`,请求要求 `recordTitle`;页面必须额外强制 `lineagePersonId`,因为家谱级列表没有人物筛选参数。`recordType/status` 无枚举,不能据此在客户端发明分类。 +- 人生事:R09 没有独立线上端点,且 `growth-records.recordType` 没有枚举或人生事件值说明。后端提供正式合同前页面硬关闭,不读取、不写入、不展示 fixture 时间轴。 +- 备忘:R10 对应 `memos`,请求要求 `memoTitle`;`completed/status` 是无枚举字符串,也没有独立幂等切换端点或版本字段,Task8 禁止点击卡片本地翻转官方状态。 +- 功德:R11 对应 `merit-records`,请求要求 `donorName/meritTitle`;`meritType/status` 无枚举,`amount` 也没有币种、精度或非负边界。当前汇总只能来自只读列表,新增预览不得改变正式次数或金额。 +- 公共边界:上述接口都只说明“需要登录”,响应没有统一 `canCreate/canEdit/canDelete`;任何页面角色、创建人或 fixture 权限都不能冒充服务端授权。所有 int64 ID 保持词法字符串;未知实体、缺参、跨谱必须失败关闭。真实写接口未接入前,R 页只允许独立且明确未提交的本地预览,生产路由没有结果能力。 + +### 2.13 N/M 系列线上接口与静态迁移边界 + +三位评审者已同时核对页面流程、2026-07-22 线上 OpenAPI、异常交互和视觉风险。Task9 完成 N/M 安全导航与诚实静态边界;其后只有 M07 在独立 Task25 接入已核对的真实反馈 owner,其余页面仍未提前接入远端: + +- 消息:Task29 把 `GET /genealogy/app/notifications` 与 unread-count 固定为独立读取批次:无筛选列表完整返回当前账号最多 200 条活动通知、最新优先,计数精确覆盖同一集合;首版 adapter 只公开 `snapshotKey/title/content/publishedAt/unread`,N02 由当前内存 generation+ordinal key 读取完整正文。Task30 单独约束两个已读 POST 的字符串 ID、幂等与 read-all 截止点。当前受保护双导出缺 unread-count 和专用模型,线上又无 required/enum/容量、ID 为 int64 且匿名行为与文档冲突,因此两项门禁均为红灯,见 5.6/5.7。首批删除所有通用目标 CTA;后端没有闭合 `bizType` 目标字典前,客户端不猜路由且永不执行服务端 URL。 +- 个人资料:Task28 固定 `GET /genealogy/app/auth/profile` 为 M01/M02/M03 唯一读取 owner。首批 wire 只 required canonical `phone`;`nickName/realName/email` 未设置时省略,出现时分别满足 1—30、1—30、email 且 1—100。adapter 立即掩码手机号并丢弃 `userId/avatar/status` 等未消费字段,当前 `PROFILE-OPENAPI-CONTRACT BLOCKED`,见 5.5。Task31 保留 PUT 为唯一 dirty-only merge owner,只允许脏的三项资料;省略保持,realName/email 精确空串清空,`profileVersion+If-Match+409` 防并发覆盖,当前 `PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED`,见 5.8。头像与 M05 换绑继续各自独立。 +- 密码与手机:Task33 已判定 `PUT /genealogy/app/auth/password` 和登录/注册/找回共用的 32 位十六进制 MD5 不可上线;四条入口须原子迁移到 raw writeOnly、15—64 Unicode/NFC、blocklist/限速/慢哈希。M04 200 前撤销包括当前设备在内的 ALL access/refresh session;unknown 也清本机回 A01,崩溃窗口由无秘密的 sessionEpoch marker 关闭,当前 `PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED`,见 5.10。Task34 又固定 M05 为 currentPassword 再认证+新号 `APP_PHONE_CHANGE` TAC/6 位 OTP;换绑发码必须走专用 SaToken operation,最终 200 前换号、消费 OTP、提升 epoch、撤销 ALL session并持久化旧号通知,当前 `PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED`,见 5.11。M04/M05 都不伪提交。 +- 家谱创建:Task35 否决空谱+通用人物两写,固定 G03 最终按钮一次 atomic bootstrap;访问规则同版统一为 accessPreset,地区只提交 selectable 项的词法 code,结果未知按 operationKey 精确查询且本地不存始祖 PII。当前 `G03-BOOTSTRAP-OPENAPI-CONTRACT BLOCKED`,见 5.12;通过前不接宽松 create API。 +- 帮助与反馈:M06 首批固定 `GET /genealogy/app/help-articles` 为完整列表唯一 owner,不调用详情、不消费 `helpId`;adapter 只允许投影分类、标题和纯文本正文,分类由当前列表动态派生。受保护双导出仍是通用 `ListResult/RList`,线上专用模型又缺 required、正文格式、仅发布和顺序语义,匿名行为也与文档 401 冲突,因此 `HELP-CENTER-OPENAPI-CONTRACT BLOCKED` 保持红灯,详见 5.4。`AppFeedbackBody.feedbackContent` 必填,`feedbackType/contactInfo` 可选且无 enum;M07 已由 `appApi.submitFeedback` 精确 POST `/genealogy/app/feedback`,调用方不能关闭认证头,只接受 HTTP 200 与整数成功 `code`。mock 模式不伪提交;成功、确定失败、结果未知和迟到输入已分离。两页真实服务与 MuMu 验收都等待认证远端门禁关闭。 +- 邀请:`GET /genealogy/app/promotions` 只返回推广内容与通用 `targetUrl`,没有家谱邀请码签发、校验、失效或直接加入端点,不能支撑产品邀请闭环。M08 已删除硬编码码值、剪贴板和海报伪能力,并显示不可用;未来只能接入“校验成功直接加入且不生成审核记录”的单一路径。 +- VIP 与订单:线上存在套餐和订单的查询/创建端点,但当前文档未闭合支付方式、价格精度、订单状态、重复下单、支付回调、退款与续费语义。M09 不读取查询参数、不生成演示订单并保持不可用,待独立支付合规审查后再开放。 +- 退出:Task32 固定 `DELETE /genealogy/app/auth/logout` 只撤销当前 bearer credential family,其他设备保持有效;同 client 的 active/revoked/expired 凭证重复调用都收敛为同一 200,非法/client 不匹配为 typed 401。客户端未来由唯一 logoutCoordinator 在同一同步段捕获 A、清本地并 bump epoch、用显式 A 启动不随 M10 卸载取消的请求,然后立即进入 A01;迟到结果不再 clear。当前本地/线上合同都缺 required、范围/幂等/no-store 与复用反例,`LOGOUT-OPENAPI-CONTRACT BLOCKED`,见 5.9。 ## 三、52 个活动页面映射 -表中“返回或完成目标”描述业务意图,不表示现有导航 API 已经正确;导航栈阶段需要用源码扫描、测试和 MuMu 完整流程逐项验证。除 A04 和 T01 已形成专项证据外,其余接口列继续标记“待对应业务阶段 OpenAPI 审查”,避免把旧思维导图、页面 mock 或 PC 接口误当成 App 合同。 +表中“返回或完成目标”描述业务意图,不表示现有导航 API 已经正确;导航栈阶段需要用源码扫描、测试和 MuMu 完整流程逐项验证。A04、G 系列第一轮、F 系列当前写边界、T01 和新线上差异已形成专项证据;其余接口列继续标记“待对应业务阶段 OpenAPI 审查”,避免把旧思维导图、页面 mock、旧离线快照或 PC 接口误当成当前 App 合同。 | 编号 | 页面 | 路由 | 当前业务目标 | 主要进入方式 | 返回或完成目标 | 必测状态 | 接口业务域 | 当前接口核对状态 | | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| A01 | 登录 | `pages/auth/a01-entry` | 完成密码、短信或微信认证并处理协议 | APP 启动、凭证失效、主动退出 | 成功进入 G01;取消或失败留在本页 | 密码、短信、协议错误、发送中、倒计时、授权取消、登录失败、凭证过期 | 认证与账户 | 待对应业务阶段 OpenAPI 审查 | -| A04 | 注册账号 | `pages/auth/a04-register` | 建立新账号并确认协议 | A01 注册入口 | 成功建立登录态并进入 G01;取消返回 A01 | 本地校验、行为验证、注册中、手机号占用、成功、失败、取消 | 认证与账户 | 已核对注册成功响应为 `LoginResult`;行为验证与短信流程仍待审 | -| A05 | 重设密码 | `pages/auth/a05-reset-password` | 验证手机号并设置新密码 | A01 忘记密码 | 成功返回 A01 的密码登录态;取消返回 A01 | 验证码、行为验证、密码策略、不一致、提交中、成功、失败、取消 | 认证与账户 | 待对应业务阶段 OpenAPI 审查 | -| G01 | 我的家谱 | `pages/genealogy/g01-my-genealogies` | 选择全局家谱并完成加入或创建分流 | 登录成功、根 Tab、业务完成回流 | 进入 G03、G05、G06、G09、G10、G12、T01 或 N01 | 正常、空、加载、失败、审核中、被拒绝、退出或移除、待录入始祖、切换弹层 | 家谱与成员关系 | 待对应业务阶段 OpenAPI 审查 | -| G03 | 创建家谱 | `pages/genealogy/g03-create-genealogy` | 创建独立家谱并录入始祖 | G01 创建入口或待完善记录 | 创建后续接始祖步骤;始祖完成进入 G05;取消回来源 | 创建、重复提醒、创建失败、待完善、始祖校验、保存中、成功、中断恢复 | 家谱与成员关系 | 待对应业务阶段 OpenAPI 审查 | -| G05 | 家谱总览 | `pages/genealogy/g05-genealogy-overview` | 浏览家谱身份、来源和可信度并提供管理入口 | G01、G06 公开预览、G09 通过结果、G03 完成 | 返回 G01;进入 T01、G08、G10、G11、G12 或 F01 | 公开预览、成员视图、所有者、加载、空、失败、无权限、新建引导 | 家谱与权限 | 待对应业务阶段 OpenAPI 审查 | -| G06 | 加入家谱 | `pages/genealogy/g06-search-genealogies` | 通过搜索或邀请码准确定位目标家谱或支系 | G01 空态或添加家谱弹层 | 进入 G05 公开预览、G08、G09;已加入或我创建时回 G01 并选中 | 初始、搜索中、结果、无结果、邀请码无效或过期、失败、六种用户关系 | 家谱搜索与邀请 | 待对应业务阶段 OpenAPI 审查 | -| G08 | 关系确认与入谱 | `pages/genealogy/g08-join-application` | 填写真实姓名、关系和说明并提交申请或直接加入 | G06 选定目标、G05 公开预览、G09 重新提交 | 搜索来源成功进入 G09;邀请码成功回 G01 并选中新家谱 | 双来源、字段校验、提交中、成功、失败、重复提交、放弃填写 | 加入申请与邀请 | 待对应业务阶段 OpenAPI 审查 | -| G09 | 我的申请 | `pages/genealogy/g09-my-applications` | 查看、撤回或修改加入申请 | G01 申请分组、G08 搜索申请成功、G06 审核中或被拒绝状态 | 已通过进入 G05;修改进入 G08;撤回后保留明确结果 | 列表、空、失败、待审、通过、拒绝、撤回、重新提交 | 加入申请 | 待对应业务阶段 OpenAPI 审查 | -| G10 | 入谱审核 | `pages/genealogy/g10-application-review` | 所有者审核加入申请 | G01、G05 或 N01 审核消息 | 完成后刷新审核列表及来源计数;返回来源 | 列表、空、失败、通过确认、拒绝原因、提交中、无权限;拒绝字段使用 `aria-invalid`、`aria-describedby`、错误 `role="alert"` 并在空提交后聚焦 | 加入审核与权限 | 待对应业务阶段 OpenAPI 审查 | -| G11 | 家谱设置 | `pages/genealogy/g11-genealogy-settings` | 维护家谱名称、公开范围和访问说明 | G05 所有者管理入口 | 保存后刷新 G05;取消恢复原值并返回 | 加载、字段校验、保存中、成功、失败、无权限、未保存返回 | 家谱设置与权限 | 待对应业务阶段 OpenAPI 审查 | -| G12 | 字辈诗 | `pages/genealogy/g12-generation-poems` | 浏览与维护字辈序列 | G01 快捷入口或 G05 | 保存后刷新列表;取消返回来源 | 列表、空、编辑、校验、保存中、失败、无权限、长列表 | 字辈与权限 | 待对应业务阶段 OpenAPI 审查 | +| A01 | 登录 | `pages/auth/a01-entry` | 完成短信认证并处理协议;密码与微信待合同关闭 | APP 启动、凭证失效、主动退出 | 成功进入 G01;取消或失败留在本页 | 短信、协议错误、TAC、发送中、倒计时、请求取消、登录失败、凭证过期 | 认证与账户 | `APP_SMS_LOGIN` 客户端链已落地;密码登录因 `API-AUTH-TAC-001` 硬关闭,后端与 MuMu 门禁见 2.10 | +| A04 | 注册账号 | `pages/auth/a04-register` | 建立新账号并确认协议 | A01 注册入口 | 成功建立登录态并进入 G01;取消返回 A01 | 本地校验、TAC、短信、注册中、手机号占用、成功、失败、取消 | 认证与账户 | `APP_REGISTER`、4 位码、`RAppLoginVo → AppLoginVo.access_token` 客户端链已落地;真实 challenge 与 OpenAPI 仍红灯,见 2.10 | +| A05 | 重设密码 | `pages/auth/a05-reset-password` | 验证手机号并设置新密码 | A01 忘记密码 | 成功返回 A01 的密码登录态;取消返回 A01 | 4 位码、TAC、密码策略、不一致、提交中、成功、失败、取消 | 认证与账户 | `APP_FORGOT_PASSWORD` 客户端链已落地;真实后端、Android 可访问替代与 MuMu 仍红灯,见 2.10 | +| G01 | 我的家谱 | `pages/genealogy/g01-my-genealogies` | 选择全局家谱并完成加入或创建分流 | 登录成功、根 Tab、业务完成回流 | 进入 G03、G05、G06、G09、G10、G12、T01 或 N01 | 正常、空、加载、失败、审核中、被拒绝、退出或移除、切换弹层、未读消息 | 家谱、成员关系与通知计数 | Task26 工作区红灯已建立;`/mine` 尚未接远端,见 2.8/5.3;Task29 unread-count 红灯要求与 M01 共用唯一未读数 owner,见 2.13/5.6 | +| G03 | 创建家谱 | `pages/genealogy/g03-create-genealogy` | 在同页收集家谱与始祖并最终原子创建 | G01 创建入口 | 门禁前进入 G05 本地预览;生产成功按 receipt→mine cache→context→G05 唯一次序收口,放弃零写 | 地区加载/失败、重复建议、始祖校验、提交中、PENDING、结果未知、fatal/quarantined、已提交待进入、放弃确认 | 家谱与成员关系 | Task35 已建立 atomic bootstrap、无 PII operation-status、统一 accessPreset、APP 可信地区与词法 ID 的后端/客户端双红灯;开放还依赖 Task26 workspace 与 MuMu,见 2.8/5.12 | +| G05 | 家谱总览 | `pages/genealogy/g05-genealogy-overview` | 按公开预览、成员、所有者或本地预览浏览身份、来源和可信度 | G01、G06、G09、G03 本地预览 | 返回实际来源;按权限进入 T01、G08、G10、G11、G12 或 F01 | 公开预览、成员视图、所有者、本地预览、加载、空、失败、无权限 | 家谱与权限 | Task26 选定 `/overview` 为唯一 owner;最小 schema、对象级授权和错误语义未关闭,丰富首屏字段另待取舍,见 2.8/5.3 | +| G06 | 加入家谱 | `pages/genealogy/g06-search-genealogies` | 通过搜索或邀请码本地校验定位目标家谱或支系 | G01 空态或添加家谱弹层 | 进入 G05、G08、G09;已加入或我创建时回 G01 | 初始、搜索中、结果、无结果、邀请码无效或过期、失败、六种用户关系 | 家谱搜索与邀请 | 第一轮已核对;邀请码验证与直入端点缺失,见 2.8 | +| G08 | 关系确认与入谱 | `pages/genealogy/g08-join-application` | 校验真实姓名、关系和说明并预览两种加入流程 | G06、G05 或 G09 的共享资格入口 | 搜索来源本地完成进入 G09;邀请码来源本地完成回 G01;均不建立成员关系 | 双来源、不可申请、字段校验、提交中、本地成功、失败、重复提交、放弃填写 | 加入申请与邀请 | 第一轮已核对;申请字段适配与邀请码票据缺失,见 2.8 | +| G09 | 我的申请 | `pages/genealogy/g09-my-applications` | 查看申请并预览撤回或重新申请 | G01、G08 本地流程、G06 审核中 | 已通过进入 G05;可重申记录进入 G08;本地撤回不改变服务器状态 | 列表、空、失败、待审、通过、拒绝、本地撤回、重新提交 | 加入申请 | 第一轮已核对;类型化申请行已存在,正式状态字典与词法 `applyId` 适配待补,见 2.8 | +| G10 | 入谱审核 | `pages/genealogy/g10-application-review` | 所有者预览通过或拒绝申请 | G01、G05 或 N01 审核消息 | 本页只更新本地预览;取消或返回不产生导航结果 | 列表、空、失败、通过确认、拒绝原因、提交中、无权限;拒绝字段用 `aria-describedby` 保留错误关联和失败聚焦 | 加入审核与权限 | 第一轮已核对;类型化申请行与审核体已存在,正式状态字典和词法 int64 适配待补,见 2.8 | +| G11 | 家谱设置 | `pages/genealogy/g11-genealogy-settings` | 本地维护名称、访问预设和家谱简介 | G05 所有者管理入口 | 保存只更新本页预览;取消恢复原值并返回 | 加载、字段校验、本地成功、失败、无权限、未保存返回 | 家谱设置与权限 | 第一轮已核对;Task35 只统一 accessPreset 字段,真实写入仍须独立 If-Match、版本/CAS、权限刷新与结果未知门禁,见 2.8/5.12 | +| G12 | 字辈诗 | `pages/genealogy/g12-generation-poems` | 分批浏览并本地维护完整字辈序列 | G01 快捷入口或 G05 | 保存只更新本地列表;取消恢复编辑快照并返回来源 | 列表、空、编辑、校验、无权限、500 代分批渲染、停用但保留历史 | 字辈与权限 | 第一轮已核对;batch 首项世代未定义,真实保存须以服务端 preview 为准,见 2.8 | | T01 | 世系树 | `pages/tree/t01-tree-overview` | 以当前成员为焦点阅读可扩展世系窗口,并在图、概览和线性列表间定位成员 | G01 快捷入口或 G05 | 返回来源;进入单实例 T03、T04、T06、T07 | 上二代/下二代初始窗口、搜索、四级 LOD、多配偶联合点、宽支系聚合、代际缺口、四类边界、图/列表、空、失败、版本冲突、500 节点性能;节点与线由同一 Canvas/矩阵/帧绘制 | 世系与成员 | 专项已核对:现有递归 `LineagePersonTreeView` 不满足;待后端按规范图窗口问题单更新 Apifox | | T03 | 成员档案 | `pages/tree/t03-member-profile` | 在单个原生页面实例内查看成员资料、亲属与受控状态 | T01、T07、R02 的成员关联 | 页内成员轨迹优先返回;轨迹结束后回实际来源;进入 T05、T08 | A→B→C→B→A 页内轨迹、可编辑、隐私、无权限、成员缺失、加载失败、离世状态;读取成功后才推进轨迹 | 成员档案与权限 | 待对应业务阶段 OpenAPI 审查 | -| T04 | 新增亲属 | `pages/tree/t04-add-relative` | 录入首位成员或为目标成员新增亲属 | T01 指定节点或空树入口 | 保存后回 T01 并精确定位新建成员;取消回实际来源 | 首位成员、普通亲属、关系选择、必填、长摘要、保存中、成功、失败、放弃确认 | 成员与亲属关系 | 待对应业务阶段 OpenAPI 审查 | -| T05 | 编辑成员 | `pages/tree/t05-edit-member` | 修改指定成员身份和生平资料 | T03 编辑入口 | 保存后返回 T03 并刷新;取消回 T03 | 加载、字段校验、长简介、保存中、成功、失败、无权限、放弃确认 | 成员档案与权限 | 待对应业务阶段 OpenAPI 审查 | -| T06 | 编辑关系 | `pages/tree/t06-edit-relationship` | 校正两个现有成员之间的关系 | T01 关系操作 | 保存后返回 T01 并刷新关系;取消回来源 | 成员选择、校验、冲突、循环关系、冲突规则弹窗、保存中、失败、无权限 | 亲属关系与权限 | 待对应业务阶段 OpenAPI 审查 | +| T04 | 新增亲属 | `pages/tree/t04-add-relative` | 录入首位成员或为目标成员新增亲属 | T01 指定节点或空树入口 | 当前只生成“尚未提交服务器”的本地预览,确认放弃后无结果回 T01;真实写成功后才定位新成员 | 首位成员、普通亲属、关系选择、必填、长摘要、保存中、本地预览、失败、放弃确认 | 成员与亲属关系 | 待对应业务阶段 OpenAPI 审查 | +| T05 | 编辑成员 | `pages/tree/t05-edit-member` | 修改指定成员身份和生平资料 | T03 编辑入口 | 当前本地预览确认放弃后用 `goBack()` 回原 T03 实例且不产生结果;真实写成功后才刷新当前活动成员 | 加载、字段校验、长简介、保存中、本地预览、失败、无权限、放弃确认 | 成员档案与权限 | 待对应业务阶段 OpenAPI 审查 | +| T06 | 编辑关系 | `pages/tree/t06-edit-relationship` | 校正两个现有成员之间的关系 | T01 关系操作 | 当前只生成“尚未提交服务器”的本地预览,确认放弃后无结果回 T01;真实写成功后才刷新关系 | 成员选择、校验、冲突、循环关系、冲突规则弹窗、保存中、本地预览、失败、无权限 | 亲属关系与权限 | 待对应业务阶段 OpenAPI 审查 | | T07 | 成员目录 | `pages/tree/t07-member-directory` | 搜索、筛选并选择家谱成员 | T01 成员目录入口 | 进入 T03;返回 T01 并恢复目录现场 | 完整列表、筛选、搜索无结果、明确空态、失败重试、加载、长列表、成员选择 | 成员查询 | 待对应业务阶段 OpenAPI 审查 | | T08 | 成员状态 | `pages/tree/t08-member-states` | 解释成员隐私、纪念或无权限状态 | T03 人物状态入口 | 返回 T03;家谱不可用时回 G01 | 隐私隐藏、离世纪念、无权限、无效成员、权限变化 | 成员状态与权限 | 待对应业务阶段 OpenAPI 审查 | -| F01 | 家族动态 | `pages/family/f01-family-feed` | 展示家族动态并承载内容和档案入口 | 根 Tab 或 G05 | 进入 F02—F04、F07、F10、R01、R03、R05、R10、R11 | 加载、列表、空、失败、刷新、分页、无可用家谱 | 家族内容聚合 | 待对应业务阶段 OpenAPI 审查 | -| F02 | 发布动态 | `pages/family/f02-publish-feed` | 发布家族文字或媒体动态 | F01 发布入口 | 成功回 F01 并刷新;取消保留或确认放弃 | 表单、空内容校验、长内容、媒体权限、提交中、成功、失败、重复提交、取消 | 动态发布与上传 | 待对应业务阶段 OpenAPI 审查 | -| F03 | 动态详情 | `pages/family/f03-feed-detail` | 阅读动态并查看或提交评论 | F01 动态卡 | 返回 F01 并恢复现场 | 加载、正常、内容失效、失败、评论校验、提交失败、插入成功、无权限 | 动态与评论 | 待对应业务阶段 OpenAPI 审查 | -| F04 | 谱文列表 | `pages/family/f04-article-list` | 分类、搜索和浏览谱文 | F01 谱文入口 | 进入 F05 或 F06;返回 F01 | 加载、列表、分类、搜索无结果并重置、空、失败、长列表、新建 | 谱文 | 待对应业务阶段 OpenAPI 审查 | -| F05 | 谱文详情 | `pages/family/f05-article-detail` | 阅读、收藏和按权限编辑谱文 | F04 谱文卡 | 返回 F04;有权限进入 F06 | 加载、正常、收藏切换、编辑、失效、隐私、失败、无权限 | 谱文与权限 | 待对应业务阶段 OpenAPI 审查 | -| F06 | 编辑谱文 | `pages/family/f06-article-editor` | 新建或编辑谱文草稿 | F04 新建或 F05 编辑 | 保存或发布后回 F04/F05 并刷新;取消确认放弃 | 新建、编辑、校验、加载、草稿、保存中、失败保留并重试、成功回流、长正文 | 谱文编辑 | 待对应业务阶段 OpenAPI 审查 | -| F07 | 相册列表 | `pages/family/f07-album-list` | 浏览和创建家族相册 | F01 相册入口 | 进入 F08;返回 F01 | 加载、列表、长列表、空、失败、相册导航、创建弹窗、校验、插入和轻提示、权限 | 相册 | 待对应业务阶段 OpenAPI 审查 | -| F08 | 相册详情 | `pages/family/f08-album-detail` | 浏览照片墙和相册信息 | F07 相册卡 | 返回 F07;进入 F09 | 加载、照片墙、末张预览、Android 返回先关预览、空相册、相册失效、失败、权限 | 相册与媒体 | 待对应业务阶段 OpenAPI 审查 | -| F09 | 上传照片 | `pages/family/f09-media-upload` | 选择照片、填写逐张说明并上传 | F08 添加照片入口 | 成功回 F08 并刷新;取消确认放弃 | 初始、权限、最多九张、增删、当前照片独立说明、必填、上传锁定与进度、取消、失败重试、成功 | 媒体上传 | 待对应业务阶段 OpenAPI 审查 | -| F10 | 家族视频 | `pages/family/f10-video-list` | 说明当前视频服务尚未开放 | F01 视频入口 | 当前只返回 F01 | 待开放、返回 F01;未来列表、上传和接口状态只登记为对应业务阶段依赖,不冒充当前功能 | 视频服务 | 待对应业务阶段 OpenAPI 审查 | -| R01 | 人物录 | `pages/records/r01-people-list` | 搜索和浏览家族人物记录 | F01 人物录入口 | 进入 R02;返回 F01 | 加载、列表、搜索、无结果、空、失败、分页、新建权限 | 人物记录 | 待对应业务阶段 OpenAPI 审查 | -| R02 | 人物详情 | `pages/records/r02-person-detail` | 查看、新建或编辑人物记录 | R01 人物卡或新建入口 | 保存后回 R01 并刷新;进入 R08/R09;取消回来源 | 查看、新建、编辑、校验、保存中、成功、失败、隐私、失效 | 人物记录与权限 | 待对应业务阶段 OpenAPI 审查 | -| R03 | 贺礼簿 | `pages/records/r03-gift-list` | 浏览、筛选和新增贺礼记录 | F01 贺礼簿入口 | 进入 R04;返回 F01 | 加载、列表、空、失败、筛选、分页、新增权限 | 贺礼记录 | 待对应业务阶段 OpenAPI 审查 | -| R04 | 贺礼编辑 | `pages/records/r04-gift-editor` | 查看、新增、编辑或删除贺礼 | R03 记录或新增入口 | 保存或删除后回 R03 并刷新;取消回来源 | 查看、新增、编辑、校验、保存中、成功、失败、删除确认、无权限 | 贺礼记录与权限 | 待对应业务阶段 OpenAPI 审查 | -| R05 | 礼仪列表 | `pages/records/r05-ritual-list` | 浏览和创建家族礼仪活动 | F01 礼仪入口 | 进入 R06 或 R07;返回 F01 | 加载、列表、空、失败、活动状态、分页、新建权限 | 礼仪活动 | 待对应业务阶段 OpenAPI 审查 | -| R06 | 礼仪详情 | `pages/records/r06-ritual-detail` | 查看礼仪信息、参与者和状态 | R05 活动卡 | 返回 R05;有权限进入 R07 | 加载、详情、参与者、失败、失效、无权限 | 礼仪活动与参与 | 待对应业务阶段 OpenAPI 审查 | -| R07 | 礼仪编辑 | `pages/records/r07-ritual-editor` | 新建或编辑礼仪活动 | R05 新建或 R06 编辑 | 保存或删除后回 R05 并刷新;取消回来源 | 新建、编辑、校验、保存中、成功、失败、删除确认、无权限 | 礼仪活动与权限 | 待对应业务阶段 OpenAPI 审查 | -| R08 | 成长日志 | `pages/records/r08-growth-journal` | 展示人物成长时间轴并新增记录 | R02 或 T03 人物入口 | 保存后插入时间轴并给出轻提示;返回人物来源 | 加载、时间轴、空、失败、新增弹窗、必填、长文内部滚动、保存、权限 | 人物成长记录 | 待对应业务阶段 OpenAPI 审查 | -| R09 | 人生事 | `pages/records/r09-life-events` | 展示人物人生事件时间轴并新增记录 | R02 或 T03 人物入口 | 保存后插入时间轴并给出轻提示;返回人物来源 | 加载、时间轴、空、失败、新增、校验、保存、权限 | 人生事件 | 待对应业务阶段 OpenAPI 审查 | -| R10 | 家族备忘 | `pages/records/r10-memo-list` | 管理家族备忘和完成状态 | F01 备忘入口 | 新增或切换完成后刷新本页;返回 F01 | 加载、列表、空、失败、新增校验、完成、重新打开、重复操作、权限 | 家族备忘 | 待对应业务阶段 OpenAPI 审查 | -| R11 | 功德记录 | `pages/records/r11-merit-records` | 记录贡献并展示汇总 | F01 功德录入口 | 新增后实时刷新汇总和列表并给出轻提示;返回 F01 | 加载、汇总、列表、空、失败、新增、校验、保存中、权限 | 功德与贡献 | 待对应业务阶段 OpenAPI 审查 | -| N01 | 消息中心 | `pages/notification/n01-message-center` | 汇总消息、维护已读状态并分流业务 | G01 或 M01 消息入口 | 进入 N02 或对应 G10 等业务页面;返回来源 | 加载、未读、已读、全部已读、空、失败、审核消息、分页 | 消息与通知 | 待对应业务阶段 OpenAPI 审查 | -| N02 | 消息详情 | `pages/notification/n02-message-detail` | 展示消息正文并安全跳转到业务目标 | N01 消息卡 | 返回 N01;有效目标进入对应业务页 | 加载、详情、已读、失效消息、无目标、业务目标过期、失败 | 消息与业务分流 | 待对应业务阶段 OpenAPI 审查 | -| M01 | 我的 | `pages/profile/m01-profile-home` | 展示个人资料、提醒和服务导航 | 根 Tab | 进入 M02、M03、M06、M08、M09、M10 或 N01 | 加载、正常、失败、提醒、资料不完整、服务可用性 | 个人中心聚合 | 待对应业务阶段 OpenAPI 审查 | -| M02 | 个人资料 | `pages/profile/m02-edit-profile` | 查看并编辑头像和基础资料 | M01 资料入口 | 保存后回 M01 并刷新;取消回来源 | 加载、头像权限、字段校验、保存中、成功、失败、未保存返回 | 用户资料与上传 | 待对应业务阶段 OpenAPI 审查 | -| M03 | 账号与安全 | `pages/profile/m03-security-settings` | 汇总密码、手机号和设备安全入口 | M01 安全入口 | 进入 M04 或 M05;返回 M01 | 加载、正常、异常提醒、失败、设备状态 | 账号安全 | 待对应业务阶段 OpenAPI 审查 | -| M04 | 修改密码 | `pages/profile/m04-change-password` | 验证旧密码并设置新密码 | M03 密码入口 | 成功回 M03 或按安全合同重新登录;取消回 M03 | 旧密码错误、统一密码策略、新旧相同、不一致、提交中、成功、失败、重复提交 | 账号安全 | 待对应业务阶段 OpenAPI 审查 | -| M05 | 修改手机号 | `pages/profile/m05-change-phone` | 验证并更换绑定手机号 | M03 手机号入口 | 成功回 M03 并刷新;取消回 M03 | 当前身份校验、新号码、验证码、倒计时、号码占用、成功、失败 | 账号安全与短信 | 待对应业务阶段 OpenAPI 审查 | -| M06 | 帮助中心 | `pages/profile/m06-help-center` | 搜索和浏览帮助内容 | M01 帮助入口 | 返回 M01;无法解决时进入 M07 | 加载、分类、搜索、无结果、失败、内容失效 | 帮助内容 | 待对应业务阶段 OpenAPI 审查 | -| M07 | 意见反馈 | `pages/profile/m07-feedback` | 提交问题说明和联系信息 | M06 联系入口 | 成功给出明确结果后返回 M06或 M01;取消回来源 | 校验、附件权限、提交中、成功、失败、重复提交、取消 | 用户反馈与上传 | 待对应业务阶段 OpenAPI 审查 | -| M08 | 应用推广 | `pages/profile/m08-promotion` | 生成并分享家谱邀请信息 | M01 推广入口 | 分享成功、取消或失败均留有明确结果;返回 M01 | 邀请码、海报生成、系统分享权限、取消、失败、过期 | 邀请与系统分享 | 待对应业务阶段 OpenAPI 审查 | -| M09 | VIP 与订单 | `pages/profile/m09-vip-orders` | 展示服务权益和订单;当前明确未开放付费 | M01 服务入口 | 当前关闭说明并返回 M01;未来进入合规订单流程 | 待开放、无订单、订单列表、加载失败、支付取消、退款边界 | 服务权益、订单与支付 | 待对应业务阶段 OpenAPI 审查 | -| M10 | 关于家谱 | `pages/profile/m10-about-settings` | 展示版本、协议、隐私并处理退出登录 | M01 设置入口 | 协议关闭留在本页;退出成功清凭证并回 A01;取消留在本页 | 版本、协议、隐私、退出确认、取消、退出失败 | 配置、协议与认证 | 待对应业务阶段 OpenAPI 审查 | +| F01 | 家族动态 | `pages/family/f01-family-feed` | 按当前成员家谱展示动态并承载内容和档案入口 | 根 Tab 或 G05 | URL 已规范化后进入 F02—F04、F07、F10、R01、R03、R05、R10、R11 | 加载、列表、空、失败、无有效家谱、跨谱失败关闭 | 家族内容聚合 | 任务 7 已核对;线上有动态列表/写入能力,当前静态批次只读且按家谱隔离 | +| F02 | 发布动态 | `pages/family/f02-publish-feed` | 编辑文字或媒体动态的本地预览 | F01 发布入口 | 预览不发布、不产出结果;取消保留或确认放弃后回同一 F01 | 表单、空内容校验、长内容、媒体权限、本地预览、无权限、放弃确认 | 动态发布与上传 | 线上写请求要求 `feedContent` 且 minLength=1,另有 `feedType/mediaOssIds/sortOrder/status`;真实调用与媒体上传未启用,mock 以 `WRITE_UNAVAILABLE` 失败关闭,详见 2.11 | +| F03 | 动态详情 | `pages/family/f03-feed-detail` | 按 `genealogyId + feedId` 阅读动态并编辑评论草稿 | F01 动态卡 | 评论只在本页预览,不插入、不计数、不清空;返回同一 F01 | 加载、正常、内容失效、失败、评论校验、本地预览、无权限 | 动态与评论 | 线上评论写请求要求 `commentContent`,文档边界为 minLength=0/maxLength=1000;当前未调用写接口,不宣称发送成功,详见 2.11 | +| F04 | 谱文列表 | `pages/family/f04-article-list` | 按家谱分类、搜索和浏览谱文 | F01 谱文入口 | 进入精确 F05 或 create 模式 F06;返回同一 F01 | 加载、列表、分类、搜索无结果并重置、空、失败、长列表、新建权限 | 谱文 | 任务 7 已核对线上列表及写接口;当前列表由唯一只读 owner 提供深拷贝 | +| F05 | 谱文详情 | `pages/family/f05-article-detail` | 按 `genealogyId + articleId` 阅读并按权限进入编辑 | F04 谱文卡 | 返回同一 F04;有权限进入 edit 模式 F06 | 加载、正常、收藏暂未开放、编辑、失效、隐私、失败、无权限 | 谱文与权限 | 线上未证明收藏合同,当前禁用收藏;文章写请求要求 `articleTitle/articleContent` 且均为 minLength=1,详见 2.11 | +| F06 | 编辑谱文 | `pages/family/f06-article-editor` | 新建或编辑谱文的本地预览 | F04 新建或 F05 编辑 | create 回 F04,edit 回精确 F05;预览不保存、不产出结果 | 新建、编辑、校验、加载、本地预览、失败保留、长正文、放弃确认 | 谱文编辑 | 线上 POST/PUT 已存在,`categoryId/coverOssId` 为 int64;真实调用未在导航批次启用,详见 2.11 | +| F07 | 相册列表 | `pages/family/f07-album-list` | 按家谱浏览相册并制作独立的新相册预览 | F01 相册入口 | 正式相册进入 F08;本地预览不插入列表;返回同一 F01 | 加载、列表、长列表、空、失败、创建弹窗、本地预览、校验、权限、放弃确认 | 相册 | 线上创建要求 `albumName`;当前不调用写接口、不伪增列表或计数 | +| F08 | 相册详情 | `pages/family/f08-album-detail` | 按 `genealogyId + albumId` 浏览照片墙和相册信息 | F07 相册卡 | 先关闭照片预览,再返回 F07;进入同一相册的 F09 | 加载、照片墙、末张预览、Android 返回先关预览、空相册、相册失效、失败、权限 | 相册与媒体 | 任务 7 已核对;当前严格按复合身份读快照,未知或跨谱相册不回退首条 | +| F09 | 上传照片 | `pages/family/f09-media-upload` | 选择照片并填写批量及逐张说明的本地预览 | F08 添加照片入口 | 预览不上传、不生成成功态;取消确认放弃后回同一 F08 | 初始、权限、最多九张、增删、独立说明、必填、本地预览、放弃确认、无效相册 | 媒体上传 | 旧 `/files/upload` 已下线;resumable init/chunk/complete 的 complete 返回 `ossId` 为 string,而照片创建要求 int64;后端消除类型冲突前不得转换或提交,详见 2.11 | +| F10 | 家族视频 | `pages/family/f10-video-list` | 按有效家谱说明当前视频服务尚未开放 | F01 视频入口 | 当前只返回同一 F01 | 待开放、无效家谱、返回 F01;未来能力不冒充当前功能 | 视频服务 | 任务 7 未发现可支撑当前页面闭环的已启用视频产品合同,保持关闭 | +| R01 | 人物录 | `pages/records/r01-people-list` | 按当前家谱搜索和浏览人物只读快照 | F01 人物录入口 | 进入带精确家谱与模式的 R02;返回 F01 | 加载、列表、搜索、无结果、空、失败、分页、跨谱失败 | 人物记录 | 任务 8 已核对 `lineage/persons/page`;ID 必须保持词法字符串,真实分页尚未接入 | +| R02 | 人物详情 | `pages/records/r02-person-detail` | 查看人物或制作不写库的新建/编辑预览 | R01 人物卡或预览入口 | 预览不产出结果;进入 R08 或硬关闭的 R09;取消回来源 | 查看、新建、编辑、校验、预览、隐私、失效、跨谱 | 人物记录与权限 | 任务 8 已核对 `AppLineagePersonBody`,`name` 必填;真实写入未启用,权限字典待补 | +| R03 | 贺礼簿 | `pages/records/r03-gift-list` | 浏览同谱人情往来只读快照 | F01 贺礼簿入口 | 进入携带 `relativeId` 的 R04;返回 F01 | 加载、列表、空、失败、跨谱 | 人情往来 | 任务 8 已核对 `relative-records`,不是 ceremony gifts;收礼/送礼方向语义仍缺 | +| R04 | 贺礼编辑 | `pages/records/r04-gift-editor` | 查看往来记录或制作不写库的本地预览 | R03 记录或预览入口 | 预览不插入列表、不删除记录;取消回来源 | 查看、新建、编辑、校验、本地预览、无效实体、跨谱 | 人情往来与权限 | 任务 8 已核对 `relativeId` 与 `relativeName` 必填;真实写入未启用,只允许本地预览 | +| R05 | 礼仪列表 | `pages/records/r05-ritual-list` | 浏览同谱礼仪活动只读快照 | F01 礼仪入口 | 进入 R06 或 R07 预览;返回 F01 | 加载、列表、空、失败、活动展示、跨谱 | 礼仪活动 | 任务 8 已核对 ceremonies,`ceremonyTitle/ceremonyType` 必填且类型、状态无正式枚举 | +| R06 | 礼仪详情 | `pages/records/r06-ritual-detail` | 查看精确礼仪与受邀人快照 | R05 活动卡 | 返回 R05;进入同一礼仪 R07 | 加载、详情、受邀人、失败、失效、跨谱 | 礼仪活动与邀请 | 任务 8 已核对 invitations;姓名需与成员选项受控联接,不能把受邀者冒充参与者 | +| R07 | 礼仪编辑 | `pages/records/r07-ritual-editor` | 制作新建或编辑礼仪的本地预览 | R05 预览入口或 R06 编辑入口 | create 预览回 R05,edit 预览回原 R06;不产出结果 | 新建、编辑、校验、本地预览、无效实体、跨谱 | 礼仪活动与权限 | 任务 8 已核对 `ceremonyId`;必填与枚举见 R05,真实写入未启用 | +| R08 | 成长日志 | `pages/records/r08-growth-journal` | 按同谱人物展示成长快照并制作独立预览 | R02 或 T03 人物入口 | 预览不插入正式时间轴;返回实际人物来源 | 加载、时间轴、空、失败、预览弹窗、必填、跨谱 | 人物成长记录 | 任务 8 已核对 growth-records;客户端必须强制 `lineagePersonId`,`recordType` 无枚举 | +| R09 | 人生事 | `pages/records/r09-life-events` | 明确说明人生事件服务当前不可用 | R02 或 T03 人物入口 | 不读取或写入成长记录;安全返回人物来源 | 接口缺失、无效人物、返回来源 | 人生事件 | 任务 8 已核对:没有独立人生事件接口,后端补端点或正式类型字典前硬关闭 | +| R10 | 家族备忘 | `pages/records/r10-memo-list` | 浏览同谱备忘快照并制作独立预览 | F01 备忘入口 | 预览不插入列表、不切换正式完成状态;返回 F01 | 加载、列表、空、失败、预览、跨谱 | 家族备忘 | 任务 8 已核对 memos,`memoTitle` 必填而 `completed` 无枚举,真实切换未启用 | +| R11 | 功德记录 | `pages/records/r11-merit-records` | 浏览只读汇总并制作独立贡献预览 | F01 功德录入口 | 预览不改变正式汇总或列表;返回 F01 | 加载、汇总、列表、空、失败、预览、跨谱 | 功德与贡献 | 任务 8 已核对 merit-records,`donorName/meritTitle` 必填,类型、状态与金额边界无枚举 | +| N01 | 消息中心 | `pages/notification/n01-message-center` | 展示当前账号完整活动通知快照与服务端读状态,不猜业务目标 | G01 或 M01 消息入口 | 以当前内存 `snapshotKey` 进入 N02;返回来源 | 加载、未读、已读、空、失败重试、认证失效、长内容、并发刷新 | 消息与通知 | Task29 读取红灯已建立;完整活动集合、专用 required、纯文本、时区、二值状态和计数同域待关闭,见 2.13/5.6;Task30 前不得本地伪写 | +| N02 | 消息详情 | `pages/notification/n02-message-detail` | 从当前 generation 的内存快照展示完整纯文本正文,不持有服务端 ID | N01 消息卡 | 返回 N01;无快照时提示从消息中心重新打开,不跳业务页 | 加载、详情、无快照、认证失效、长正文、读状态写入待开放 | 消息与通知状态 | Task29 选定 list-owned snapshot 且删除目标 CTA;Task30 独立约束私有字符串 ID 与幂等写入,见 2.13/5.6/5.7 | +| M01 | 我的 | `pages/profile/m01-profile-home` | 展示脱敏账号身份并保持通知、服务与设置入口可达 | 根 Tab | 进入 M02、M03、M06、M08、M09、M10 或 N01 | 身份与通知局部加载、正常、失败重试、认证失效、未读数不可用、服务可用性 | 个人中心聚合 | Task28 profile GET 红灯保证身份失败不锁菜单,见 2.13/5.5;Task29 unread-count 红灯删除 fixture 伪数并统一“未读消息”,见 2.13/5.6 | +| M02 | 个人资料 | `pages/profile/m02-edit-profile` | 从唯一 profile owner 初始化并以版本化 dirty-only merge 保存昵称、真实姓名和邮箱 | M01 资料入口 | 成功应用权威响应并留在本页;放弃确认后回 M01 | 加载、失败重试、认证失效、异步 baseline、字段校验、保存、结果未知、版本冲突、账号切换、头像未接入 | 用户资料 | Task28 读取红灯仍是前置;Task31 已建立 PUT merge、清空、If-Match、typed response 和 409 红灯,头像不混入,见 2.13/5.5/5.8 | +| M03 | 账号与安全 | `pages/profile/m03-security-settings` | 展示密码入口与脱敏绑定手机号,不伪造设备安全结论 | M01 安全入口 | 进入 M04 或 M05;返回 M01 | 手机号局部加载、正常、失败重试、认证失效、功能受限 | 账号安全 | Task28 已建立 profile GET 红灯;普通读取失败不得阻断密码入口,设备状态仍无合同,见 2.13/5.5 | +| M04 | 修改密码 | `pages/profile/m04-change-password` | 以当前密码重新认证并安全更新统一密码凭证;门禁前保持本地预览 | M03 密码入口 | 确定错误留页;200/401/409/结果未知清本机并回 A01;放弃确认回 M03 | 空字段、15/64 边界、Unicode/NFC、blocklist、当前错误、并发、限流、提交中、结果未知、进程终止 | 账号安全与会话 | Task33 已建立 raw writeOnly、ALL 会话撤销、typed 错误与 session marker 红灯;四条密码 wire 必须同批迁移,见 2.13/5.10 | +| M05 | 修改手机号 | `pages/profile/m05-change-phone` | 以当前密码重新认证,并通过受保护 TAC/6 位 OTP 验证新号码;门禁前保持本地预览 | M03 手机号入口 | 确定错误留页;最终 200/401/409/结果未知清本机回 A01;放弃确认回 M03 | 当前密码、新号、TAC、发送与倒计时、6 位码、占用、限流、提交中、并发、结果未知、进程终止 | 账号安全、短信与会话 | Task34 已建立专用 SaToken 发码、全活动六位码、ALL 会话撤销、outbox 与 credential marker 红灯;依赖 M04 raw wire,见 2.13/5.11 | +| M06 | 帮助中心 | `pages/profile/m06-help-center` | 从完整帮助列表搜索、分类并展开纯文本正文,无法解决时进入反馈 | M01 帮助入口 | 返回 M01;无法解决时进入 M07 | 加载、服务端空、分类、搜索无结果、展开、失败重试、认证失效、取消 | 帮助内容 | Task27 选定列表唯一 owner 并建立红灯;专用 required、纯文本、仅发布、顺序和认证语义待后端关闭,见 2.13/5.4 | +| M07 | 意见反馈 | `pages/profile/m07-feedback` | 通过唯一真实 owner 提交必填内容及可选类型、联系方式 | M06 联系入口 | 成功留页保留提交快照;编辑后可再提交;未提交修改放弃确认回 M06 | 必填、提交中、成功防重、失败、结果未知、mock 不可提交、未保存返回 | 用户反馈 | Task25 已接 `POST /genealogy/app/feedback` 严格客户端;remote 实测与 MuMu 待认证门禁关闭,见 2.13 | +| M08 | 应用推广 | `pages/profile/m08-promotion` | 明确说明邀请码服务当前不可用 | M01 推广入口 | 查看不可用说明;返回 M01 | 无可用邀请码、服务未接入、说明弹层 | 邀请与系统分享 | 线上无邀请码签发/校验/直入合同,已删除码值、复制和海报伪能力,见 2.13 | +| M09 | VIP 与订单 | `pages/profile/m09-vip-orders` | 展示基础说明并明确订单服务当前不可用 | M01 服务入口 | 查看关闭说明;返回 M01 | 服务未开放、无订单数据、说明弹层 | 服务权益、订单与支付 | 套餐/订单端点存在但支付闭环未定义,已删除查询参数演示订单,见 2.13 | +| M10 | 关于家谱 | `pages/profile/m10-about-settings` | 从 manifest 展示版本和协议说明,并安全退出当前设备凭证族 | M01 设置入口 | 协议/退出取消留在本页;确认后立即清本机并回 A01,远端结果只更新一次性提示 | 版本、协议、隐私、退出确认、双击、pending、撤销确认/未确认/拒绝、账号竞态、根导航失败 | 配置、协议与认证 | Task9 已完成本机清理基线;Task32 已建立当前凭证族、幂等 200、logoutCoordinator、required RVoid 与部署复用红灯,见 2.13/5.9 | ## 四、封存与已移除页面 @@ -236,7 +307,8 @@ M01 我的 - 页面与动作:A04 提交注册并建立会话。 - 当前接口:`POST /genealogy/app/auth/register`。 -- 已确认响应:HTTP `200` 复用 `LoginResult`,`data` 引用 `LoginVo`,可返回 `token/accessToken/tokenValue`。 +- 已确认响应:受保护旧快照的响应形状已经过期;2026-07-22 新线上注册成功响应为 `RAppLoginVo`,`data` 引用 `AppLoginVo`,唯一会话字段为 `access_token`。密码登录与短信登录使用同一响应链。 +- 客户端合同:`utils/api.js` 只消费 `AppLoginVo.access_token`;旧字段读取已经删除。必须等同版本双导出落地后再把页面接到远端,不能把线上证据手工写回受保护源文件。 - 产品结论:取得并保存有效令牌后直接清理认证流程并进入 G01;不保留“注册成功后再登录”的并行终点。 - 尚未关闭范围:短信发送、行为验证、限流和验证码状态机不由本结论代替,按后续短信阶段单独审查。 @@ -274,9 +346,187 @@ PATCH /genealogy/app/v2/genealogies/{genealogyId}/lineage/relationships/{relatio - overview 只接受必填 `treeVersion`,版本变化返回 `409 TREE_VERSION_CHANGED`;响应以 `state=EMPTY/POPULATED` 使用 `oneOf`。EMPTY 精确为 `genealogyPersonCount=0、genealogyRootPersonIds=[]、redactedGenealogyRootCount=0、generationRange=null、buckets=[]`;POPULATED 要求正数总量、非空范围和 buckets,并满足可见根数加隐私根数至少为 1、全部 bucket 三类计数之和等于总量。可见根与 bucket `focusPersonId` 只能使用 VISIBLE 稳定 ID,隐私根只计数不返回 opaque ID。locator 将 `rootVisibility=VISIBLE/REDACTED` 与 `pathCompleteness=COMPLETE/REDACTED_GAPS` 独立建模;`ancestorPathSegments` 用 VISIBLE 人物 ID 段与不含 ID 的 REDACTED gap 段表达任意中间隐私,支持可见根但中间祖先隐藏,任何路径都不得包含 opaque ID。 - 世系写接口携带 `If-Match`,成功返回新 `treeVersion` 以及受影响人员、家庭和关系 ID;关系 PATCH 以不可变 `relationshipKind` 为 discriminator 使用 `oneOf`,PARTNER 只更新 `relationType/status`,PARENT_CHILD 只更新 `relationType/parentRole`。每个分支至少提交一个可修改字段,省略字段保持原值;只有 `relationshipKind` 的空更新返回 `422 RELATIONSHIP_PATCH_EMPTY`,不得偷换参与人。 - 客户端 Scene 根固定为 `{ sceneVersion, treeVersion, focusPersonId, bounds, items }`;`utils/lineage/scene.js` 唯一生成 `sceneVersion`,缺失版本或相同版本对应不同 payload 均拒绝原子替换。瞬时 `selectedId` 不进入 Scene 或版本摘要,renderjs 只用它在同一 Canvas 动态重绘光晕。 -- OpenAPI 必须列出 `400/401/403/404/422/429/5xx`、409、字符串 ID、nullable 头像和 `additionalProperties: false`。 +- 四条操作统一声明 `200/400/401/403/404/422/429/5XX`;tree、overview、relationship PATCH 另声明 `409`。错误响应根层唯一业务码字段为必填字符串 `businessCode`,稳定码必须在对应响应 `oneOf` 分支中用单值 enum(OpenAPI 3.1 可用 `const`)表达;关键词、description、example 和无关 metadata 都不算证明。非空 `generationRange` 固定为关闭额外字段的 `{ minGeneration, maxGeneration }`,两项均为大于等于 1 的整数,大小顺序交给运行时 validator。 - 旧 v1 树路径保持原合同;App 只实现上述四条固定 `/genealogy/app/v2/...` 路径,不双读、不运行时探测版本。 **关闭条件:** 用户从更新后的 Apifox 重新导出 JSON/YAML;两份文件同时通过 `tests/lineage-openapi-contract.ps1`;三人逐字段复核后,客户端才能开始规范化、布局和 Canvas 实施。 +### 5.3 后端问题单 API-GENEALOGY-WORKSPACE-001—003 + +**优先级:** P1;阻塞 G01/G05 切到 remote 和正式上线,不阻塞继续审查无依赖业务域。 + +**唯一 owner:** `GET /genealogy/app/genealogies/mine` 持有当前账号可访问集合;`GET /genealogy/app/genealogies/{genealogyId}/overview` 持有 G05 只读展示。首批不接语义重复的 `GET /{genealogyId}`,不让两个详情响应互相补字段。 + +**当前线上证据:** `RListAppGenealogyVo/RAppGenealogyVo/AppGenealogyVo` 均无 `required`;`AppGenealogyVo.genealogyId` 为 `integer/int64`,`roleType/status/memberStatus` 无 enum,且没有 `canView`。最大合法 int64 经 JavaScript JSON 解析会失真,解析后再转字符串无法恢复。无令牌调用 `/mine`、`/1` 和 `/1/overview` 均实测返回 HTTP 200、`application/json;charset=UTF-8`、`{code:401,msg,data:null}`;线上文档却只列 200/401,401 schema 为 `*/* string`,也没有有效 security 声明。无令牌行为已经拒绝访问,因此不能仅凭注解缺失断言已发生公开泄漏;对象级授权仍没有有效账号反例证据。 + +**API-GENEALOGY-WORKSPACE-001:无损身份与最小 schema 闭包。** `/mine` 的 200 响应固定为 `RListAppGenealogyVo`,`/overview` 固定为 `RAppGenealogyVo`;两层 envelope 的 `code/data` 必填,列表 `data` 为 `AppGenealogyVo[]`,详情 `data` 为单个 `AppGenealogyVo`。首批实体必填字段固定为 `genealogyId/genealogyName/canView/canManage/canEditContent/roleType`:ID、名称为 `minLength >= 1` 的字符串,三项 capability 为 boolean,角色为至少两个稳定非空值的正式 enum。地点、堂号、人数、简介等展示字段可选,响应不强制 `additionalProperties:false`。URL path 本身以文本传输,不因 JSON 响应问题机械强制改类型;真正必须改的是响应身份。 + +**API-GENEALOGY-WORKSPACE-002:可访问集合与能力投影。** `/mine` 中只有 `canView=true` 的行能进入 current context;也接受后端明确并由集成测试证明“只返回当前账号仍可查看 ACTIVE 家谱”的等价合同。G01 不能从未声明的 status 或 role 猜可访问性。`canManage/canEditContent` 是管理与内容入口的授权 UI 投影;真正写接口仍须后端逐次鉴权,capability 不能替代服务端授权。`roleType` 只拥有 G01 分组和角色标签,不替代 capability。 + +**API-GENEALOGY-WORKSPACE-003:错误语义与对象级授权。** 认证失效、对象无权/撤权、家谱不存在和服务故障必须在同版本文档、runtime validator 与部署行为中稳定一致。后端可使用规范 HTTP 401/403/404/5xx,也可继续 HTTP 200+稳定业务码;不能出现文档写 HTTP 错误、部署只回无字典业务码的双合同。至少用两个账号执行无凭证、跨账号、撤权、删除、服务异常和正常访问反例。G01 只在成功列表中确认 ID 消失或明确撤权时写 tombstone,网络/5xx 保留现场;G05 不把错误回退为 fixture 权限。 + +**展示取舍:** 当前 G05 fixture 还显示 `source/manager/certification/ancestorName/parentName/branchName/updatedAt/activeCount`,线上没有等价字段。首批允许把这些区域隐藏或使用明确“待补充/待同步”的非业务降级;若产品要求继续把它们作为可信首屏信息,后端需另补字段或明确组合接口。不能为了复刻 mock 把 22 个字段全部列为当前硬门禁,也不能把 mock 值带进 remote。 + +**关闭条件:** 后端从同一版本重新导出 JSON/YAML,`tests/genealogy-workspace-openapi-contract.ps1` 通过;三人复核 enum 与页面消费后,才实现专属 adapter、G01 `onShow` 刷新和 G05 `/overview` 接线。随后完成有效账号行为矩阵和 MuMu 原生状态矩阵;缺少任一层证据都不能把工作区称为可上线。 + +### 5.4 后端问题单 API-M06-001—003 + +**优先级:** P1;阻塞 M06 切到 remote 和帮助内容上线,不阻塞继续审查其他无依赖页面。 + +**唯一 owner:** 当前 M06 只使用 `GET /genealogy/app/help-articles`。线上 `HelpArticleVo` 已携带完整 `helpContent`,所以手风琴展开直接使用同一列表快照;不调用 `GET /{helpId}`,不建立文章深链、详情缓存或第二正文 owner,也不让 `helpId` 进入页面模型。 + +**当前线上证据:** 列表返回 `RListHelpArticleVo`,列表项含 `helpId/helpCategory/helpTitle/helpContent/coverOssId/sortOrder/viewCount/status/remark`,但 wrapper 与 VO 都无 `required`,正文无格式语义,分类 query 无正式值域。受保护双导出更旧,只引用通用 `ListResult/RList`,没有专用 Help schema。匿名调用列表、带任意分类列表和详情均为 HTTP 200、`application/json;charset=UTF-8`、`{code:401,msg,data:null}`;线上文档则声明 HTTP 401 string 且 operation 无有效 security。 + +**API-M06-001:专用响应与最小 schema 闭包。** 列表 200 固定引用 `RListHelpArticleVo`;envelope 的 `code/data` required,`code` 为整数,`data` 为允许空数组的 `HelpArticleVo[]`。每行只把 `helpCategory/helpTitle/helpContent` 设为 required、`minLength >= 1` 的字符串;不要求当前页面不消费的 ID、封面、浏览量、状态和排序字段。后端必须从同一版本重新导出 JSON/YAML,不允许客户端手工补 schema。 + +**API-M06-002:展示标签、正文格式与发布范围。** `helpCategory` 是去边界空白后可直接展示的标签,不是需要客户端猜字典的内部代码;“全部”只由客户端拥有。`helpContent` 首版明确为 plain text,客户端只按字面显示,不解释 HTML、Markdown、图片或外链。用户侧列表只返回当前可展示的已发布文章,响应数组顺序就是页面展示顺序。若未来需要富文本、文章深链或详情,则另立内容安全和词法字符串 `helpId` 合同,不能偷偷扩张当前批次。 + +**API-M06-003:认证和错误承载一致性。** 后端可选择规范 HTTP 401,也可继续 HTTP 200+稳定业务 `code=401`,但 SaToken/security、JSON 媒体、OpenAPI 响应、部署行为和客户端 validator 必须一致。有效/失效令牌、空列表、畸形列表、5xx、超时和取消都要有集成反例;失败不得被解释成服务端空列表,也不得回退本地 FAQ 冒充线上成功。 + +**客户端关闭后的唯一形状:** adapter allowlist 输出 `{renderKey,category,title,content}`,其中 key 只由当前 response generation 与映射前 ordinal 组成;输入即使含 unsafe 或重复 `helpId` 也必须完全丢弃。筛选作用于已映射数组,搜索/分类/刷新前清空展开,旧 generation 迟到响应不得替换新列表。页面必须区分加载、服务端空、搜索无结果、错误重试和认证失效,并用原生按钮、`aria-pressed/aria-expanded/aria-controls`、状态播报及至少 44dp 目标完成无障碍闭环。 + +**关闭条件:** 后端同版本 JSON/YAML 通过 `tests/help-center-openapi-contract.ps1`;三人复核后才写专属 adapter 和页面异步状态。任务 23 关闭认证门禁后,完成有效账号部署矩阵及 MuMu 的系统字号、TalkBack、焦点、长正文、Android 返回和 M06→M07 验收。详情端点不属于本关闭条件。 + +### 5.5 后端问题单 API-PROFILE-READ-001—003 + +**优先级:** P1;阻塞 M01/M02/M03 使用真实资料和正式 remote 发布,不阻塞继续审查通知等其他只读域。 + +**唯一 owner:** `GET /genealogy/app/auth/profile` 持有当前登录账号资料。M01 身份卡、M02 表单初值和 M03 绑定手机号行都调用同一个窄 adapter,但不建立跨账号缓存、不通过路由传 PII。M05 当前手机号在 remote 发布前也必须消费同一脱敏结果或隐藏;这不等于提前接入换绑写接口。 + +**当前线上证据:** 200 已返回 `RAppProfileVo → AppProfileVo`,实体含 `userId/tenantId/userNo/phone/nickName/realName/avatar/sex/birthday/email/registerSource/loginIp/loginDate/status/clientKey/deviceType`,但 wrapper 与 VO 无 required,phone 无 pattern,姓名/邮箱无 length/format。受保护双导出仍为通用 `ObjectResult/RObject`。匿名 GET 实测 HTTP 200、`application/json;charset=UTF-8`、`{code:401,msg,data:null}`;线上文档则声明 HTTP 401 string,operation 只有“需要登录”文字而无有效 security/clientid。 + +**API-PROFILE-READ-001:专用响应与最小字段闭包。** 200 固定引用 `RAppProfileVo`,envelope 的 `code/data` required,`data` 引用 `AppProfileVo`。实体只 required `phone`,其值必须匹配 canonical `^1[3-9]\d{9}$`。`nickName/realName/email` 都是声明过的可选属性;出现时必须为非空且去边界空白的字符串,姓名长度 1—30,邮箱长度 1—100 且 `format: email`。不要求当前页面不消费的 ID、头像、状态、设备和登录审计字段。 + +**API-PROFILE-READ-002:可选值与隐私投影。** 三项可选字段唯一未设置形态是属性省略,不再并行接受 null、空串和缺失。adapter 固定输出 `{maskedPhone,phoneAccessibleLabel,nickName,realName,email}`,真正缺席的可选值规范为内部空串;出现但非法则整份失败。明文手机号只在函数局部校验后立即变为掩码和“绑定手机号,尾号 xxxx”读屏标签,不得进入页面模型、缓存、路由、日志或错误。`userId/avatar` 即使是 unsafe int64 也通过 allowlist 完全丢弃,不做 `String(number)`。 + +**API-PROFILE-READ-003:认证、配置与错误一致性。** 后端可选择规范 HTTP 401 或 HTTP 200+稳定业务 401,但 required clientid、SaToken/security、JSON 媒体、OpenAPI 与部署必须一致。客户端运行模式经 `resolveRuntimeMode()` 校验,错误 remote 配置不得静默回 fixture;读取使用严格 envelope、15 秒超时、取消和 generation 防迟到。账号失效交给 session owner,network/timeout/5xx/畸形数据是可重试读取失败,不存在写请求 uncertain。 + +**页面与后续写边界:** M01 资料失败只替换身份卡,菜单和底栏保持;查询参数假错误、fixture“创建者”和 remote 下伪通知数删除。M02 异步填表后才建立 baseline,GET 不证明 PUT;未来写批次必须验证省略字段保持、dirty-only payload、清空语义和并发。M03 仅手机号行局部失败,密码入口保持。头像、M04/M05 写入和设备管理不混入本批。 + +**关闭条件:** 后端从同一版本重新导出 JSON/YAML 并通过 `tests/profile-openapi-contract.ps1`;三人复核后才实现唯一 normalizer、三页局部状态与 M05 脱敏展示迁移。随后以不同资料完整度账号验证掩码、401、畸形响应、5xx、超时、账号切换和取消,并在 MuMu 完成系统字号、TalkBack、焦点、键盘、长昵称和返回流程。 + +### 5.6 后端问题单 API-NOTIFICATION-READ-001—003 + +**优先级:** P1;阻塞 N01/N02、M01/G01 未读数使用真实通知和正式 remote 发布,不阻塞继续审查其他业务域。 + +**唯一 owner:** `GET /genealogy/app/notifications` 不带 `readStatus` 时持有当前账号完整活动通知集合;`GET /genealogy/app/notifications/unread-count` 持有同一集合的未读数量。当前没有详情端点,N02 只消费列表成功响应形成的不可变内存快照,不建立第二正文 owner。 + +**当前线上证据:** 线上列表为 `RListNotificationVo → NotificationVo[]`,未读数为 `RLong`;两者和实体都无 required。`NotificationVo` 的 `notificationId/genealogyId/senderUserId/bizId` 是 int64,`readStatus` 无 enum,标题/正文无长度与格式,列表无分页、容量、完整性和排序。受保护双导出列表仍引用通用 `ListResult/RList` 且完全没有 unread-count。匿名 list/count 均实测 HTTP 200、`application/json;charset=UTF-8`、`{code:401,msg,data:null}`,而线上文档声明 HTTP 401 string,operation 无有效 security/clientid。 + +**API-NOTIFICATION-READ-001:专用响应与最小字段闭包。** 列表 200 固定 `RListNotificationVo`,未读数 200 固定 `RNotificationUnreadCount`;两层 `code/data` required,`code` 为 integer。列表 data 是允许为空且 `maxItems` 不超过 200 的 `NotificationVo[]`;实体 required `noticeTitle/noticeContent/publishTime/readStatus`。标题为 1—50 字符;正文为 1—1000 字符、完整未截断 plain text;时间是带 `Z` 或显式 offset 的 RFC3339 date-time;状态只允许 `READ/UNREAD`。计数是 0—200 的 int32。 + +**API-NOTIFICATION-READ-002:完整活动集合与快照。** 服务端活动集合本身最多 200 条;列表无筛选时完整返回该集合并按最新优先,未读数精确等于同一集合中 `readStatus=UNREAD` 的数量。两个请求之间并发变化允许瞬时差异,不要求客户端强行相等。adapter 公开 `{snapshotKey,title,content,publishedAt,unread}`,key 为成功响应 generation+映射前 ordinal;筛选不重编号。成功刷新原子替换快照,退出/账号切换清空且不落盘;N02 无 key 时提示“请返回消息中心重新打开”。 + +**API-NOTIFICATION-READ-003:认证、错误与内容安全。** 后端统一 required clientid、SaToken/security、JSON 媒体、HTTP 401 或业务 401 的文档与部署行为。客户端拒绝无时区时间、未知状态、空/超长标题正文和畸形 envelope;失败不得回退 fixture。首批丢弃所有 ID、sender、`noticeType/bizType/bizId`,不解释 HTML/Markdown/URL,不执行目标跳转;未知业务通知仍完整显示内容。 + +**客户端关闭后的唯一行为:** N01 摘要最多 160 个 Unicode 字素并可换行,N02 显示同一快照完整正文;M01/G01 共同调用 count owner,文案为“未读消息”,可见 `99+` 但读屏播报真实数。读取批次原子删除 fixture 未读数、本地已读 mutation、通用 G10/审核 CTA、N02 目标按钮和假重试;写能力等待 5.7。 + +**关闭条件:** 后端同版本 JSON/YAML 通过 `tests/notification-read-openapi-contract.ps1`;三人复核后才实现读取 adapter 和四页局部状态。认证门禁关闭后完成 0/1/99/100/200、并发、畸形响应和账号切换反例,再在 MuMu 验证系统字号、TalkBack、键盘/焦点、长文本、刷新、N01→N02 和 Android 返回。 + +### 5.7 后端问题单 API-NOTIFICATION-STATE-001—003 + +**优先级:** P1;阻塞真实单条/全部已读和四页状态收敛。必须在 5.6 读取批次之后独立实施,不能与读取代码混成一个不可验证批次。 + +**唯一 owner:** `POST /genealogy/app/notifications/{notificationId}/read` 持有单条已读,`POST /genealogy/app/notifications/read-all` 持有全部已读。两者无 request body,成功精确返回 `RVoid`;页面只把 `snapshotKey` 交给 notification controller,由 controller 私有解析服务端 ID。 + +**当前线上证据:** 两条 POST 已存在且返回 `RVoid`,但 path 和 `NotificationVo.notificationId` 都是 int64;最大值进入 JavaScript 会失真。`RVoid.code` 未 required,操作没有正式幂等、重试、超时未知、当前账号作用域、跨账号/不存在、read-all 截止点、并发新消息或刷新收敛语义。 + +**API-NOTIFICATION-STATE-001:无损身份和响应闭包。** `NotificationVo.notificationId` 与 path 参数必须同为 required 的 1—128 位 URL-safe opaque string,pattern 固定 `^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$`;禁止 int64 双读或解析后转字符串。`RVoid.code` 为 required integer,两个 POST 都要求 SaToken 和 required string clientid。 + +**API-NOTIFICATION-STATE-002:幂等与并发截止点。** 两个操作均对当前账号幂等,重复调用成功且没有重复副作用。read-all 以服务端接收请求时当前账号已存在的活动通知为截止集合,之后并发到达的通知保持未读;成功后客户端重取列表和 count。超时、断网、408/5xx 或畸形响应属于结果未知,允许依靠服务端幂等安全重试或先读取状态收敛,不能本地递减计数猜结果。 + +**API-NOTIFICATION-STATE-003:对象隔离和错误一致性。** 无效会话使用稳定 401;单条 ID 不存在或属于其他账号时统一返回 404 `NOTIFICATION_NOT_AVAILABLE`,避免暴露存在性。文档、部署、错误 envelope 与客户端 validator 必须一致,并以两个账号、删除消息、多端并发和迟到响应做反例。 + +**客户端迁移与关闭条件:** 写合同通过后,controller 才能私有保留 server ID,并原子删除读取首版“内部也完全丢弃 ID”的实现以及 N01/N02 的 clone mutation;公开页面模型、路由、日志和持久存储仍不得出现 ID。后端同版本 JSON/YAML 必须通过 `tests/notification-read-state-openapi-contract.ps1`,随后完成真实账号与 MuMu 的重复点击、提交中、失败/未知播报及 N01/N02/M01/G01 收敛矩阵。 + +### 5.8 后端问题单 API-PROFILE-UPDATE-001—004 + +**优先级:** P1;阻塞 M02 真实保存和 profile 正式 remote 发布。Task28 的 GET 是前置依赖;头像、相册权限、OSS、性别、生日、密码和手机号均不属于本问题单。 + +**唯一 owner:** 继续使用 `PUT /genealogy/app/auth/profile`,不再增加 PATCH。operation 必须把自身定义为字段级原子 merge update:出现的可编辑属性更新,省略的可编辑属性保持不变;重复相同字段集只设置同一状态,不产生重复通知等额外业务副作用。请求专用 owner 命名为 `AppProfileMergeUpdateBody`,避免旧 `ProfileUpdateBody/AppProfileUpdateBody` 被误当资源替换。 + +**当前双版本证据:** 受保护双导出的 PUT 使用 `ProfileUpdateBody`,只有 nickName、avatarOssId、sex、birthday 和省市区,没有 realName/email;示例又含 schema 外 `regionCode/addressDetail`,成功返回 generic `RObject`。线上变为 `AppProfileUpdateBody`,含 nickName/realName/avatar/sex/birthday/email,但无 required、`minProperties`、关闭额外字段、merge/clear/version;200 为 `*/* → RAppProfileVo`,实体与 envelope 无 required,operation 无 security/clientid,只列 200/401。两者都不能证明安全写入;未发送真实 PUT。 + +**API-PROFILE-UPDATE-001:最小 dirty command。** `AppProfileMergeUpdateBody` 是 `additionalProperties:false`、`minProperties:1/maxProperties:3` 的对象,属性集合精确为 nickName/realName/email 且均非 required。nickName 出现时为无边界空白的 1—30 字符,空串/null 非法;realName/email 分别以 `oneOf` 区分精确 `""` clear 命令与非空规范值,非空 realName 1—30,email 1—100 且 format=email。省略保持;纯空白和边界空白拒绝,服务端清库后响应省略该属性。 + +**API-PROFILE-UPDATE-002:单一版本并发。** `AppProfileVo.profileVersion` required,固定为 1—128 位 URL-safe opaque string;PUT required `If-Match` 采用同形状,body 不重复 version。服务端以当前账号和租户做原子 CAS;成功返回新版本,旧版本固定 HTTP 409 与 `RProfileVersionChanged.businessCode=PROFILE_VERSION_CHANGED`,不得 last-write-wins。H5 正式 origin 的 CORS 必须允许 `If-Match`。 + +**API-PROFILE-UPDATE-003:typed 响应、认证、错误与隐私。** 200 精确 `application/json → RAppProfileVo`,envelope `code/data` required,data 是完整 canonical profile;400/401/409/422/429/500 均进入同版本文档。operation required SaToken 和 string clientid;GET/PUT 资料响应声明并实测 `Cache-Control: private, no-store`。422 只返回 nickName/realName/email 的结构化字段错误。客户端及服务端日志、路由、持久缓存、遥测和异常不得含真实姓名、邮箱或请求/响应 payload。 + +**API-PROFILE-UPDATE-004:结果未知与账号隔离。** timeout、network、408/5xx、取消和畸形 200 均视为 outcome unknown;客户端先 GET 对账本次脏字段,全匹配确认成功、仍为旧 baseline 才允许重试、第三值或无法归因版本进入 conflict。session generation 变化时清草稿并拒绝迟到响应;RequestTask 取消不表示服务端未写。 + +**页面迁移与关闭条件:** 首次 GET 后才建立 baseline,clean 不发请求,saving 冻结三输入和返回;成功应用响应并重置 baseline,失败/unknown/conflict 保留草稿。原子删除 `currentUser.name` 同时冒充昵称/实名、500ms 假保存、API 禁用旧测试断言、假头像按钮和“邮箱用于接收通知”无依据承诺。后端同版本双导出通过 `tests/profile-update-openapi-contract.ps1` 后,才依次实现 normalizer、API、M02 状态机;再用两个账号/多端并发、超时对账和正式 H5 CORS 验证,最终在 MuMu 检查键盘、TalkBack、错误聚焦、长文本、冲突与返回。 + +### 5.9 后端问题单 API-LOGOUT-001—003 + +**优先级:** P1;阻塞 M10 服务端撤销和正式 remote 退出闭环。现有本机 `session.clear()` 仍保留为任何网络状态下的安全底线,但不能冒充服务端成功。 + +**唯一 owner 与当前证据:** `DELETE /genealogy/app/auth/logout` 无 body。受保护双导出有 required clientid、SaToken 和 200 `RVoid`,但只列 200、RVoid 无 required,未定义 scope/幂等/撤销传播。线上只有 200 RVoid 与 401 string,媒体为 `*/*`,operation 无 security/clientid;同样没有 scope、复用和其他设备反例。页面当前只执行一次本地清理并根跳转,相关测试没有远端请求、迟到 A/B 账号竞态或离线状态。 + +**API-LOGOUT-001:当前凭证族范围与撤销传播。** DELETE 只撤销 bearer 所属当前设备 credential family,包括同一登录会话的 refresh 能力;同账号其他设备 token 保持有效。200 必须表示撤销已传播至所有鉴权节点:旧 access 不能访问任一受保护接口,旧 refresh 不能换新 access。已经鉴权通过的并发业务请求不属于可回滚范围;全设备退出必须另接口。 + +**API-LOGOUT-002:唯一幂等成功和拒绝。** 能验证为该 client 历史签发的 active、revoked、expired credential 重复 DELETE 都返回相同 200 RVoid且无额外副作用。伪造、格式非法或 client 不匹配才返回 HTTP 401 `RLogoutRejected`,required `code/businessCode`,businessCode 只允许 `TOKEN_INVALID/TOKEN_CLIENT_MISMATCH`;这些拒绝不算远端撤销成功。不得长期并存 200 业务 401、HTTP 401 string 和 typed JSON 三种合同。 + +**API-LOGOUT-003:安全、媒体、缓存和反例。** operation required SaToken 与非空 string clientid,并验证 clientid 与 token client 绑定;200/401 为 application/json,`Cache-Control: private, no-store`,RVoid required integer code,另声明 400/429/500。以同账号两设备 token A/B 验证:A 删除后全受保护接口拒绝 A,重复 A 仍 200,B 保持有效;再验证 expired、伪造、client mismatch、跨鉴权节点传播、弱网/超时和正式 H5 Authorization/clientid CORS。服务端日志不得记录 bearer。 + +**客户端关闭后的唯一流程:** `logoutCoordinator` 同步捕获 A token/clientid/epoch,立即经 session owner bump epoch 并清全部账号态,再用显式 A 创建后台 RequestTask且立即 `goRoot(A01)`;M10 不持 token,请求不绑定页面 controller。coordinator 仅保存 attemptId/logoutEpoch/status,绝不在异步 finally 再 clear;A01 只在 session 为空且 epoch 未变时消费一次状态,B 登录后丢弃 A 迟到结果。所有分支都承诺“已从本机退出”,再区分 confirmed/unconfirmed/not-revoked;不持久 token、不跨重启重试、不阻塞重新登录。 + +**关闭条件:** 后端同版本 JSON/YAML 通过 `tests/logout-openapi-contract.ps1`;三人复核后才实现 session epoch、coordinator、严格 API 和 A01 提示,并原子替换 M10/导航/NM 旧静态断言。随后完成两设备部署矩阵和 MuMu 的确认、双击、系统返回、网络异常、状态播报、快速重新登录与根导航失败验收。 + +### 5.10 后端问题单 API-PASSWORD-001—005 + +**优先级:** P0;同时阻塞 A01 密码登录、A04 注册、A05 找回后的新密码验证、M04 登录态改密与正式 remote 模式。当前 M04 本地预览不得冒充修改成功。 + +**当前三方证据:** 受保护双导出的 `PasswordLoginBody/PasswordRegisterBody/PasswordResetBody/PasswordChangeBody` 都把密码写成静态 32 个十六进制字符 MD5;改密虽有 SaToken/clientid,却只有 200 RVoid,RVoid 无 required。线上相应 `AppPassword*Body` 仍接受大小写 MD5,M04 只有 200 `*/* → RVoid` 与 401 string,operation 无 security/clientid;live server 还发布 HTTP URL。现有 M04 只有 500ms 本地定时器,旧测试正确禁止提前导入 API;未发送 PUT。 + +**API-PASSWORD-001:唯一 raw wire 与策略 owner。** 新增 `CurrentPasswordSecret` 和 `NewPasswordSecret` 两个共享 schema。登录 password 与改密 oldPassword 只能引用前者,1—64 Unicode code point、原样不 trim;注册、找回和改密 newPassword 只能引用后者,NFC 后 15—64 code point,允许空格/Unicode/粘贴/密码管理器且无组成规则。四条入口在同一版本删除 MD5 与任何 raw/hash oneOf fallback;服务端执行常见/泄露密码 blocklist、账号限速、新旧不同与带独立盐的 Argon2id,无法使用时才选合规 scrypt/PBKDF2。confirm 永不出端。 + +**API-PASSWORD-002:重新认证、ALL session 与原子 CAS。** M04 以当前密码重新认证,TAC 不能替代;严格 200 前在同一安全事务中写入新 verifier、递增账号 credentialEpoch,并跨节点撤销所有设备/所有 client 的既有 access、refresh 与 renewal session,包括调用者。两个同旧密码并发请求至多一个 200,另一个 typed 409 `CREDENTIAL_VERSION_CONFLICT`。不返回新 token,不保留旧 bearer。 + +**API-PASSWORD-003:typed 错误与确定未写边界。** PUT 声明 200/400/401/409/422/429/500;409/422 的 `RPasswordChangeRejected.businessCode` 精确为 `CREDENTIAL_VERSION_CONFLICT/CURRENT_PASSWORD_INCORRECT/NEW_PASSWORD_SAME_AS_CURRENT/PASSWORD_POLICY_VIOLATION`。400/422/429 明确保证未修改;401/409 进入重新登录;network/timeout/取消/畸形 2xx/5xx 均为结果未知,客户端不得自动重试或解析 msg。 + +**API-PASSWORD-004:鉴权、媒体、缓存与秘密卫生。** required SaToken、与 token client 绑定的非空 clientid、关闭额外字段的 JSON body;所有响应 application/json 且 `Cache-Control: private, no-store`,429 required `Retry-After`,RVoid integer code required。OpenAPI server 和实际重定向全程 HTTPS。反向代理、应用日志、APM、分析、崩溃报告与错误 body 不记录 old/new/confirm、MD5、Authorization 或完整请求。 + +**API-PASSWORD-005:账号能力和客户端崩溃边界。** 后端明确所有 App 账号是否都已配置密码;若不是,profile 返回稳定 `passwordConfigured` 并让无密码账号进入独立 step-up 设置流程,M04 不猜。客户端 session owner 在 dispatch 前只持久化 `{sessionEpoch,startedAt}` 的 `credentialChangeInFlight`;确定未写清 marker,200/401/409/unknown 清同 epoch 账号态并回 A01。冷启动同 epoch marker 在任何缓存渲染前 fail closed,新登录 bump epoch,迟到旧响应不得清新账号。禁止持久 token、密码、摘要、body、operation 状态或自动重试。 + +**关闭条件:** 后端同版本 JSON/YAML 通过 `tests/password-change-openapi-contract.ps1`,并先关闭密码登录 TAC 门禁;三人复核后按共享策略→四条 API wire→session epoch/marker→M04 状态机顺序原子实施,删除 `calcMD5` 生产消费者、8—32 旧规则和 NM preview 断言。最后以两设备全部 access/refresh 撤销、并发/fault injection、秘密日志扫描、正式 CORS/HTTPS 和 MuMu 的密码管理器、系统字号、TalkBack、44dp、错误聚焦、Android 返回与跨根提示验收。 + +### 5.11 后端问题单 API-PHONE-001—005 + +**优先级:** P0;阻塞 M05、全活动短信码生产强度及正式 remote 模式,并依赖 M04 raw-password 和认证/TAC 门禁先关闭。当前 M05 只做本地 4 位码校验,不得冒充换绑。 + +**当前三方证据:** 本地 `PhoneChangeBody` 要求 `clientId/phone/smsCode`,线上 `AppPhoneChangeBody` 只要求 `phone/smsCode`;两边都是 4 位码,都没有 currentPassword、号码占用、并发、会话撤销、outbox 或结果未知语义。线上 PUT 无有效 security/clientid且返回完整 `RAppProfileVo`,错误只有 401 string;共享发码 operation 在线上明确忽略权限,当前客户端方法也固定不携 bearer。页面使用脱敏 fixture、70rpx `view role=button` 和 500ms 定时器,未调用 API/TAC;未发送 POST/PUT、短信,未操作 MuMu。 + +**API-PHONE-001:专用受保护发码 operation。** 新增 `POST /genealogy/app/auth/phone/sms/code`,required SaToken 与非空 clientid,闭合 `PhoneChangeSmsCodeBody` 只含 `phone/validToken`;服务端固定 scene=`APP_PHONE_CHANGE`,不接受 sceneCode/clientId/tenantId/grantType。公共 `/auth/sms/code` 删除该 scene。两个 operation 复用同一 OTP 服务 owner;匿名专用调用必须 401,公开登录/注册/找回发码仍可匿名。validToken 必须绑定当前账号/session、tenant、client、scene 与规范化新号并单次消费。 + +**API-PHONE-002:唯一六位 OTP wire 与生命周期。** 新增 `SmsCodeSecret`:CSPRNG 生成恰好 6 位 ASCII 数字、保留前导零、writeOnly、无示例;5 分钟 TTL、60 秒重发、最多 5 次失败、单次消费,重发废止旧 generation且不重置累计失败次数。同一复合键只有一条 active generation。A01/A04/A05/M05、`AccountDeactivateBody` 及同源生成器、短信模板、双导出、validator、页面和测试同版删除全部 4 位规则,不保留 4/6 fallback。 + +**API-PHONE-003:existing-factor 与闭合最终 PUT。** `PUT /genealogy/app/auth/phone` required SaToken/clientid,`PhoneChangeBody` 只含 required `currentPassword/phone/smsCode` 且关闭额外字段;密码引用 `CurrentPasswordSecret`,新号引用 11 位 `NewBoundPhone`,短信引用 `SmsCodeSecret`。当前密码是既有因子再认证,TAC 不能代替;无密码账号返回 `STEP_UP_UNAVAILABLE` 进入独立恢复,不能降级为 bearer+新号 OTP。不要求旧号 OTP,成功后改用旧号安全通知。 + +**API-PHONE-004:原子换绑、唯一约束与会话。** 在一个事务中验证 currentPassword/active OTP、执行 `(tenantId,canonicalPhone)` 唯一约束、消费 OTP、CAS 更新号码、递增 credentialEpoch、撤销包括当前在内的全部 access/refresh/renewal session,并持久化旧号通知 outbox;严格 200 只返回 `RVoid`。并发至多一笔成功;通知投递失败不回滚换绑,但 outbox 必须重试并告警。不得在证明新号控制权前泄露号码是否已绑定。 + +**API-PHONE-005:typed 错误、传输与客户端恢复。** POST/PUT 都声明 200/400/401/409/422/429/500 JSON、`private, no-store`,429 有 `Retry-After`;409/422/429 使用 required `RPhoneChangeRejected.code/businessCode`,稳定覆盖 current password、同号/占用、验证码错误/过期/尝试耗尽、credential 冲突、step-up 不可用、验证重做与限流,客户端不解析 msg。最终 PUT dispatch 前复用无秘密 `{sessionEpoch,startedAt}` marker;200/401/409/unknown 清同 epoch 账号态回 A01,不自动重试。HTTPS、Authorization/clientid CORS 和密码/手机号/OTP/TAC/token 全链路日志脱敏必须实测。 + +**关闭条件:** 同版本 JSON/YAML 通过 `tests/phone-change-openapi-contract.ps1`,且认证、密码和 profile 读取前置门禁全部通过;三人复核后按全活动六位码→专用发码 API→共享 credential marker→M05 状态机原子实施,替换旧四位/preview 断言。最后完成匿名/错场景/TAC 重放、前导零、重发/过期/限流、号码唯一与枚举、两设备并发、全部 session 撤销、fault injection、旧号 outbox 和 MuMu 的输入法、TalkBack、44dp、系统返回与结果未知矩阵。 + +### 5.12 后端问题单 API-G03-001—005 + +**优先级:** P0;阻塞 G03 真实创建、创建后 G01/G05/context 闭环及 APP 家谱访问规则唯一化。当前同页两步是明确本地预览,不得把 `local-created-*` 或 fixture mutation 当作后端成功。 + +**当前三方证据与方案结论:** 本地 `GenealogyCreateBody` 和线上 `AppGenealogyCreateBody` 都只创建家谱,通用人物 POST 另写始祖;创建响应未形成 required 词法 ID/OWNER/READY 回执。页面缺可信 regionCode,默认男性、硬限 1800 年、把“一世”混入 generationName;成功不安装真实 context。三人先设计 `ROOT_REQUIRED` 两写及恢复,再确认没有跨库或保存空谱需求,最终否决这类客户端 saga:它只会新增半成品配额、可见性、删除/过期、版本、G01 恢复卡和第二次未知结果。唯一最小生产方案是最终按钮一次原子 bootstrap,第一步零网络写。 + +**API-G03-001:闭合 bootstrap 与领域事务。** `POST /genealogy/app/genealogies` 唯一 body 改为 additionalProperties=false 的 `AppGenealogyBootstrapBody`,字段精确为 `genealogyName/surname/ancestralHall/regionCode/accessPreset/rootPerson`,除堂号外全部 required;rootPerson 只含 `name/sex/birthDate/biography` 且前两项 required。sex=`MALE/FEMALE/UNKNOWN`,生日 format=date,服务端固定 generation=1、唯一首根且不接收账号/编号/父母/字辈/状态。严格 200 前一个事务完成 quota、谱、OWNER、根、READY 和幂等回执,失败全回滚;通用人物 POST 仅用于 READY 后普通人物。同名不是冲突,重复提醒只做建议。数据库 bootstrap-root marker 是身份权威:根 PUT 可编辑白名单精确只有 `name/sex/birthDate/biography`,status/personStatus、账号绑定、世代、父母、根标记及任何白名单外字段一律 422;collection POST、人物 DELETE 和 parents mutation 也不能创建第二根、删除根或给根重挂父母。 + +**API-G03-002:幂等键、控制事务、结果和稳定错误。** required `Idempotency-Key` 引用 `GenealogyBootstrapOperationKey`,精确格式为 `gcb.{13位 issuedAt 毫秒}.{22—43位 base64url CSPRNG}`,随机量至少 128 位;固定 `acceptUntil=issuedAt+10 分钟`,以 server time 判定,未来超过 5 分钟返回 400 `OPERATION_KEY_INVALID`,并以 600/300 秒 extension 锁定。窗口内首次 POST 用短控制事务按 account/tenant/client/path/key 唯一 CAS 认领 PENDING、canonical digest、fencing lease 与 `resolveBy<=claimedAt+2 分钟`,以 120 秒 extension 锁定;相同作用域/key/body 的已存在操作在截止后仍返回同一结果,不同 digest 返回 409 `IDEMPOTENCY_KEY_REUSED`,过期且不存在的 key 返回 409 `OPERATION_KEY_EXPIRED`。业务事务才原子处理 quota、谱、OWNER、唯一根、READY 和 SUCCEEDED;失败回滚后 CAS FAILED_NO_COMMIT,watchdog 同样用 fencing CAS,旧 worker 不能迟交。`GenealogyBootstrapResult` required 词法字符串 genealogyId/rootPersonId、setupState=READY、roleType=OWNER、canView=true;成功防重记录至少覆盖实体生命周期。POST 声明状态专属、`code` 与 HTTP 状态单值一致的 400/401/403/409/422/429/500 typed JSON、private/no-store,429 有 Retry-After。 + +**API-G03-003:无 PII operation-status 与迟到竞态。** 新增 required SaToken/clientid 的 `GET /genealogy/app/genealogy-bootstrap-operations/{operationKey}`,有效参数只有 operationKey/clientid且没有 request body,响应集精确为 200/400/401/404/429/500,禁止泄漏性 403/default。响应以带显式 mapping 的 discriminator `oneOf` 关闭为 `PENDING{resolveBy,retryAfterSeconds}`、`SUCCEEDED{result}`、`FAILED_NO_COMMIT`,三个 status 均为单值 string;`x-state-transitions` 精确登记 `ABSENT→PENDING→SUCCEEDED/FAILED_NO_COMMIT`,两个终态无出边且 `x-terminal-immutable=true`,FAILED 同时固定 `x-domain-effects=NONE/x-quota-consumed=false`。PENDING 的 retryAfterSeconds 为 1—30,200 不强制 Retry-After。GET 必须纯读且始终无副作用:acceptUntil 前无记录返回 typed 404 `BOOTSTRAP_OPERATION_NOT_AVAILABLE`、服务端 acceptUntil 和 Retry-After,客户端保持 unknown;截止后无记录按 key 可计算地返回 FAILED_NO_COMMIT,不写 tombstone,迟到 POST 永久拒绝。PENDING 最迟 claimedAt 后 2 分钟终结;SUCCEEDED 记录至少保留实体生命周期,FAILED 至少 30 天;跨 account/tenant/client 统一不泄漏 404。响应不含原请求或人物 PII,operation 不进 `/mine`、不占业务 quota;GET 零写与 FAILED 零领域提交仍必须另以 DB 观测测试证明。 + +**API-G03-004:APP 访问预设单一 owner。** 新 `GenealogyAccessPreset` 只允许 MEMBER_ONLY/PUBLIC_APPLY,并在同一版本成为 `AppGenealogyBootstrapBody`、实际 `/mine`/overview 读取所用 `AppGenealogyVo` 和闭合 `AppGenealogySettingsUpdateBody` 的唯一访问字段。删除 `GenealogyCreateBody/AppGenealogyCreateBody/GenealogyUpdateBody/AppGenealogyUpdateBody` 旧入口以及 visibility/joinMode;不保留 oneOf fallback、数字 pair 或邀请码 mode 的暗中映射。validator、runtime、双导出、fixture 迁移、G03/G05/G11 测试和文档同批更新。这里只关闭共享字段迁移;G11 写入仍须另行完成 If-Match、版本/CAS、权限刷新和结果未知门禁。 + +**API-G03-005:可信地区、始祖不变量、认证与部署反例。** `GenealogyRegionCode` 是 1—32 位 URL-safe 词法标识;唯一地区 owner 改为 required SaToken/clientid 的 `GET /genealogy/app/region/search`,同版删除旧公共 `/genealogy/region/search`。keyword required 且 minLength=1/maxLength≤50;`RListRegionSelectVo.code/data` 和 `RegionSelectVo.regionCode/label/selectable` required,leaf 不等于 selectable,不强制层级;页面只展示 label、提交 code,POST 在业务事务中复验仍可选。通用人物写入以 typed 409 `GENEALOGY_NOT_READY` 和 422 `BOOTSTRAP_ROOT_IMMUTABLE` 覆盖 collection/PUT/DELETE/parents;PUT operation 以 `x-bootstrap-root-editable-fields=[name,sex,birthDate,biography]` 和 `x-bootstrap-root-noneditable-policy=REJECT_422_BOOTSTRAP_ROOT_IMMUTABLE` 精确锁定仅四项可编辑,其余字段一律 422。create/status/settings/region及相关人物私有响应必须是 JSON+private/no-store;同一后端模型重导后先由 `openapi-yaml-json-parity-runtime-smoke.js` 以无损任意精度数字和严格 YAML mapping 语法深比较完整 JSON/YAML,再递归检查组合 schema 字段。以匿名、错 client、跨账号 status、unsafe 数字 ID 差一、地区失效、quota race、同 key 并发、control/business/terminal 各写点 fault injection、GET 零写、根 PUT 白名单及其他绕过、超时/5xx/畸形响应和正式 HTTPS/CORS 验证文档与部署一致。 + +**客户端关闭后的唯一流程:** 第一步只校验并进入页内始祖步骤;最终校验后冻结 canonical snapshot,先持久 `{sessionEpoch,operationKey,startedAt}` 再 POST。本地校验或可证明零发出的 request-build 失败不留 marker;服务端在 claim 前返回的 400/401/403 清 marker,401 同时清会话;`IDEMPOTENCY_KEY_REUSED` 进入 fatal/quarantined,不查装 status、不自动换 key,只有用户看到警告并显式放弃才清;确定未提交的 limit/expired/422 可清。429 保持同 key/body并先查 status;500/network/timeout/408/发出后取消/意外 2xx/3xx/畸形 200 都保持 marker按 unknown 查询。status 截止前 404 保持,400 清损坏 marker,401 走会话失效,429/500/network/cancel/unexpected/malformed 保持退避,FAILED_NO_COMMIT 才允许新 key。冷启动只查 status,不保存姓名/生日/生平、完整 body或可逆日志。SUCCEEDED 唯一次序为 committed receipt → 失效或定点更新 `/mine` → 安装 context → G05;context/导航失败不重发创建。落地时删除 local preview/mock create 与旧禁止 API 断言,不能长期并存两个创建 owner。 + +**页面与关闭条件:** 地区搜索、PENDING、unknown、fatal/quarantined、committed、context/导航失败必须可见;默认 UNKNOWN,删除 1800/UTC 日期错误;label、radio、aria-invalid/describedby、首错聚焦、至少 44dp 原生按钮、AppDialog 焦点与状态播报同批实现。后端同版本 JSON/YAML 必须通过 `tests/g03-bootstrap-openapi-contract.ps1`,客户端实现必须另行通过会实际执行状态机套件的 `tests/g03-bootstrap-client-release-gate.ps1`;它们只是 G03 自身两门禁,真实开放还要求 Task26 workspace 读取门禁、聚焦/全量回归和 MuMu 原生字号、TalkBack、键盘、慢网、双击、杀进程及 Android 返回矩阵全部通过。 + 当前其余已知但尚未核实的重点依赖包括:微信登录、公共行为验证、完整短信状态机、邀请码验证与直接加入、结构化亲属关系、上级家谱与支系权限、管理员授权与功能开关、上传与系统权限、消息业务目标、系统分享、订单支付与退款。它们只表示审查重点,不预判后端一定缺失。 diff --git a/docs/视觉资产与构建基线.md b/docs/视觉资产与构建基线.md index 9030d9a..556bbd6 100644 --- a/docs/视觉资产与构建基线.md +++ b/docs/视觉资产与构建基线.md @@ -13,6 +13,8 @@ schema v3 注册入口是 `design-pipeline/manifests/runtime-assets.json`。任何新增的 `runtime-asset-inventory` 或 `asset-build-manifest` 都必须进入该注册表的导入闭包;未注册 owner、重复资产 `id` 与重复正式输出都会被拒绝。当前 `static/assets` 中的每个文件都必须恰好属于一个正式 owner,并至少存在一个真实运行时消费者。 +导航任务 3 删除了无活动消费者的通用页面旧入口,并同步删除两张仅靠该入口人工补数的 notification frame。当前注册表闭合 `77` 个正式 `static/assets` 输出;family/profile/records 的同名 frame 仍有真实专项 mixin 消费,`ModulePageBackground` 仍有活动页面消费者,均继续保留。`tests/retired-module-page-contract.ps1` 是这次退役边界的防回归所有者。 + 认证直接资产由 `design-pipeline/manifests/auth-runtime-assets.json` 管理,其余无法重建但仍被产品消费的直接二进制由 `design-pipeline/manifests/application-runtime-assets.json` 管理。四张共享卷轴、六张长页面背景和 G01 空态边框的生成事实依次只属于 `design-pipeline/manifests/shared-scroll-skins-v3.json`、`design-pipeline/manifests/page-backgrounds-v3.json` 与 `design-pipeline/manifests/g01-state-frame-v3.json`。 页面、组件、样式、数据映射和工具代码本身是消费者关系的唯一事实源。生成清单不得保存槽位、Vue 组件、选择器、`uni-app mode`、消费者列表或其他运行时渲染语义;这些规则只能由实际消费者源码及对应合同拥有。 @@ -62,6 +64,7 @@ npm.cmd run verify:shared-scroll-skins ```powershell powershell -ExecutionPolicy Bypass -File tests/runtime-assets-contract.ps1 powershell -ExecutionPolicy Bypass -File tests/retired-asset-removal-contract.ps1 +powershell -ExecutionPolicy Bypass -File tests/retired-module-page-contract.ps1 powershell -ExecutionPolicy Bypass -File tests/a01-retired-pipeline-removal-contract.ps1 powershell -ExecutionPolicy Bypass -File tests/a01-no-photoshop-pipeline-contract.ps1 powershell -ExecutionPolicy Bypass -File tests/mumu-visual-acceptance-boundary-contract.ps1 @@ -94,4 +97,4 @@ powershell -ExecutionPolicy Bypass -File tests/compile-audit.ps1 - `docs/design/assets/a01-vnext/A01-shared-scroll-skins-with-dialog-approved.png` - `docs/design/assets/a01-vnext/A01-shared-skin-family-option-2-selected.png` -G01 的 A/B/C 方向候选、无确定变换的 add-sheet/close 色键源、重复的旗舰 ImageGen 源、旧 `static/icons` 和未接入业务的 TAC 文件已经退役。正式 add-sheet 与 close 位图作为 `committed-binary` 由 `application-runtime-assets.json` 锁定;六张长背景及 G01 空态边框则保留可执行母版和 schema v3 构建链。不得重新引入候选入口、虚构可重建关系或在文档复制正式输出哈希。 +G01 的 A/B/C 方向候选、无确定变换的 add-sheet/close 色键源、重复的旗舰 ImageGen 源、旧 `static/icons` 和阶段 0 当时存在的未接入 TAC 旧批次已经退役。`static/tac/` 当前共 5 个文件:4 个后端提供的供应商文件保持原字节,项目只新增 `static/tac/js/jiapu-tac-adapter.js` 作为唯一协议适配层。A01/A04/A05 已通过 `components/TacVerification.vue` 形成真实运行时消费者;这组供应商资产不是视觉构建管线的可再生输出,不进入 `runtime-assets.json`,其存在性、精确文件集合、消费者和哈希由 `tests/auth-tac-integration-contract.ps1` 唯一拥有。受保护哈希分别为 `tac.css=181694518971a9f991d551b6a6e6dab2bf750f940bfc1673a158213f92eedbe0`、`tac.min.js=505f73c051908d7b805db458990790be3e91f792c4001cec0ea9377d7d302b55`、`icon.png=53e37ffc5bb81c46e6306b7d61d2eaa3de57e47ca6cdb8d5210022ae815c21c2`、`dun.jpeg=d9178a8c4cca36e3df6c3acd7e895ce9d34dd60ef3f1cf4a70c94d4324ed96e7`;不得修改、格式化、覆盖或以重新下载文件替换。正式 add-sheet 与 close 位图作为 `committed-binary` 由 `application-runtime-assets.json` 锁定;六张长背景及 G01 空态边框则保留可执行母版和 schema v3 构建链。不得重新引入候选入口、虚构可重建关系或复制第二套 TAC 资产所有权。 diff --git a/docs/项目当前总览.md b/docs/项目当前总览.md index 908b81b..8ca03e1 100644 --- a/docs/项目当前总览.md +++ b/docs/项目当前总览.md @@ -1,13 +1,52 @@ # 项目当前总览 -> 当前阶段:阶段 1——导航栈与 T01 大规模世系树书面设计已收口 -> 当前状态:三位审查者已完成导航与 T01 的独立审查、交叉补漏和终审;业务代码尚未实施,下一步从导航失败测试开始 -> 当前提交:`eced3d1 完成全项目响应式审核与换机交接` -> 更新日期:2026-07-22 +> 当前阶段:导航任务 1—10、TAC 认证客户端、领域上下文基础与 M07 反馈客户端已经完成;T01、认证、家谱工作区、G03 原子创建、M06 帮助、个人资料读写、通知读写、M10 服务端退出、M04 登录态改密和 M05 手机号换绑后端接口门禁均为红灯,继续逐域关闭真实接口 +> 当前状态:A01/A04/A05 已接入统一 TAC、真实短信和认证请求代码,M07 已接真实反馈提交 owner;G01/G05、G03、M06、M01/M02/M03 个人资料读写、N01/N02/M01/G01 通知域、M10 退出域、M04 密码凭证域与 M05 换绑域已完成三人接口审查和失败门禁,均未猜测接线;`runtimeConfig.mode` 仍为 `mock`,MuMu 原生矩阵和真实环境联调待执行 +> 当前基准 HEAD:`9b0ad62df467f4e2c58b7689087683c77755e07b`(`main`,工作区有本轮未提交变更) +> 更新日期:2026-07-23 + +## 2026-07-23 换机续作断点 + +### 总目标与完成定义 + +本项目的总目标不可缩减为“只写规划”或“只让静态测试通过”:必须在 `main` 工作区内持续推进整个 UniApp 家谱项目,按测试先行和三人交叉评审完成导航、T01 长世代世系树、TAC/认证、家谱领域数据、G/T/F/R/N/M 全量页面与接口、异常恢复、无障碍、构建及上线验证,最终交付能够正常上线使用的项目。若本地可完成的工作全部完成后只剩后端合同重导、有效测试账号、应用签名、发布凭证或 MuMu 人工操作等外部状态,才允许停在明确硬阻塞;每项阻塞必须有失败门禁、复现证据、唯一所有者、解除条件和继续步骤,不能用 mock、fixture、定时器或宽松兼容伪装完成。 + +协作固定为当前主代理加两位评审者,共三人。三人都必须独立检查接口/字段、页面/业务闭环、交互/异常/视觉,再交叉质询并统一结论;主代理是唯一写入者,不再创建旧专家身份或增加并发评审者。普通技术取舍由三人自行收敛,不反复交给用户。系统保存的目标记录仍存在,但在 2026-07-23 读取时状态为 `paused`;换机后的执行者不得据此把任务视为取消,应以上述总目标和本节断点继续。只有项目真实达到完成定义时才能标记完成。 + +### 仓库与保护基线 + +- 当前分支为 `main`,HEAD 为 `9b0ad62df467f4e2c58b7689087683c77755e07b`,上游显示 `main...origin/main`。工作区含大量本轮未提交的修改、删除和新增文件,均属于当前连续治理成果;换机后先运行 `git status -sb` 和 `git rev-parse HEAD` 核对,禁止用 `restore/checkout/reset` 清理,也禁止自行改分支或 worktree。 +- 本节仅记录断点,没有执行 `git add/commit/push`。由用户自行上传;换机后必须保留当前完整工作区,而不能只依赖旧远端基线。 +- 后端唯一目标地址是 `https://backend-api.ddxcjp.cn/`。受保护 `APP.openapi.yaml` 的 SHA-256 为 `8964CD583CE172425B63BBFD802F7EB587EB3641EADFD6F9D3B264FAA8090C6C`,`APP.openapi.json` 为 `87DB1DC148C5E6E877815AFF7B3A7FC7C7ECA95A2CEC50A88B42F3908961F31A`;两文件相对 HEAD 无差异。不得修改、格式化、覆盖或删除,只接受后端同一版本重新导出的双文件。 +- 不得启动、关闭或调整 MuMu。浏览器截图和源码检查不能冒充 Android 原生视觉、TalkBack、系统字号、软键盘或返回键验收。 +- 当前物理测试库存仍是 PowerShell `140`、Node `47`。最近一次完整记录为 PowerShell `126/140`,14 项均是预期发布红灯;Node 语法 `47/47`、纯 Node `19/19`、活动 Vue 脚本 `64/64` 通过。G03 最后一次布尔类型加固后又单独通过聚焦合同;换机后应先重跑完整套件,不能把这些历史数字冒充新机器证据。 + +### 已完成到哪里 + +导航任务 1—10、统一 TAC 客户端、领域上下文基础和 M07 真实反馈客户端已经完成本地可完成部分。T01、认证/TAC、家谱工作区、M06、个人资料读写、通知读写、退出、密码、手机号换绑均已经三人审查并建立明确 OpenAPI 红灯。任务 35 已完成 G03 原子创建合同:否决空谱加通用人物的两写方案,固定一次 atomic bootstrap、无 PII operation-status、统一 accessPreset、可信地区、词法 ID、始祖不变量及客户端激活门禁;`openapi-yaml-json-parity-runtime-smoke.js` 已升级为严格 YAML mapping 和任意精度数字的完整双源深比较。尚未批量把这些红灯域接到宽松线上接口,`runtimeConfig.mode` 仍诚实保持 `mock`。 + +### 当前精确断点:任务 36 普通加入申请闭环 + +三人已经完成 G06/G08/G09/G10 与 2026-07-22 线上 OpenAPI 的只读核对,范围只包括“鉴权搜索公开可申请家谱 → 普通申请 → 我的申请/撤回 → 待审列表/单条审核”。邀请码校验和直接加入仍是后续独立任务,产品结论保持“邀请码成功后直接加入且不生成审核记录”,不得混入普通审核合同。 + +已统一的最小正确方案如下: + +- 搜索、我的申请和待审列表使用各自专用最小投影与稳定 cursor 分页,不返回 `total`,所有 `genealogyId/applyId` 都是有界词法字符串。搜索项只暴露识别家谱所需字段和当前查看者状态;我的申请完整表达 `PENDING/APPROVED/REJECTED/WITHDRAWN`;待审项只含 `applyId/applicantName/relationDesc/applyReason?/submittedAt`,不得泄漏手机号、用户 ID、邀请人或审核人内部字段。 +- 申请 body 闭合为 `applicantName/relationDesc/applyReason?`,前两项必填;删除 `phone/inviterUserId`。同账号、租户、家谱最多一个活动 `PENDING`,由数据库唯一约束而非先查后插保证。 +- 申请 POST 必须带无 PII 的 `Idempotency-Key`。同 key、同 canonical body 重放同一结果;同 key、不同 body 返回 409。客户端只持久 `{sessionEpoch,requestKey,startedAt}`,不持久姓名、关系和理由。 +- 冷启动恢复不复用 `mine` 查询,也不持久表单 PII;新增唯一只读 owner `GET /genealogy/app/genealogies/join-apply-requests/{requestKey}`,以显式 discriminator 返回 `PENDING {resolveBy,retryAfterSeconds}`、`SUCCEEDED {applyId/genealogyId/...无 PII 回执}` 或 `FAILED_NO_COMMIT`。只允许 `ABSENT→PENDING→SUCCEEDED/FAILED_NO_COMMIT`,两个终态不可变;GET 纯读,跨账号/租户/client 统一非泄漏 404,PENDING 必须有收敛期限,从未到达的 key 也必须在可计算时间边界后成为零写 `FAILED_NO_COMMIT`。 +- 不引入 `applicationVersion`、`If-Match` 或审核详情端点。申请在 PENDING 时不可编辑,撤回、通过和拒绝都以 `WHERE status=PENDING` 的数据库 CAS 决定唯一赢家。重复相同撤回返回同一 200;重复相同审核决定返回原 200,拒绝时只有规范化后理由相同才算相同动作;相反决定、不同拒绝理由或撤回/审核竞态败方返回 typed 409 和当前最小状态。终态不可改,重新申请创建新 `applyId`。 +- 审核 body 只允许 `APPROVE`,或 `REJECT+必填申请人可见 rejectionReason`。批准必须在同一事务完成唯一成员关系和申请终态;G05 的 `canReviewJoinApplications` 只是入口 capability,服务端在事务/CAS 时仍重新验证权限、家谱状态和 `PUBLIC_APPLY`。 +- 所有操作 required SaToken 和非空 `clientid`,仅使用 `application/json`、`Cache-Control: private, no-store`,429 带 `Retry-After`。线上三个匿名 GET 当前实测为 HTTP 200 加业务 `code=401`,却与文档 HTTP 401 string 冲突;发布合同必须统一为真实 HTTP 状态与 typed JSON,禁止 `*/*`、200 包装认证错误、`default` 响应和 int64 JSON 身份。 +- 客户端结果未知时禁止乐观改列表:申请查专用 operation;撤回刷新 mine 并可安全重放同一 DELETE;审核刷新 pending,行消失只能说“状态已变化/已被处理”,不能冒充本次审核成功。首版不承诺消息中心通知;G09 的 `onShow` 与手动刷新是当前业务真相。 + +**尚未实施的边界必须原样保留:** `tests/join-application-openapi-contract.ps1` 还没有创建,任务 36 尚未写入实施计划和接口映射,测试计数仍为 140/47;G06/G08/G09/G10 业务代码也没有在本任务中修改。换机后的第一项写操作应是用 `apply_patch` 新增该 OpenAPI 失败门禁,先运行并取得精确 `JOIN-APPLICATION-OPENAPI-CONTRACT BLOCKED`,再把 `API-JOIN-001`—`006`、七个 operation、状态机、隐私投影、竞态和部署反例同步写入现有四份中文权威文档及文档合同。后端门禁通过前页面继续保持诚实本地预览,不写只适配当前宽松线上模型的临时代码。 + +完成任务 36 的本地门禁、全量回归和三人终审后,继续按独立批次推进邀请码直入、G11 设置写入、G12 字辈真实保存,以及其余 G/T/F/R/N/M 业务域;不得一次混合导航、验证码、领域持久化和无障碍多个阶段。最终仍须完成真实后端联调、构建、签名/隐私配置、MuMu 全流程矩阵和发布终审。 ## 当前目标 -阶段 0 的清理和验证基线已经完成。当前主代理与两位固定评审者已把下一轮工作收敛为两个严格串行阶段:先统一导航栈语义,再在后端新图合同通过后重建 T01 大规模世系树。当前只完成中文设计和实施计划,没有接入新接口、迁移导航业务代码或改动 T01 运行时。 +阶段 0 的清理和验证基线已经完成。当前主代理与两位固定评审者已完成导航任务 1—10,建立路由注册表、导航网关和零债务门禁,并迁移共享组件与认证、G、T、F、R、N/M 全部活动页面。T01、认证/TAC、G01/G05 家谱工作区、G03 原子创建、M06 帮助、M01/M02/M03 个人资料读取、M02 个人资料写入、N01/N02/M01/G01 通知读写、M10 当前设备退出、M04/全认证密码凭证及 M05 手机号换绑的规范 OpenAPI 合同都已测试先行落地;TAC 客户端批次已经完成,A01 短信登录、A04 注册和 A05 忘记密码当前共用严格验证组件、服务端 `validToken`、4 位短信码与可取消请求,但生产 OTP 目标已收紧为统一 6 位,须等待后端同版原子迁移后再替换客户端,禁止 4/6 双接受。领域基础已把会话与当前家谱 ID 收紧为词法字符串:账号切换和损坏存储清理上下文;给定新列表发现历史 ID 消失或显式目标无权时写入持久失效标记,跨重载也禁止静默切谱;T01 也会在写入路由家谱前拒绝已有失效标记和无访问权限的夹具。M07 已按 `POST /genealogy/app/feedback` 接入严格真实提交,mock 模式固定返回 `WRITE_UNAVAILABLE`,不再伪造成功。后端认证门禁未关闭前保持 `mock`;其余尚未接真实写接口的页面仍是明确本地预览或硬关闭。 ## 当前权威资料 @@ -18,7 +57,7 @@ - 接口导出:`APP.openapi.json`、`APP.openapi.yaml` - schema v3 视觉资产注册表:`design-pipeline/manifests/runtime-assets.json` - 九宫格所有者:`styles/adaptive-frame-profiles.scss` -- 密码策略所有者:`utils/validation.js` +- 当前预览密码策略所有者:`utils/validation.js`;生产目标 wire/policy owner 等待 `API-PASSWORD-001`—`005` 关闭后原子替换,禁止单页双轨。 - 响应式覆盖:`tests/responsive-layout-coverage.json` - 固定尺寸例外:`tests/responsive-layout-allowlist.json` - 活动路由:`pages.json`;项目固定使用 Vue 3,`uni.scss` 是 Sass 设计令牌唯一入口。 @@ -27,18 +66,41 @@ ## 接口文档所有权 -Apifox 是接口合同的唯一源头。`APP.openapi.json` 用于自动扫描和测试,`APP.openapi.yaml` 用于人工阅读与跨工具导入。两份文件都由用户从 Apifox 导出;后续目标合同要求它们来自同一版本并保持语义一致,不得分别手工维护。阶段 0 只保护原文件,尚未把“当前两份导出语义完全一致”当作已验证结论。 +后端维护的 Apifox 项目是接口合同的唯一编辑源。`APP.openapi.json` 用于离线自动扫描和测试,`APP.openapi.yaml` 用于人工阅读与跨工具导入;两份文件都只接受来自同一后端版本的重新导出,不得分别手工维护。部署地址的 `/v3/api-docs` 只作为当前线上实现证据,发现差异时必须推动同版本双导出更新,不能反向覆盖受保护文件。 -当前 JSON 文档为 OpenAPI `3.0.1`,包含 `112` 条路径、`153` 个操作和 `72` 个模型。完整的 JSON/YAML 语义一致性合同将在阶段 1 建立。 +当前 JSON 文档为 OpenAPI `3.0.1`,包含 `112` 条路径、`153` 个操作和 `72` 个模型。`tests/openapi-yaml-json-parity-runtime-smoke.js` 已使用无第三方依赖、无损任意精度数字且严格校验 mapping 分隔符的结构化解析器深比较完整 JSON/YAML;相邻不安全大整数差一反例已先红后绿,当前两份受保护快照语义一致。后端今后必须同版本双导出,任何单边漂移都会在领域合同前失败。 -阶段 0 结束时,JSON 的 SHA-256 为 `2b5b9a0ffdcd901c361fb7500bfd354d5fd6d038639fb6350c7d1ef7c0d43ab9`,YAML 的 SHA-256 为 `ce6553577d441fee8c5023a87c30f9b411469cc1e7ce2d3c237314df0272ad77`。Git 只读状态仍为 YAML 已修改、JSON 未跟踪,与接管时一致;阶段 0 没有改写两份导出。 +后端在 2026-07-22 新提供 `https://backend-api.ddxcjp.cn/`。同日 21:52(Asia/Shanghai)只读获取其 `/v3/api-docs`,线上为 OpenAPI `3.1.0`、`722` 条路径、`858` 个操作和 `507` 个模型;本地 112 条路径中有 109 条仍在线,`/genealogy/app/files/reference`、`/genealogy/app/files/upload`、`/genealogy/pc/files/upload` 三条不在当前线上文档,线上另有 613 条路径。线上模型已把部分 `Genealogy*Body` 重命名为 `AppGenealogy*Body`,`GenerationPoemBatchBody` 也新增了由路径写入的 `genealogyId` 字段,因此本地双导出是明确的旧快照,不能再代表当前部署的完整合同。`utils/config.js` 的唯一 `baseUrl` 已更新为无尾斜杠的 HTTPS 地址;`mode` 仍保持 `mock`,在页面接口和安全合同逐批闭合前不得提前切换远端。 -## 必须保护的用户改动 +线上文档已经提供 `/captcha/require`、`/captcha/challenge`、`/captcha/verify`:挑战与校验绑定 `tenantId/clientId/sceneCode/subject`,校验成功响应可返回 `validToken`,而发送短信的 `AppSmsCodeBody` 已把 `validToken` 列为必填。客户端已按 `APP_SMS_LOGIN/APP_REGISTER/APP_FORGOT_PASSWORD` 三个精确场景实现“查询要求→取得挑战→供应商完成→服务端校验→携票发送短信”,注册和找回提交不重复执行 TAC;认证请求只接受 HTTP 200 的严格 JSON envelope,统一 15 秒超时,离页或返回会中止当前 RequestTask。密码登录体尚无 `validToken`,因此 A01 以短信登录为默认且密码登录入口保持不可用,绝不以客户端先滑动冒充服务端强制校验。 -- `APP.openapi.yaml`:用户当前修改。 -- `APP.openapi.json`:用户新导出的未跟踪文件。 -- `docs/家谱项目全量治理设计.md`:已确认中文设计。 -- `docs/家谱项目全量治理实施计划.md`:已改写为导航栈与 T01 的当前中文实施计划。 +认证后端门禁仍有四组问题:`API-AUTH-TAC-001` 要求密码登录加入服务端可消费的同语义票据;`API-AUTH-TAC-002` 记录线上 `/captcha/challenge` 在 `APP_REGISTER` 实测返回 HTTP 500 且空响应;`API-AUTH-TAC-003` 要求验证请求以 provider discriminator/`oneOf` 严格关闭根对象和各 payload 的额外字段,并补齐必填 `providerCode/captchaType/payload`;`API-AUTH-TAC-004` 要求同一验证中心返回服务端绑定的 verification session、可验证方法和同一类短时单次 `validToken`,`required=false` 也必须直接签发可供短信接口消费的票据,不得形成无障碍绕过。`tests/auth-tac-openapi-contract.ps1` 当前输出 `AUTH-TAC-OPENAPI-CONTRACT BLOCKED`。密码登录、短信登录和注册的线上成功响应已统一为 `RAppLoginVo → AppLoginVo.access_token`,客户端旧令牌字段读取已删除。线上文档自身仍发布 `http://backend-api.ddxcjp.cn` server URL,与已验证可用的 HTTPS 地址不一致;客户端只能使用显式 HTTPS,后端还需修正文档 server 声明。对 `http://localhost:5173` 的预检已返回允许 `content-type/clientid`,这不能替代正式 H5 域名的 CORS 验证。 + +家谱工作区只读审查已固定 `/genealogy/app/genealogies/mine` 为可访问集合 owner、`/genealogy/app/genealogies/{genealogyId}/overview` 为 G05 唯一详情 owner;首批不同时请求语义重复的 `/{genealogyId}`。线上 `AppGenealogyVo.genealogyId` 仍是 JSON `integer/int64`,最大合法值进入 JavaScript 后会不可逆失真;三个相关模型均无 `required`,也没有必填 `canView`,`roleType` 无枚举。无令牌实测三条读取均返回 HTTP 200、`application/json;charset=UTF-8` 与 `{code:401,msg,data:null}`,但文档只列 200/401 且 401 为字符串,尚不能稳定区分登录失效、对象无权、已删除和服务故障。问题单 `API-GENEALOGY-WORKSPACE-001`—`003` 与 `tests/genealogy-workspace-openapi-contract.ps1` 已建立;同版本双导出通过前不写 G01/G05 专属 adapter,不用本地字段猜测替代服务端 owner。 + +M06 三人审查选择 `GET /genealogy/app/help-articles` 的完整列表作为唯一远端 owner;线上列表模型已经包含 `helpContent`,因此页面不调用详情端点、不消费 `helpId`,也不让 JSON `int64` 进入页面模型。每次响应只允许显式投影 `helpCategory/helpTitle/helpContent`,分类从当前列表动态派生,展开键只在当前响应生命周期内使用并在搜索、分类、刷新前清空。当前受保护双导出仍返回通用 `ListResult/RList`,线上 `RListHelpArticleVo/HelpArticleVo` 又没有 `required`,正文格式、仅发布内容、展示顺序和认证失败承载也未形成一致合同;匿名实测列表与详情均为 HTTP 200+业务 `code=401`,而线上文档声明 HTTP 401 string。问题单 `API-M06-001`—`003` 与 `tests/help-center-openapi-contract.ps1` 已建立;门禁通过前保留明确本地 FAQ,不写 live-only adapter。 + +个人资料读取三人审查固定 `GET /genealogy/app/auth/profile` 为 M01/M02/M03 共用的唯一接口 owner。首批只要求 canonical 11 位 `phone` 必填;`nickName/realName/email` 未设置时唯一省略,出现时必须是非空规范字符串,姓名最多 30、邮箱格式有效且最多 100。adapter 立即把明文手机号变为掩码和读屏标签,只输出固定页面模型;`userId/avatar/status` 等字段全部丢弃,因此它们的 int64/枚举不阻塞本批。受保护双导出仍是通用 `ObjectResult/RObject`,线上 `RAppProfileVo/AppProfileVo` 则无 required 和字段边界;匿名实测仍是 HTTP 200+业务 401,与文档 401 string 冲突。问题单 `API-PROFILE-READ-001`—`003` 与 `tests/profile-openapi-contract.ps1` 已建立;门禁通过前不把 live-only 字段接进页面。 + +通知三人审查把读取与写入拆成两个原子批次。读取唯一使用 `GET /genealogy/app/notifications` 和 `GET /genealogy/app/notifications/unread-count`:列表必须完整返回当前账号最多 200 条活动通知并按最新优先,未读数精确统计同一集合;页面只消费完整纯文本标题/正文、带时区时间和 `READ/UNREAD`,所有服务端 ID、发送者与业务目标都不进入公开页面模型。N02 没有详情接口,首批只能以当前内存 `generation+ordinal` 的 `snapshotKey` 打开完整快照,重启、账号切换或成功刷新后的旧 key 均提示返回消息中心重新打开。已读写入另由两个 POST 持有,要求无损字符串 `notificationId`、当前账号幂等、read-all 截止点和并发新消息语义;通过前删除伪本地已读而不发请求。当前受保护双导出的列表仍是通用模型且没有未读数路径,线上模型无 required/枚举/容量,所有 ID 仍为 int64,匿名实测又是 HTTP 200+业务 401。问题单 `API-NOTIFICATION-READ-001`—`003`、`API-NOTIFICATION-STATE-001`—`003` 以及两项失败合同已经建立,均不以猜测代码绕过。 + +M02 资料写入三人审查保留现有 `PUT /genealogy/app/auth/profile` 作为唯一 App owner,但要求后端把它正式定义为原子 dirty-only merge,而非全资源替换或依赖 DTO 惯例猜测。请求只允许脏的 `nickName/realName/email`:省略保持,昵称出现时必须非空,真实姓名/邮箱的精确空串表示清空,null、纯空白和边界空白非法;成功返回完整 canonical `RAppProfileVo`,清空后的可选字段仍省略。并发沿用 T01 的单一版本模型:`AppProfileVo.profileVersion` 是 opaque string,PUT 必带同形状 `If-Match`,旧版本返回 409 `PROFILE_VERSION_CHANGED`,H5 CORS 同步允许该 header。受保护双导出还是旧 `ProfileUpdateBody → RObject`,线上则是无 required/merge/version/security 的 `AppProfileUpdateBody → RAppProfileVo`;当前页面还把同一 fixture 名称同时填入昵称和真实姓名,并用 500ms 定时器伪造本地校验。问题单 `API-PROFILE-UPDATE-001`—`004` 与 `tests/profile-update-openapi-contract.ps1` 已建立;GET 门禁和本门禁通过前不接写入。 + +M10 退出三人审查固定 `DELETE /genealogy/app/auth/logout` 只撤销请求中 bearer 所属的当前设备凭证族;同账号其他设备保持登录,“全部设备退出”必须另立接口。活动、已撤销和已过期但仍可验证为本 client 签发的历史凭证重复 DELETE 都返回同一个 200 `RVoid`,且成功后旧 token 对任何受保护接口均不可用;伪造、格式非法或 client 不匹配才返回 typed 401 拒绝。客户端唯一 `logoutCoordinator` 在同一同步临界段捕获 A 的 token/clientid、清本地 token/家谱上下文并 bump epoch、用显式快照创建不绑定 M10 生命周期的请求,然后立即 `goRoot(A01)`;异步回调永不再次 clear,避免误删随后登录的 B。线上 endpoint 虽存在,却无 security/clientid、required RVoid、范围/幂等/复用反例和 JSON/no-store;问题单 `API-LOGOUT-001`—`003` 与 `tests/logout-openapi-contract.ps1` 已建立。任何远端失败都不恢复本地 token,只区分“已从本机退出;服务器撤销已确认/未确认/未能撤销”。 + +M04 改密三人审查否决当前登录/注册/找回/改密共用的静态 32 个十六进制字符 MD5 wire:它是可直接重放的密码等价物,也使服务端无法执行真实新密码策略。唯一生产目标是四条入口原子迁移到 HTTPS 中的 raw `writeOnly` 密码;当前密码与登录兼容 1—64 Unicode code point,新密码统一 15—64 code point、NFC、允许空格与 Unicode、无组成规则,并由服务端执行常见/泄露密码 blocklist、账号限速和带独立盐的自适应慢哈希。M04 当前密码就是重新认证,TAC 不替代身份;用户明确要求的 A01/A04/A05 TAC 仍由原认证门禁持有。严格 200 前服务端必须原子落密、提升 `credentialEpoch` 并撤销包括调用者在内的所有 access/refresh session,客户端清本机回 A01;网络、超时、畸形响应或 5xx 也是结果未知,同样清本机且不自动重试。问题单 `API-PASSWORD-001`—`005` 与 `tests/password-change-openapi-contract.ps1` 已建立;门禁通过前 M04 保持诚实本地预览。 + +M05 换绑三人审查否决“活动 bearer+新号验证码”直接改号:这只能证明控制新号码,不能证明当前账号本人。唯一生产流程是活动 session、最终 PUT 内 raw `currentPassword` 重新认证、`APP_PHONE_CHANGE` TAC 和新号严格 6 位 OTP;不强制旧号 OTP,避免用户丢失旧号时永久锁死,但成功事务必须持久写入旧号安全通知 outbox。标准 OpenAPI 无法按公共发码 body 的 `sceneCode` 条件化鉴权,因此采用独立且强制 SaToken 的 `POST /genealogy/app/auth/phone/sms/code`,scene 由路径固定;公共 `/auth/sms/code` 删除 `APP_PHONE_CHANGE`,两个 operation 仍复用同一 OTP 生成、限速和存储 owner。最终 `PUT /auth/phone` body 只含 `currentPassword/phone/smsCode`,在同一事务内消费 OTP、执行号码唯一约束、更新号码、提升 `credentialEpoch`、撤销包括当前在内的全部 access/refresh session并写通知 outbox,严格 200 返回 `RVoid`。无密码账号返回 `STEP_UP_UNAVAILABLE`,不能降级;最终 PUT 的超时、5xx、畸形响应或进程终止均按结果未知清本机回 A01且不自动重试。问题单 `API-PHONE-001`—`005` 与 `tests/phone-change-openapi-contract.ps1` 已建立;M04 raw-password 门禁、认证/TAC 门禁和本门禁通过前,M05 保持诚实本地预览。 + +G03 创建链路三人反向质询后否决“先建空谱、再写始祖”的 `ROOT_REQUIRED` 两写方案:当前产品没有跨会话保存空谱的需求,而两写会凭空增加半成品配额、可见性、恢复、取消、过期和第二次未知结果。唯一生产目标是第一步只在本页收集资料,最终按钮以 `AppGenealogyBootstrapBody` 一次原子创建家谱、OWNER 成员关系、带数据库权威标记的唯一一世始祖、READY 状态和幂等回执;通用人物 collection、人物 DELETE 与 parents mutation 必须阻断第二根、删除根和重挂父母,人物 PUT 对根的可编辑白名单精确只有 `name/sex/birthDate/biography`,其他字段全部 typed 422。首次 POST 先以短控制事务认领 PENDING/digest/fencing,再由业务事务完成全部写入,任一步失败全回滚并 CAS 终态;结构化转换只允许 `ABSENT→PENDING→SUCCEEDED/FAILED_NO_COMMIT` 且终态不可变,FAILED 机器保证零领域写与零 quota,PENDING 的 `resolveBy<=claimedAt+2 分钟`。`GenealogyBootstrapOperationKey` 由 13 位服务端判定的 issuedAt 毫秒与至少 128 位 CSPRNG 组成,`acceptUntil=issuedAt+10 分钟`、最大未来偏差 5 分钟,并由 600/300 秒扩展锁定;operation-status GET 无 body、纯读且只返回显式 discriminator 的三个状态,截止前 404 保持同 key,截止后无记录按 key 计算 FAILED 而不写墓碑。请求使用受鉴权 `/genealogy/app/region/search`、词法 `GenealogyRegionCode`、服务端 `selectable` 和统一 `GenealogyAccessPreset=MEMBER_ONLY/PUBLIC_APPLY`,同版删除公共地区旧路由,并递归清除组合 schema 中的 `visibility/joinMode` 及旧 create/update DTO;G11 在本批只取得共享字段形状,其 If-Match、版本/CAS、权限刷新与结果未知仍是独立门禁。客户端仅持久 `{sessionEpoch,operationKey,startedAt}`;`IDEMPOTENCY_KEY_REUSED` 必须进入 fatal/quarantined,禁止查询或安装 status、自动换 key,只有用户明确放弃才能清理;其他分支按可证明未提交与 unknown 分离。成功固定按 receipt→`/mine` cache→context→G05 收口,context/导航失败只重试本地闭环。问题单 `API-G03-001`—`005`、完整双源深比较、后端 `tests/g03-bootstrap-openapi-contract.ps1` 和实际执行状态机套件的客户端 `tests/g03-bootstrap-client-release-gate.ps1` 已建立;G03 两门禁转绿仍不能绕过家谱工作区读取门禁、聚焦/全量回归与 MuMu 原生验收,全部通过前保留诚实本地预览,不接 `appApi.createGenealogy`。 + +当前 JSON 的 SHA-256 为 `87db1dc148c5e6e877815aff7b3a7fc7c7eca95a2cec50a88b42f3908961f31a`,YAML 的 SHA-256 为 `8964cd583ce172425b63bbfd802f7eb587eb3641eadfd6f9d3b264faa8090c6c`。两份文件均已被 Git 跟踪,当前相对 HEAD 无差异;本轮没有改写、格式化或覆盖接口导出。 + +## 必须保护的用户文件 + +- `APP.openapi.yaml`、`APP.openapi.json`:后端接口离线源快照,只接受同一后端版本的双导出替换,不得手工修改。 +- `static/tac/`:当前共 5 个文件,其中 4 个是后端提供且不可改写的供应商文件,第 5 个是项目适配器 `static/tac/js/jiapu-tac-adapter.js`;A01/A04/A05 已形成真实运行时消费者,文件集合与供应商哈希由 `tests/auth-tac-integration-contract.ps1` 唯一保护。 +- 当前五份中文治理文档:长期唯一入口,随已验证进度同步更新,不另建平行计划。 任何清理批次都不得覆盖、恢复或删除以上文件。 @@ -59,41 +121,111 @@ Apifox 是接口合同的唯一源头。`APP.openapi.json` 用于自动扫描和 - 旧 A01 固定画布、PSD、Photoshop、v2 按钮和旧卷轴入口已经三人一致退役;没有通过重建旧预览制造假绿。 - schema v3 注册表已经完整覆盖当前 `static/assets`:认证直接资产、其余应用直接资产、共享卷轴、六张长页面背景和 G01 空态边框各有唯一 owner;旧 G01 候选与模块背景清单及注册例外已经删除。 - `design-pipeline/manifests/shared-scroll-skins-v3.json` 只拥有母版、处理、物理输出和质量规则,不再拥有页面槽位或 `uni-app mode`。 -- `tests/runtime-assets-contract.ps1` 精确闭合 `79` 个正式输出、物理文件和源码消费者;`tests/retired-asset-removal-contract.ps1` 防止旧候选、旧图标、TAC 残件与旧构建入口回归。 +- 阶段 0 验收时,`tests/runtime-assets-contract.ps1` 精确闭合 `79` 个正式输出、物理文件和源码消费者;导航任务 3 又删除了退役通用页面唯一虚构消费的两张 notification frame,当前正式输出为 `77` 个,并由 `tests/retired-module-page-contract.ps1` 防止旧入口与孤立资产回归。 - 设计管线当前为 Node `25/25 PASS`、Python `19/19 PASS`;四张共享卷轴、六张长背景和 G01 空态边框均可真实重建并通过质量审计。 - 质量报告只含工作区相对路径;确定性测试证明相同输入连续两次得到相同字节哈希与报告。 - 六张长背景与 G01 空态边框迁移后的可见像素哈希逐张等于迁移前正式图;连续两次构建的文件 SHA-256 也完全一致。旧 Sharp 单用途依赖和专用构建器已删除。 - 已删除 `28` 个无运行时消费者的 `static/assets`、`3` 个旧 `static/icons`、`4` 个未接入业务的 TAC 文件和 `8` 个候选或伪母版;两张用户批准的 A01 选型图作为 `reference-only / non-runtime` 视觉锚点保留。 - 阶段 0 验收时的 MuMu 证据:`emulator-5554` 当时在线,ADB 设备字段为 `model:SDY_AN00`,系统型号为 `SDY-AN00`,物理尺寸 `720×1280`,density `320 dpi`。本轮原生截图逐张打开复核了 A01、G01、T01 和 F01,未见缺图、透明错误、异常裁切或样式回退;复核结束后已回到 A01 页面,未启动、关闭或调整模拟器。 -- `docs/接口与页面映射总表.md` 已按 `pages.json` 建立 `52/52` 活动页面映射;除 A04 与 T01 专项结论外,其余具体接口明确标记为“待对应业务阶段 OpenAPI 审查”。 -- 第一轮六项产品优化已经完成:G01/G03/G06 统一读取 `onLoad(query)`;AppDialog 统一安全区与高度预算;AppTabbar/GenealogyCard 补齐可访问点击语义;G10 拒绝原因建立错误关联与失败聚焦;A04/A05/M04 统一消费唯一密码策略;T01 可在视图漂移后精确回到当前成员。 +- `docs/接口与页面映射总表.md` 已按 `pages.json` 建立 `52/52` 活动页面映射;A04、G 系列第一轮、T01 和新线上 OpenAPI 差异已有专项结论,其余具体接口仍标记为“待对应业务阶段 OpenAPI 审查”。 +- 第一轮六项产品优化已经完成:G01/G06 统一读取受控查询参数,G03 在本轮进一步删除了无业务参数的 `onLoad`;AppDialog 统一安全区与高度预算;AppTabbar/GenealogyCard 补齐可访问点击语义;G10 拒绝原因建立错误关联与失败聚焦;A04/A05/M04 统一消费唯一密码策略;T01 可在视图漂移后精确回到当前成员。 - 后续产品阶段按顺序为导航栈语义统一、T01 大规模世系树、短信验证码完整状态机、跨页面领域数据持久化、全局文字层级和无障碍第二轮;不得混合实施。 - 文档权威迁移已删除 `198` 个旧 Markdown、`35` 张旧 H5/静态设计图、`4` 个只保护旧资料的测试和 `.superpowers/sdd` 的 `12` 个过程文件;阶段 0 临时清单随后也已删除,当前 `docs/` 只保留 `5` 个长期中文 Markdown 入口。 - 浏览器截图链已把 G03、G06、G08—G12、T06、F02、N01 的有效状态断言迁回各自现有运行时合同;G01、T03—T08、T07 三个有价值的浏览器测试只剥离截图写盘,继续验证状态、滚动、溢出与交互。 - 已删除固定 Chrome 截图助手、联系表、8 个只保护截图助手的合同,以及迁移后重复的 N01/A02 合同;`tests/mumu-visual-acceptance-boundary-contract.ps1` 现在唯一约束“仓库不维护自动截图证据链、最终视觉通过只来自 MuMu”。 -当前全量 PowerShell 合同为 `119/119 PASS`;五个核心验证、密码策略 Node 冒烟、设计管线 Node `25/25`、Python `19/19`、四份资产清单验证和共享卷轴质量验证均通过。根 `tests/` 现有 `29` 个 Node 文件,其中唯一不依赖 H5/CDP 服务的密码策略冒烟已实际执行;其余运行时文件只在具备 `127.0.0.1:9222` 调试页时执行,不能用语法检查冒充运行通过。 +当前物理库存为 `140` 个 PowerShell 合同、`47` 个 Node 文件和 `4` 个 JSON 合同数据文件。最新 fresh 结果为 PowerShell `126/140` 通过;其余 14 项不是可删除的普通回归,而是保留真实外部或发布阻塞的 `tests/lineage-openapi-contract.ps1`、`tests/auth-tac-openapi-contract.ps1`、`tests/genealogy-workspace-openapi-contract.ps1`、`tests/g03-bootstrap-openapi-contract.ps1`、`tests/g03-bootstrap-client-release-gate.ps1`、`tests/help-center-openapi-contract.ps1`、`tests/profile-openapi-contract.ps1`、`tests/profile-update-openapi-contract.ps1`、`tests/notification-read-openapi-contract.ps1`、`tests/notification-read-state-openapi-contract.ps1`、`tests/logout-openapi-contract.ps1`、`tests/password-change-openapi-contract.ps1`、`tests/phone-change-openapi-contract.ps1` 和缺少 MuMu 人工证据时必须失败的 `tests/auth-android-accessibility-release-gate.ps1`。十四个输出分别为 `LINEAGE-OPENAPI-CONTRACT BLOCKED`、`AUTH-TAC-OPENAPI-CONTRACT BLOCKED`、`GENEALOGY-WORKSPACE-OPENAPI-CONTRACT BLOCKED`、`G03-BOOTSTRAP-OPENAPI-CONTRACT BLOCKED`、`G03-BOOTSTRAP-CLIENT-RELEASE BLOCKED`、`HELP-CENTER-OPENAPI-CONTRACT BLOCKED`、`PROFILE-OPENAPI-CONTRACT BLOCKED`、`PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED`、`NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED`、`NOTIFICATION-READ-STATE-OPENAPI-CONTRACT BLOCKED`、`LOGOUT-OPENAPI-CONTRACT BLOCKED`、`PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED`、`PHONE-CHANGE-OPENAPI-CONTRACT BLOCKED` 和 `ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED`。Node 语法 `47/47` 通过,纯 Node 冒烟 `19/19` 通过,活动 Vue 脚本模块语法 `64/64` 通过;导航源码扫描保持 `MIGRATION-DEBT=0`。依赖 `127.0.0.1:9222` 调试页的浏览器运行时文件本轮只做语法检查,没有执行;这不能冒充浏览器行为或 MuMu 原生视觉通过。 -清理结束后,`static/` 精确保留 `79` 个正式文件、共 `70,414,397` 字节;`tmp/`、`unpackage/` 和 `design-pipeline/generated/` 均不存在。三位审查者确认删除的三类临时输出分别为 `94`、`188` 和 `3` 个文件;正式生成资产仍由清单和构建器恢复,质量报告不作为长期资料保留。 +阶段 0 清理结束时,`static/` 的正式基线为 `79` 个文件、共 `70,414,397` 字节。任务 3 退役两张孤立通知 frame 后,正式 `static/assets` 为 `77` 个文件、共 `69,093,742` 字节;`static/tac/` 当前 5 个文件、共 `68,190` 字节,因而当前 `static/` 物理库存为 `82` 个文件、共 `69,161,932` 字节。4 个供应商文件保持原字节并由哈希合同保护,新增适配器是唯一项目映射层;TAC 已进入 A01/A04/A05 业务运行时,但不属于可重建视觉资产注册表。`tmp/`、`unpackage/` 和 `design-pipeline/generated/` 均不存在;正式生成资产仍由清单和构建器恢复,质量报告不作为长期资料保留。 ## 当前规划结论 ### 导航栈 -- 52 个活动页面中只读统计为 `navigateTo 59`、`navigateBack 11`、`redirectTo 14`、`reLaunch 7`,活动页面合计 91 次;活动组件另有 4 次,封存 A06 另有 2 次;没有 `switchTab`。 +- 迁移前 52 个活动页面只读统计为 `navigateTo 59`、`navigateBack 11`、`redirectTo 14`、`reLaunch 7`,活动页面合计 91 次;活动组件另有 4 次,封存 A06 另有 2 次。任务 3—9 已依次迁移共享组件、认证、G、T、F、R、N/M,任务 10 删除最后一个无消费者旧表单组件并关闭门禁。当前 pages/components 中 `navigateTo`、`navigateBack`、`redirectTo`、`reLaunch`、`getCurrentPages` 和业务页面路径字面量均为 `0`,`switchTab` 全项目为零;五种 Uni 导航调用和页面栈读取只允许由 `utils/navigation.js` 持有,业务路径只允许由 `utils/navigation-routes.js` 持有。 - MuMu 已复现 F01→F03 返回后残留两个 F01、A01→A04→登录后残留两个 A01、T03 同路由连续叠页,以及栈深为 1 时 F03 错回 G01。 -- 三人终选两个唯一所有者:`utils/navigation-routes.js` 持有 52 条路由、父页参数映射和结果操作枚举,`utils/navigation.js` 持有五种 Uni 导航 API、六个公开语义方法、一次性结果与统一返回优先级。 -- G01、F01、M01 保持自定义 Tab 根页;A01 是认证根页。T03 只保留一个原生页面实例,亲属浏览使用页内成员轨迹。 -- 当前尚未新建这两个模块,也没有迁移任何页面;实施从 `tests/navigation-routes-contract.ps1` 的预期红灯开始。 +- 三人终选并已实现两个唯一所有者:`utils/navigation-routes.js` 持有 52 条路由、父页参数映射和结果操作枚举,`utils/navigation.js` 持有五种 Uni 导航 API、五个公开导航语义方法、一次性结果与统一返回优先级;没有真实替换边,因此不公开替换方法。 +- G01、F01、M01 保持自定义 Tab 根页;A01 是认证根页。T03 只保留一个原生页面实例,初始路由 `personId` 是不可变宿主页身份,亲属浏览使用页内成员轨迹和可变活动成员;T05 本地预览离开使用 `goBack()`,不会把活动成员误写成宿主页路由身份。 +- 任务 1—10 已完成测试先行和静态实现;一次性结果绑定真实目标路由与业务参数上下文,只能由当前真实栈顶目标页消费。52 个活动路由、共享组件、四个根语义以及认证、G、T、F、R、N/M 系列均已迁移;通知目标只能由本地类型白名单映射,未知或越权目标失败关闭。未接真实写接口的页面只能形成明确的本地预览或硬关闭,不再展示伪保存、伪邀请码、伪订单或伪安全结论。任务 4—10 的 MuMu 原生流程复核仍因本轮禁止触碰模拟器而待执行,静态零债务不能替代该验收。 ### T01 大规模世系树 -- 当前默认 6 人数据已存在 103→106 断线;普通节点实际半高与全局常量相差约 4rpx;10×12 压力数据约 45/54 个父分组会断线,而旧测试只检查线段数量。 -- 当前布局最坏为 `O(G×N)`,单人一代时接近 `O(N²)`;页面、mock 和 OpenAPI 分别使用 `parentId`、无关系字段和递归 `children/spouses`,无法直接对接。 +- 此前默认 6 人数据中的 103→106 断线已通过唯一成员 owner 与字符串 `parentId` 修复;普通节点实际半高与全局常量仍相差约 4rpx,10×12 压力数据约 45/54 个父分组仍会断线,而旧测试只检查线段数量。 +- 当前布局最坏为 `O(G×N)`,单人一代时接近 `O(N²)`;页面与 mock 已统一当前阶段的 `id/parentId` 夹具,但 OpenAPI 仍使用递归 `children/spouses`,无法直接对接未来规范图窗口。 - 产品终选“焦点成员渐进窗口+独立全谱概览”,初始上二代/下二代;主世系为骨架,配偶并排,每段家庭关系有独立联合点,子女从对应联合点向下。 - 技术终选视口大小的单 Canvas 同画节点和边;纯 JavaScript 按“规范化→严格校验→可见投影→确定性布局→Scene/空间索引→相机”处理,DOM 只保留页头、工具条、两档抽屉、搜索、概览和无障碍线性列表。 -- 当前 OpenAPI 的 v1 `/lineage/tree` 仍是递归 `LineagePersonTreeView[]`,并缺少 overview、locator、稳定关系寻址和树版本并发合同。后端问题单 `API-T01-001` 已固定四条 `/genealogy/app/v2/...` 路径、FOCUS/BOUNDARY 查询、`schemaVersion/treeVersion`、EMPTY/POPULATED 空谱判别、窗口入口与全谱根分离、严格匿名节点、以 `relationshipKind` 判别的可寻址伴侣/父子关系、空 PATCH 错误码和 Scene 原子版本;在用户重新导出通过新合同前,T01 客户端不得开始实施。 -- 当前规划轮没有改写 OpenAPI 文件,没有启动、关闭或调整 MuMu,也没有修改页面与业务代码。 +- 本地旧导出和 2026-07-22 线上 OpenAPI 都只有 v1 `/lineage/tree`,仍是递归模型,并缺少 overview、locator、稳定关系寻址和树版本并发合同。最新线上文档的 `722` 条路径中没有任何 `/genealogy/app/v2/`,`507` 个模型中也没有新图辨识字段,四个稳定业务码同样缺失。后端问题单 `API-T01-001` 已固定四条 v2 路径、FOCUS/BOUNDARY 查询、`schemaVersion/treeVersion`、EMPTY/POPULATED 空谱判别、窗口入口与全谱根分离、严格匿名节点、以 `relationshipKind` 判别的可寻址伴侣/父子关系、空 PATCH 错误码和 Scene 原子版本;`tests/lineage-openapi-contract.ps1` 已建立严格红灯,在同版本线上文档与双导出通过前,T01 客户端不得开始任务 12。 +- 当前实施轮没有改写 OpenAPI 文件,也没有启动、关闭或调整 MuMu;已完成导航共享所有者、共享组件/根页头部语义、退役死入口、认证、G 系列、T 系列和 F 系列导航合同,以及树成员与家族内容夹具统一。认证请求代码已对准真实端点,但运行模式仍失败关闭;G/T/F 系列真实写接口与 T01 新图尚未接入。 + +### TAC 认证与无障碍安全边界 + +- `utils/auth-verification.js` 是当前认证场景、4 位短信码和服务端票据形状的唯一客户端 owner;生产目标由后端 `SmsCodeSecret` 和 `tests/phone-change-openapi-contract.ps1` 锁定为严格 6 位,门禁通过时必须一次替换所有活动消费者并删除旧 owner 的四位规则。`components/TacVerification.vue` 是验证浮层与 renderjs 生命周期 owner;`static/tac/js/jiapu-tac-adapter.js` 是 TianAi challenge/proof/verify 映射 owner;`utils/api.js` 是严格 HTTP 200 envelope、15 秒超时、离页中止、认证会话写入和反馈 wire payload 的唯一 owner。旧认证专用请求控制器入口、占位验证、伪验证码、宽松令牌兼容和 M07 假提交均已删除。 +- A01 默认短信登录;密码登录因 `API-AUTH-TAC-001` 保持可见但不可用。A04/A05 都只在发送短信前验证一次,并在手机号改变后使旧验证码上下文失效;成功、失败、取消、重复回调、空响应、非 JSON、超时、离页和返回键均有静态或纯运行时合同。短信发送与重设密码的 `RVoid` 没有声明 `data` 必填,客户端因此只要求合法 HTTP 200 与整数成功 `code`,并把“省略 data”或 `data:null` 都精确归一为 `null`;登录、注册和 challenge 等有实体响应仍强制 `data`。 +- 当前客户端浮层已补齐对话框命名、说明关联、初始聚焦、Tab 圈定、Escape/Android 返回、焦点恢复、原生刷新与关闭按钮、48px 目标及小视口内部滚动;这只是客户端壳层预检,不代表第三方滑块本身可由 TalkBack 或键盘完成。`tests/auth-android-accessibility-release-gate.ps1` 要求 MuMu 原生证据与非拖动等价验证方式,证据缺失时固定输出 `ANDROID-AUTH-ACCESSIBILITY-RELEASE BLOCKED`。 +- 三人交叉质询后的唯一方向是“同一验证中心、同一短时单次 `validToken`、供应商无关的服务端决策”,永久禁止 `accessibility=true`、跳过 TAC 或无票据发短信。中国大陆非交互风控供应商只进入限时 POC,必须在真实 UniApp Android WebView 中证明 TalkBack、外接键盘和 Switch Access 无焦点陷阱且误杀、防刷、弱网与故障指标达标,不能预先写成无障碍完成。已安全绑定设备只能作为加权信号;供应商不确定或不可用时进入支持文字/中继的可审计人工兜底或稍后重试,绝不 fail-open;音频验证码仅为后续独立 POC 候选,不能作为 P0 或唯一替代。 + +### 领域上下文与 M07 反馈 + +- `utils/genealogy-context.js` 只接受无边界空白的非空词法字符串 ID,并持有独立失效标记;`utils/session.js` 在损坏令牌、退出和账号令牌变化时同步清理 ID 与标记。首次且没有历史选择或标记时可确定选择首个可用家谱;针对调用方提供的新列表,历史选择消失或显式目标不可用时写入标记并由 G01 要求用户明确重选,跨重载也禁止静默串谱。真实后台撤权能否被及时发现仍取决于后续 workspace 的 onShow/事件刷新。 +- `appApi.submitFeedback` 是反馈提交唯一 owner:只接受 `feedbackContent` 必填、`feedbackType/contactInfo` 可选的三个字符串字段,trim 后删除空选填值;调用方不能关闭认证头。remote 精确 POST `/genealogy/app/feedback`,只认 HTTP 200 且 `code` 为整数成功码的 envelope;反馈响应没有声明 `data` 必填,页面也不消费返回实体。mock 固定失败关闭且不调用网络。 +- M07 提交中禁用输入并使用提交前快照;成功后保留回执内容并禁止原样重复提交,请求期若仍有迟到输入则明确区分“上一份已提交”和“当前修改未提交”。超时、断网、意外 2xx/3xx、HTTP 408/5xx 或响应无效进入 `uncertain`,同一快照禁止重提;unknown 回流也会重新比较当前表单,迟到输入保持可提交而不冒充旧快照。只有确定拒绝才允许重试。`tests/m07-feedback-state-runtime-smoke.js` 已直接执行这些页面状态;真实服务联调仍受认证远端门禁约束。 + +### 家谱工作区远端门禁 + +- 三人反向质询后没有要求后端把 `AppGenealogyVo` 全部字段设为必填,也不把缺少 `security` 注解或文档媒体类型 `*/*` 单独宣称为数据泄漏。首批只消费 `genealogyId/genealogyName/canView/canManage/canEditContent/roleType`;前五项需明确类型并必填,`roleType` 还需非空正式枚举。地点、堂号、人数和简介可选并由页面诚实降级,未知额外响应字段允许忽略。 +- JSON 响应身份必须使用非空词法字符串。仅在客户端拒绝 unsafe number 虽可避免串谱,却会让 OpenAPI 合法的 int64 用户永久不可用,不能作为普遍上线方案;URL path 在 wire 上本就是文本,不机械要求为了同一问题改类型。 +- `/mine` 必须以必填 `canView` 或等价、可自动验证的投影保证只把当前账号仍可查看的家谱送入 context;G01 只有成功取得新列表才 reconcile。网络、超时和 5xx 保留旧现场并显示错误,明确撤权/无权/不存在才写 tombstone。G05 使用 `/overview`,取消迟到请求并在新身份加载前清掉旧数据,远端失败绝不回退 fixture 角色。 +- OpenAPI 注解修复后仍须用有效账号执行无凭证、跨账号、撤权、删除、5xx、畸形 JSON 和取消反例;当前无令牌 HTTP 200+业务 `code=401` 的承载方式可以保留,也可改规范 HTTP 状态,但文档、运行时 validator 与部署行为必须一致。 + +### M06 帮助内容远端门禁 + +- 首批只使用 `/genealogy/app/help-articles` 一次取得完整正文;详情接口、文章深链、封面、浏览量和业务 ID 均不属于 M06。adapter 只准输出 `{category,title,content}` 与当前响应生命周期的展示键,必须显式丢弃 `helpId/status/sortOrder/viewCount/coverOssId/remark`。 +- 响应 envelope 的 `code/data` 和每行 `helpCategory/helpTitle/helpContent` 必须 required;三项均为非空字符串,分类是可直接展示标签,正文首版固定为纯文本。用户侧列表只返回已发布内容,数组顺序就是展示顺序;客户端不猜分类码、不解释 HTML/Markdown。 +- 搜索与筛选只作用于已投影数组;筛选后不得重新编号。搜索、分类和刷新先收起正文,新请求以 controller+generation 拒绝迟到响应,离页取消请求;空列表、搜索无结果、加载失败和认证失效必须分别表达。 +- M06 现有分类和问题标题使用 `` 且触控高度不足;真正接线时改为原生按钮语义,补 `aria-pressed/aria-expanded/aria-controls`、状态播报、重试和至少 44dp 目标。源码审查不能替代 MuMu 的系统字号、TalkBack、焦点与视觉验收。 + +### 个人资料读取远端门禁 + +- 唯一 profile adapter 在 mock/remote 两种模式都产出同一窄模型;运行模式必须经 `resolveRuntimeMode()` 校验,错误配置不得静默回 fixture。remote 使用严格 envelope、15 秒超时、请求取消和 generation 防迟到,不建立跨账号缓存或经路由传递个人资料。 +- 原始手机号只在 adapter 局部验证 canonical 格式,输出仅含 `maskedPhone` 与“绑定手机号,尾号 xxxx”读屏标签;页面、日志、错误、缓存和路由都不得接触明文。可选昵称、实名、邮箱只有真正省略时规范为内部空串;出现 null、空白、超长或非法邮箱时整份失败关闭。 +- M01 的加载/失败只替换身份卡,服务菜单与底栏保持可用,并删除查询参数制造的假错误和“重试即成功”;通知 fixture 不得与真实身份混装为线上数量。M02 完成异步填表后才建立 dirty baseline,GET 不冒充 PUT 保存;M03 只让手机号行局部加载/失败,密码入口不受普通读取失败影响,并删除不属于账号资料的“创建者”角色。 +- 真正实施时原子替换禁止页面调用 API 的旧静态断言,使用 `AppLoading` 和原生按钮,补 M02 的 `aria-invalid/aria-describedby`、失败聚焦、状态播报、44dp 目标、长昵称换行和装饰图隐藏;MuMu 仍须验证系统字号、TalkBack、焦点与真实纹理对比。 + +### 通知读取与已读状态远端门禁 + +- 读取批次以无筛选列表作为当前账号完整活动集合 owner,`RListNotificationVo.data` 允许空但最多 200 条并按 `publishTime` 最新优先;`RNotificationUnreadCount.data` 为 0—200,精确等于同一活动集合中 `readStatus=UNREAD` 的数量。两个请求之间发生并发新消息时允许瞬时差异,客户端不得据此判错。 +- `NotificationVo` 的首批 required 字段为 `noticeTitle/noticeContent/publishTime/readStatus`:标题 1—50、完整正文 1—1000 且为纯文本,时间必须是带时区 RFC3339,状态只允许 `READ/UNREAD`。N01 摘要最多显示 160 个字素,N02 展示同一快照的完整正文;长文本必须换行,不解释 HTML、Markdown、URL 或服务端跳转字段。 +- 读取 adapter 只公开 `{snapshotKey,title,content,publishedAt,unread}`,key 由成功响应 generation 与映射前 ordinal 组成;不持久化、不使用裸序号,也不把 `notificationId/genealogyId/bizId/bizType/senderPhone` 暴露给页面。成功刷新原子替换 generation,退出和账号切换立即清空;N02 不能解析 key 时只提示“请返回消息中心重新打开”。 +- 首批删除通用“前往入谱审核”、详情目标按钮和从消息字段猜路由;后端将来只有提供闭合的 `bizType → route key+必填词法参数+权限/失效语义` 字典后才可独立恢复 CTA。M01/G01 共同消费未读数 owner,文案统一为“未读消息”,可见角标封顶 `99+`,读屏仍播报真实数量。 +- 写批次通过前,打开 N01/N02、点击“标记已读”或“全部已读”都不得只改本地 clone。写合同通过后,唯一通知 controller 才能私有保留 1—128 位 URL-safe opaque `notificationId`;页面仍只持有 `snapshotKey`。单条和全部已读必须对当前账号幂等;read-all 只覆盖服务端接收时已存在的活动通知,并发新消息保持未读;超时或结果未知时重新读取列表和计数收敛。 +- 两个失败门禁分别是 `tests/notification-read-openapi-contract.ps1` 与 `tests/notification-read-state-openapi-contract.ps1`。当前分别输出 `NOTIFICATION-READ-OPENAPI-CONTRACT BLOCKED` 和 `NOTIFICATION-READ-STATE-OPENAPI-CONTRACT BLOCKED`;后端同版本双导出、有效账号反例和 MuMu 状态矩阵完成前不能宣称通知闭环上线。 + +### M02 个人资料写入远端门禁 + +- PUT 请求体唯一 owner 固定为关闭额外字段的 `AppProfileMergeUpdateBody`,只能包含 1—3 个真正脏的 `nickName/realName/email`。昵称出现时为去边界空白的 1—30 字符;真实姓名和邮箱的精确 `""` 是唯一清空命令,非空真实姓名 1—30、邮箱 1—100 且格式有效。省略表示保持,null、纯空白、边界空白、空 body、头像/性别/生日或任意额外字段全部早失败。 +- `AppProfileVo` 新增 required `profileVersion`,形状为 1—128 位 URL-safe opaque string;写请求只在 required `If-Match` header 携带,不在 body 建第二版本字段。服务端按当前账号和租户原子 CAS,成功返回含新版本的完整 canonical profile,旧版本固定 409 `PROFILE_VERSION_CHANGED`;相同字段集重复执行不产生通知、审计之外的额外业务副作用。 +- M02 首次 GET 完成后才建立 baseline;保存时冻结三个输入和提交快照,成功用响应回填并重置 baseline。确定失败保留草稿;超时、断网、408/5xx 或畸形成功响应属于结果未知,先 GET 逐项核对本次脏字段,匹配则确认成功、仍为旧值才允许重试、第三值进入冲突,不盲目重复 PUT。 +- session generation 是账号隔离边界:账号切换、退出和页面卸载中止等待、清空未持久草稿并拒绝旧账号迟到响应;取消 RequestTask 不等于服务端未写。真实姓名、邮箱、提交 payload 和版本不得进入日志、路由、持久缓存或遥测。 +- 当前假定昵称必填,所以旧账号昵称省略时可以只改其他字段,但不能把已有昵称清空。头像选择、相册权限、OSS ID、性别和生日全部留到独立批次;当前 `` 假头像动作在写入实施时删除。邮箱没有验证与送达合同,页面不得继续承诺“用于接收通知”。 +- `tests/profile-update-openapi-contract.ps1` 当前输出 `PROFILE-UPDATE-OPENAPI-CONTRACT BLOCKED`。后端关闭 `API-PROFILE-UPDATE-001`—`004`、同版本双导出通过、读取 owner 可用、有效账号/CORS 反例和 MuMu 表单矩阵完成前,M02 保持诚实本地校验而不发写请求。 + +### M10 当前设备退出远端门禁 + +- 后端 DELETE 只撤销当前请求 bearer 及同一可续签凭证族;其他设备 token 保持有效。合法签发给同一 client 的 active/revoked/expired token 重复调用统一返回 200 RVoid,操作幂等且无额外副作用;成功后用旧 token 调 profile 等受保护接口必须失败。非法或 client 不匹配返回 `RLogoutRejected`,业务码只允许 `TOKEN_INVALID/TOKEN_CLIENT_MISMATCH`,不能冒充远端已撤销。 +- operation 必须 required SaToken 与非空 clientid、禁止 request body、200/401 使用 application/json 并带 `Cache-Control: private, no-store`;RVoid 的整数 code required,另声明 400/429/500。线上仅有文字“需要登录”和 `*/*`,本地虽有 security/clientid 但 RVoid 无 required、只列 200,均未达到发布合同。 +- `logoutCoordinator` 是唯一跨页 owner:同步捕获 token/clientid 与 logoutEpoch,立即调用 session owner 清 token、家谱上下文和账号缓存并 bump epoch,再以显式 A 快照创建 RequestTask;整个同步段不 await,M10 页面拿不到 token。创建失败也不恢复;请求不绑定页面 controller,reLaunch/A01/M10 卸载不 abort。 +- coordinator 的公开内存态只含 `{attemptId,logoutEpoch,status}`,状态为 pending/confirmed/unconfirmed/not-revoked,不保存 token、请求头或错误 payload。A01 仅在 session 仍为空且 epoch 未变化时原子消费一次提示;B 登录后 A 的迟到结果直接丢弃。进程被杀允许丢提示,但旧 token 不落盘、不排队、不跨重启重试。 +- 唯一用户承诺始终是“已从本机退出”。200 显示服务器撤销已确认;network/timeout/408/429/5xx/畸形响应与 generic 401 显示未确认;typed 401/400/403 显示未能撤销。所有分支都留在 A01,不恢复 token、不返回 M10、不重新开放登录后页面;异步 callback/finally 绝不能再次 `session.clear()`。 +- `tests/logout-openapi-contract.ps1` 当前输出 `LOGOUT-OPENAPI-CONTRACT BLOCKED`。同版本双导出、两设备 token 隔离/复用反例、真实网络异常和 MuMu 退出提示完成前,只能称现有行为为本机退出,不能宣称服务端注销完成。 + +### M04 登录态修改密码远端门禁 + +- 密码 wire 是跨 A01 登录、A04 注册、A05 找回和 M04 登录态改密的单一合同。`PasswordLoginBody.password` 与 `PasswordChangeBody.oldPassword` 只引用 `CurrentPasswordSecret`;注册、找回和改密的新密码只引用 `NewPasswordSecret`。生产入口不再接受 MD5、十六进制摘要或两套兼容分支;确认密码只留客户端。 +- `CurrentPasswordSecret` 是原样、不 trim 的 1—64 Unicode code point;`NewPasswordSecret` 是 NFC 后 15—64 code point,允许空格、Unicode 和密码管理器粘贴,不强制字母/数字组成。服务端才是策略权威,必须执行常见/泄露密码 blocklist、当前密码验证、新旧不同、账号级限速和带盐慢哈希;页面校验只是即时提示。 +- PUT required SaToken、非空 clientid 与关闭额外字段的 JSON body;200/400/401/409/422/429/500 均为 typed JSON、`private, no-store`,429 带 `Retry-After`。409/422 用 `RPasswordChangeRejected` 区分 `CREDENTIAL_VERSION_CONFLICT/CURRENT_PASSWORD_INCORRECT/NEW_PASSWORD_SAME_AS_CURRENT/PASSWORD_POLICY_VIOLATION`,不解析 `msg`。 +- 唯一会话方案是 ALL:严格 200 前原子写入新 verifier、提升 credential epoch,并让所有设备/所有 client 的旧 access/refresh session 跨节点失效;不返回新 token,也不让当前 bearer 继续存活。200、401、409 和传输结果未知都清秘密与对应本机会话并回 A01;只有明确未写的 400/422/429 可清 `credentialChangeInFlight` 后留页。 +- 为关闭 PUT 已发出后进程被杀的窗口,session owner 在发送前只持久化 `{sessionEpoch,startedAt}` marker,禁止保存 token、密码、摘要或 body。冷启动发现同 epoch marker 时必须在任何账号缓存渲染前清会话并进 A01;新登录 bump epoch,使旧 marker 和迟到响应失效。客户端不自动重试、不建 operation-status;重新登录就是最小对账路径。 +- `tests/password-change-openapi-contract.ps1` 当前输出 `PASSWORD-CHANGE-OPENAPI-CONTRACT BLOCKED`。后端关闭 `API-PASSWORD-001`—`005`、原子重导双文件、密码登录 TAC 可用、两设备/并发/故障注入与 MuMu 无障碍矩阵完成前,M04 继续显示“不提交服务器”。 ## 三人规则 @@ -107,15 +239,24 @@ Apifox 是接口合同的唯一源头。`APP.openapi.json` 用于自动扫描和 - 不执行 Git add、commit、push、restore、checkout 或 reset。 - 不启动、关闭或调整 MuMu。 -- 不删除源码、接口导出、当前权威设计或用户改动。 +- 不删除活动业务源码、接口导出、当前权威设计或用户改动;只有三人以消费者、合同和资产清单共同证明已经退役的旧入口才可同轮删除。 - 不因测试失败而删除测试。 - 不因静态搜索无匹配而直接删除资产。 - 不归档确认失效的旧内容,不保留兼容入口。 ## 下一步 -1. 按 `docs/家谱项目全量治理实施计划.md` 的任务 1 先写路由注册表失败合同,再实现两个导航唯一所有者。 -2. 认证、G、T、F、R、N/M 每批独立运行聚焦合同、全局响应式合同、编译审计和 MuMu 进入/返回/取消/完成/重复进入矩阵。 -3. 导航门禁完全通过后,运行当前 OpenAPI 上预期失败的 T01 图合同,把 `API-T01-001` 交给后端;只接受用户从更新后的 Apifox 重新导出的 JSON/YAML。 +1. T01 后端接口门禁已经建立并运行出预期红灯;等待后端按 `API-T01-001` 发布四条 v2 操作并提供同一版本重新导出的 JSON/YAML,不手改当前快照,也不提前实施任务 12。 +2. TAC 客户端批次已经完成。认证后端仍须关闭 `API-AUTH-TAC-001`—`004`、提供同版本双导出并修复 challenge 500;随后才允许把 `runtimeConfig.mode` 从 `mock` 切为 `remote`,执行正式 H5 CORS、真实票据重放/限流和 MuMu Android 流程。 +3. 并行推进非交互风控的限时 POC 与文字/中继人工兜底合同;只有 POC、服务端原子消费和 `ANDROID-AUTH-ACCESSIBILITY-RELEASE` 全部通过,才可宣称认证达到上线门槛。 4. 新图合同通过后,按规范化与校验、布局、Scene 与空间索引、单 Canvas、页面交互、接口回流的顺序实施 T01。 -5. T01 完成后,再依次规划短信验证码、跨页面领域数据持久化、全局文字层级与无障碍第二轮,不混合阶段。 +5. 家谱工作区三人审查和失败合同已经完成;等待后端关闭 `API-GENEALOGY-WORKSPACE-001`—`003` 并提供同版本双导出。门禁通过后再测试先行实现唯一 adapter、G01 `onShow` 列表现场和 G05 `/overview` 读取,不提前固化字段或枚举。 +6. G03 原子创建三人审查和失败合同已经完成;等待后端关闭 `API-G03-001`—`005`、以同版本双导出交付 bootstrap POST、状态查询、地区选择与统一 accessPreset。通过前不接当前宽松 `createGenealogy`;通过后先实现严格 coordinator、无 PII 恢复 marker、地区选择器、context/G05/G01 闭环,再独立完成 MuMu 表单与无障碍矩阵。 +7. M06 三人审查和失败合同已经完成;等待后端关闭 `API-M06-001`—`003` 并提供同版本双导出。通过后测试先行实现 list-only adapter、加载/空/失败/重试、动态分类、纯文本手风琴和无障碍语义,不接详情或文章 ID。 +8. 个人资料读取三人审查和失败合同已经完成;等待后端关闭 `API-PROFILE-READ-001`—`003` 并提供同版本双导出。通过后测试先行实现唯一掩码 adapter 与 M01/M02/M03 局部状态,M05 当前手机号展示随同一只读 owner 原子迁移但不接换绑写接口。 +9. 通知读取与已读写入已分别完成三人审查和失败合同;等待后端关闭 `API-NOTIFICATION-READ-001`—`003` 与 `API-NOTIFICATION-STATE-001`—`003`。读取通过后先实现无 ID 的内存快照和 M01/G01 未读数;写入另批迁移私有 ID、删除伪本地写并验证并发收敛,不恢复不可信目标 CTA。 +10. M02 个人资料 PUT 已完成三人审查和失败合同;等待后端关闭 `API-PROFILE-UPDATE-001`—`004`。读取与写入门禁都通过后,按 normalizer、严格 API、M02 状态机和 MuMu 表单矩阵独立实施,不混头像或账号安全。 +11. M10 服务端退出已完成三人审查和失败合同;等待后端关闭 `API-LOGOUT-001`—`003` 后,再测试先行实现 session epoch、唯一 logoutCoordinator、A01 一次性提示和两设备撤销矩阵。 +12. M04 登录态改密与四条密码 wire 已完成三人审查和失败合同;等待后端关闭 `API-PASSWORD-001`—`005` 后,才原子迁移共享密码策略、MD5 调用、session marker、M04 状态机和全设备撤销矩阵。 +13. M05 手机号换绑三人审查和失败合同已经完成;等待后端关闭 `API-PHONE-001`—`005`,且 M04 raw-password 与认证/TAC 前置门禁同时通过后,再原子迁移全活动 OTP、实现专用受保护发码、共享 credential marker 与 M05 状态机。 +14. 继续审查下一个不依赖现有红灯的业务域;按 G/F/R 逐域推进。随后逐批完成文字层级与无障碍,不混合验证码、账号写入、领域数据或支付。 diff --git a/pages/auth/a01-entry.vue b/pages/auth/a01-entry.vue index 4a966bf..56b2dfb 100644 --- a/pages/auth/a01-entry.vue +++ b/pages/auth/a01-entry.vue @@ -11,20 +11,24 @@ /> - @@ -89,33 +100,38 @@ v-model.trim="verificationCode" class="auth-input" type="number" - maxlength="6" + maxlength="4" + :disabled="submitting" placeholder="短信验证码" + aria-label="短信验证码" placeholder-class="input-placeholder" confirm-type="done" @confirm="submitLogin" /> - 获取验证码{{ sendingCode ? "请求中…" : cooldownSeconds > 0 ? `${cooldownSeconds}s 后重发` : "获取验证码" }} - 忘记密码忘记密码 - + + diff --git a/pages/auth/a04-register.vue b/pages/auth/a04-register.vue index 105666b..e1ff88a 100644 --- a/pages/auth/a04-register.vue +++ b/pages/auth/a04-register.vue @@ -3,13 +3,19 @@ - + 注册账号 创建属于你的家谱账号 - 手机号 + - {{ + {{ fieldErrors.phone }} + + + + + + + {{ + fieldErrors.verificationCode + }} + + - 设置密码 + - {{ + {{ fieldErrors.password }} @@ -67,25 +114,31 @@ :class="{ 'field-block--error': fieldErrors.confirmPassword }" > - 确认密码 + - {{ + {{ fieldErrors.confirmPassword }} - @@ -94,91 +147,189 @@ src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /> - 注册账号 - + {{ submitting ? "注册中…" : "注册账号" }} + - - + + 我已阅读并同意 - 《用户协议》《用户协议》 - 《隐私政策》《隐私政策》 - 请先阅读并同意用户协议与隐私政策 diff --git a/pages/auth/a05-reset-password.vue b/pages/auth/a05-reset-password.vue index 573b2e0..482ae18 100644 --- a/pages/auth/a05-reset-password.vue +++ b/pages/auth/a05-reset-password.vue @@ -3,13 +3,19 @@ - + 重设密码 验证手机号后设置新的登录密码 - 手机号 + - {{ + {{ fieldErrors.phone }} @@ -46,24 +56,30 @@ :class="{ 'field-block--error': fieldErrors.verificationCode }" > - 验证码 + - 获取验证码{{ sendingCode ? "请求中…" : cooldownSeconds > 0 ? `${cooldownSeconds}s 后重发` : "获取验证码" }} - {{ + {{ fieldErrors.verificationCode }} @@ -73,18 +89,22 @@ :class="{ 'field-block--error': fieldErrors.password }" > - 新密码 + - {{ + {{ fieldErrors.password }} @@ -94,25 +114,31 @@ :class="{ 'field-block--error': fieldErrors.confirmPassword }" > - 确认新密码 + - {{ + {{ fieldErrors.confirmPassword }} - @@ -121,64 +147,92 @@ src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png" mode="aspectFit" /> - 确认重设 - + {{ submitting ? "提交中…" : "确认重设" }} + 重设成功后,请使用新密码登录 diff --git a/pages/profile/m08-promotion.vue b/pages/profile/m08-promotion.vue index 447e11b..5b01d4b 100644 --- a/pages/profile/m08-promotion.vue +++ b/pages/profile/m08-promotion.vue @@ -1,47 +1,46 @@ diff --git a/pages/profile/m09-vip-orders.vue b/pages/profile/m09-vip-orders.vue index 4696572..3a2585c 100644 --- a/pages/profile/m09-vip-orders.vue +++ b/pages/profile/m09-vip-orders.vue @@ -1,40 +1,45 @@ diff --git a/pages/profile/m10-about-settings.vue b/pages/profile/m10-about-settings.vue index 7a7520c..a28c73a 100644 --- a/pages/profile/m10-about-settings.vue +++ b/pages/profile/m10-about-settings.vue @@ -2,39 +2,55 @@ diff --git a/pages/records/r10-memo-list.vue b/pages/records/r10-memo-list.vue index ee45a0d..b5a4afb 100644 --- a/pages/records/r10-memo-list.vue +++ b/pages/records/r10-memo-list.vue @@ -1,246 +1,251 @@ - +