diff --git a/components/TacVerification.vue b/components/TacVerification.vue index 0fd2ff2..304d57e 100644 --- a/components/TacVerification.vue +++ b/components/TacVerification.vue @@ -239,8 +239,6 @@ export default { if (type === "requestCaptchaData") { request.data = { tenantId: this.requestContext.tenantId, - clientId: this.requestContext.clientId, - sceneCode: this.requestContext.sceneCode, subject: this.requestContext.subject, }; return true; diff --git a/docs/APP-136接口功能归属与重复关系-2026-07-26.md b/docs/APP-136接口功能归属与重复关系-2026-07-26.md new file mode 100644 index 0000000..3f7702f --- /dev/null +++ b/docs/APP-136接口功能归属与重复关系-2026-07-26.md @@ -0,0 +1,82 @@ +# APP 136 接口功能归属与重复关系 + +更新时间:2026-07-26 + +## 结论 + +Apifox 当前目录共 136 条:128 条 APP 接口、4 条 APP/PC 共用行政区划接口、4 条旧系统兼容验证接口。 + +“136 条”不等于 136 个独立页面功能。真正功能重复的只有旧兼容验证/发码链路;列表、分页、详情、选项、管理态接口是同一资源在不同交互下的不同合同,不能擅自删掉或混用。 + +字段、必填、枚举、请求/响应 DTO 以桌面 Apifox 为准。根目录 `家谱.openapi.json` 只用于核对目录数量和路径,不用其缺失字段推断前端 DTO。 + +## 一、唯一入口与功能重复 + +| 分类 | 接口 | 当前唯一使用策略 | 结论 | +| --- | --- | --- | --- | +| APP 人机验证 | `GET /genealogy/app/auth/verification/{operationCode}/require`、`POST .../challenge`、`POST .../verify` | A01 密码/短信登录、A04 注册、A05 找回密码,通过 `TacVerification` 与 `utils/auth-verification.js` | 当前 APP 唯一验证链路 | +| 旧验证码兼容 | `GET /captcha/require`、`POST /captcha/challenge`、`POST /captcha/verify`、`GET /auth/code` | 当前 APP 不调用 | 与 APP 人机验证功能重叠,保留 API 目录记录但不能再接回 A01/A04/A05,避免重复验证 | +| APP 场景发码 | `POST /genealogy/app/auth/sms/{operationCode}/code` | A01/A04/A05;`operationCode` 是 `password-login`、`sms-login`、`register`、`forgot-password` | 当前 APP 唯一发码链路 | +| 旧短信发码兼容 | `POST /genealogy/app/auth/sms/code` | 仅 `appApi.sendLegacySmsCode` API 层 owner;没有页面入口 | 与上项发码功能重叠。保留兼容方法供后端指定旧调用方,不得在现有认证页重复发短信 | +| 行政区划(共享) | `GET /genealogy/region/children`、`/path/{regionCode}`、`/search`、`/{regionCode}` | APP 与 PC 共用;当前 G03/G11 使用 children | 不是旧接口、不是重复接口。其余三条在对应 UI 出现“回填路径/搜索/单项校验”交互后接入 | + +## 二、136 条按功能模块归属 + +| Apifox 模块 | 数量 | 应归属页面/组件 | 备注 | +| --- | ---: | --- | --- | +| 验证中心 | 7 | A01、A04、A05、`TacVerification` | 其中 4 条为旧兼容,见上表 | +| 认证登录 | 12 | A01、A04、A05、M01、M02、M04、M05、M10 | 改密、换绑、注销、退出属于敏感写操作,测试不执行 | +| 文件上传 | 6 | G03、G11、M02、F02、F06、F07、F09、R04、R07、R08、R10,通过 `resumable-image-upload.js` | 单文件、分片 init/chunk/complete、业务引用绑定/释放各自用途不同 | +| 行政区划 | 4 | G03 创建家谱、G11 家谱设置 | APP/PC 共用基础查询 | +| 家谱 | 13 | G01、G03、G05、G06、G08、G09、G10、G11 | 配额、options、详情、审核/撤销等要与具体页面按钮逐项接线,不因已有列表接口视为完成 | +| 家谱成员 | 6 | 当前没有“家谱成员(账号成员)管理”页面;不要误接到世系人物 T03–T08 | `memberId` 与 `personId` 不是同一 ID。成员管理页/候选 DTO 缺失需单列 | +| 字辈谱 | 6 | G12 | 正常列表、维护列表、批量预览、批量保存、单条新增/修改是不同操作 | +| 世系人物 | 12 | T01、T03–T08、R01、R02 | 首位成员创建后端 `code:500` 阻塞;不能伪造 personId | +| 家族圈 | 14 | F01、F02、F03 | 列表与分页、评论与评论分页、回复与回复分页均非重复;编辑/删除/点赞/回复 UI 需逐项确认 | +| 内容文章 | 9 | F04、F05、F06、M06、M08 | 谱文分类、帮助详情目前没有已确认页面入口,不能靠列表代替 | +| 相册 | 7 | F07、F08、F09 | 相册本身与相片记录是两级资源;编辑/删除动作需遵守测试禁止删除边界 | +| 祭祀 | 8 | R05、R06、R07 | 活动、献礼分别有列表和写入合同 | +| 族务记录 | 18 | R03、R04、R08、R10、R11 | 成长/备忘/亲友的详情、修改、删除需有明确详情或编辑入口;R09 人生事件没有独立资源合同 | +| 消息通知 | 4 | N01、G01 未读数 | N02 没有单条详情读取接口;标已读/全部已读不能在未授权测试中触发 | +| 意见反馈 | 2 | M07 | 列表与提交分别接线 | +| VIP | 3 | M09 | 套餐、订单列表可读取;创建订单属于支付链路,不执行 | +| 视频 | 1 | F10 | 仅有删除视频接口,缺少视频列表/详情/上传/播放合同,因此 F10 不能假装可用 | +| 贺礼邀约 | 4 | R06(活动详情)及“我的活动邀请”入口待产品页面 | 受邀人需要真实 `appUserId`;当前成员/世系 DTO 没有可确认候选来源,不能拿 `memberId` 或 `personId` 猜代 | + +以上数量相加为 136。 + +## 三、共享行政区划的实测合同 + +桌面 Apifox 已核对: + +| 接口 | 必填 | 可选 | 当前 API owner | 页面状态 | +| --- | --- | --- | --- | --- | +| `GET /genealogy/region/children` | 无 | `parentCode`(不传或 `0` 为省级) | `appApi.getRegionChildren` | G03/G11 已真实读取 | +| `GET /genealogy/region/path/{regionCode}` | path `regionCode:string` | `clientid` header | `appApi.getRegionPath` | 暂无“按已存地区回填层级路径”控件 | +| `GET /genealogy/region/search` | query `keyword:string` | `level: 1..5`、`limit:integer`、`clientid` header | `appApi.searchRegions` | 暂无地区关键字搜索控件 | +| `GET /genealogy/region/{regionCode}` | path `regionCode:string` | `clientid` header | `appApi.getRegion` | 暂无单项详情校验控件 | + +后三条返回 DTO 在桌面文档当前显示为通用对象/列表对象,API 层只校验 envelope 与最外层数组/对象,不擅自猜测内部字段。等页面要消费具体字段时,先在桌面 Apifox 展开响应模型并补严字段校验。 + +## 四、接线判定标准 + +一条接口只有同时满足以下三项才标记“页面完成”: + +1. `utils/api.js` 有唯一方法 owner,按桌面 Apifox 的必填/可选参数组装请求; +2. 明确页面按钮、页面加载或组件行为调用该方法,且参数来源不是猜测 ID; +3. 用浏览器在测试账号和测试数据上做真实请求验证;写入再做列表或详情回读。 + +仅有 API 方法、或仅页面能打开、或只有导出文件字段,均不算页面完成。 + +## 五、2026-07-26 浏览器复测结论 + +- 从 A01 密码登录进入测试账号后,以真实 `genealogyId`、动态、谱文和相册标识运行 `tests/all-page-route-runtime-smoke.js`,52/52 页面均通过;认证态访问 A01 被守卫重定向到 G01 属于预期行为,测试已明确校验该分支。 +- G01 的“世系图、成员、字辈诗、申请审核”四个快捷入口均通过浏览器页面点击复测,分别到达 T01、G05、G12、G10,未捕获运行时异常。 +- T04 再次经页面输入首位成员姓名并提交。请求仍未获服务端确认,页面显示“保存失败 / 发生未知异常,请联系管理员”;未写入本地成员、未生成假 `personId`。该结果继续受“首位成员创建 code:500”阻塞。 +- 该路由复测只验证真实读取、页面状态和既有测试数据;未执行短信、改密、换绑、退出、删除、审核或支付。 + +## 六、桌面 Apifox 与导出目录核对 + +桌面 Apifox 本地接口树(2026-07-26 16:56 更新)包含根目录 `家谱.openapi.json` 的全部 136 个 `HTTP method + path`;没有只存在于导出而桌面目录不存在的 operation。导出文件可用于路径、方法和数量的完整审计。 + +该核对不扩大为 DTO 字段已完整:页面要消费的 body/response 字段、必填、枚举和示例仍以桌面 Apifox 的接口详情为准。若桌面详情未声明条目 DTO 或枚举,前端继续把该字段标为阻塞,而不从导出占位结构猜测。 diff --git a/docs/APP-136请求参数字段核对表-2026-07-26.md b/docs/APP-136请求参数字段核对表-2026-07-26.md new file mode 100644 index 0000000..6f59fc8 --- /dev/null +++ b/docs/APP-136请求参数字段核对表-2026-07-26.md @@ -0,0 +1,75 @@ +# APP 136 请求参数字段核对表 + +更新时间:2026-07-26 + +## 使用规则 + +- 路径和 operation 数量以桌面 Apifox 当前 APP 目录为准;本机已核对根目录 `家谱.openapi.json` 的 136 个 `method + path` 均存在于桌面目录。 +- 下表字段来自该次导出快照,用于逐项核对前端 consumer。桌面 Apifox 若显示更严格的 required、枚举、长度、oneOf 或条目 DTO,以桌面详情覆盖本表并重新导出。 +- `int64` 在 H5 JSON 中不得强制转换为 JavaScript `number`。真实雪花 ID 超过安全整数范围时,必须由后端改为十进制字符串合同,不能截断或猜测。 +- “阻塞”表示前端没有安全的参数来源、DTO、operation 或服务端成功响应;不是用本地默认值补齐的许可。 + +## 公共路径与查询参数 + +| operation 类别 | 必填 | 可选 | 当前页面/状态 | +| --- | --- | --- | --- | +| 绝大多数 `/genealogy/app/**` | Header `clientid` | 当前登录 bearer | API 请求层统一注入;实际浏览器验证 | +| 资源路径 `/{genealogyId}` | path `genealogyId` | — | 所有家谱内页面从真实当前家谱上下文取得 | +| 世系资源 `/{personId}` | path `personId` | — | T03–T08、R01–R02 被首位成员 `code:500` 阻塞 | +| 动态资源 `/{feedId}` | path `feedId` | — | F01/F03 实际读取;点赞状态 DTO 阻塞 | +| 谱文/相册/礼仪/记录资源 ID | 对应 path ID | — | 已有真实测试数据的列表/详情/创建按页面验证;编辑/删除需要产品入口或受测试边界限制 | +| 分页 | `pageNum`、`pageSize` 在 Apifox 均为可选 | keyword、generation、status 等按各 operation | 没有页面分页交互时不以默认假分页替代 | +| 行政区划搜索 | query `keyword` | `level` 1–5、`limit` | `children` 已在 G03/G11 实读;其余三个尚无对应交互控件 | + +## 写入 body 字段 + +星号为当前导出中的 required 字段。没有写入页面、稳定候选 ID 或枚举的字段均明确列为阻塞,不能手填内部 ID。 + +| Schema / operation | 页面 | 必填字段 | 可选字段 | 当前消费结论 | +| --- | --- | --- | --- | --- | +| `VerificationChallengeBody`(challenge) | A01/A04/A05 | `tenantId`、`subject` | — | 已接验证组件 | +| `VerificationCheckBody`(verify) | A01/A04/A05 | `tenantId`、`subject`、`challengeId` | `providerCode`、`captchaType`、`payload` | provider 细节以桌面 TAC DTO 为准 | +| `PasswordRegisterBody` | A04 | `grantType`、`tenantId`、`phone`、`password`、`smsCode` | `nickName`、`registerSource` | 短信写入未执行(测试边界) | +| `PasswordLoginBody` | A01 | `grantType`、`tenantId`、`phone`、`password` | `validToken` | 已真实登录 | +| `SmsLoginBody` | A01 | `grantType`、`tenantId`、`phone`、`smsCode` | — | 短信写入未执行 | +| `SmsCodeBody` | A01/A04/A05 | `grantType`、`tenantId`、`phone` | `validToken` | 场景路径已接;未发短信 | +| `ProfileUpdateBody` | M02 | — | `nickName`、`avatarOssId`、`sex`、`birthday`、`provinceCode`、`cityCode`、`districtCode` | 头像 `ossId` 类型冲突;字典/地区回填 DTO 需桌面详情确认 | +| `PasswordChangeBody` | M04 | `oldPassword`、`newPassword` | — | 敏感操作,不执行 | +| `PasswordResetBody` | A05 | `grantType`、`tenantId`、`phone`、`smsCode`、`newPassword` | — | 短信/改密不执行 | +| `PhoneChangeBody` | M05 | `phone`、`smsCode` | — | 敏感操作,不执行 | +| `AccountDeactivateBody` | M10 | `smsCode` | — | 敏感操作,不执行 | +| `ResumableInitBody` | 上传组件 | 实际服务端:`uploadId`、`fileName`、`fileMd5`、`totalSize`、`chunkSize`、`totalChunks` | `contentType` | 导出快照写为 `fileSize` 且漏 `uploadId`;真实请求省略 `uploadId` 返回“上传ID不能为空”,改用 `fileSize` 返回“文件大小不能为空”。前端只按已验证的实际契约发送,等待 Apifox 统一(B17) | +| `ResumableCompleteBody` | 上传组件 | 实际已验证链路:`uploadId`、`fileName`、`fileMd5`、`totalSize`、`totalChunks` | — | 导出快照与实际字段冲突待后端统一;回执 `ossId` 仍为不安全 19 位字符串 | +| `FileReferenceBody` | 业务附件 | `bizType`、`bizTable`、`bizId`、`bizField` | `bizName`、`ossId`、`ossIds`、`usageScene`、`usageName` | 缺稳定业务表/字段 owner,且 `ossId` 类型冲突;不能猜绑定 | +| `GenealogyCreateBody` | G03 | `genealogyName`、`surname`、`regionCode` | `ancestralHall`、`originPlace`、`addressDetail`、`coverOssId`、`intro`、`visibility`、`joinMode` | 已真实创建/回读;封面受 `ossId` 阻塞 | +| `GenealogyUpdateBody` | G11 | — | 与创建同名字段 | 读取已接;写入需页面确切 dirty/权限合同 | +| `GenealogyJoinApplyBody` | G08 | — | `applicantName`、`phone`、`relationDesc`、`applyReason`、`inviterUserId` | 已真实申请列表回读;内部邀请人 ID 无候选来源 | +| `GenealogyJoinAuditBody` | G10 | `status` | `auditRemark` | 审核为敏感操作,不执行 | +| `GenealogyMemberUpdateBody` | 无独立成员管理页 | — | `memberName`、`relationName`、`roleType`、`lineagePersonId` | `memberId` 非 `personId`;不得误接世系页 | +| `GenealogyOwnerTransferBody` | 无 | `targetMemberId` | — | 缺成员候选与产品入口,不能手填 ID | +| `GenerationPoemBody` | G12 | `generationNo`、`generationText` | `description`、`sortOrder`、`status` | 已真实维护/回读 | +| `GenerationPoemBatchBody` | G12 | `poemText` | `disableMissing` | 已真实预览/保存回读 | +| `LineagePersonBody` | T04/T05/T06、R02 | `name` | `appUserId`、`personNo`、`aliasName`、`sex`、`generation`、`generationName`、`fatherId`、`motherId`、`avatarOssId`、`birthDate`、`birthLunar`、`birthPlace`、`deathDate`、`deathLunar`、`deathPlace`、`burialPlace`、`personStatus`、`biography`、`sortOrder`、`remark`、`relationName` | 首位成员最小合法请求仍 `code:500`;业务用户/父母候选、性别/状态字典、头像 `ossId` 均无安全闭环 | +| `FamilyFeedBody` | F02/F03 编辑入口缺失 | `feedContent` | `feedType`、`mediaOssIds`、`sortOrder`、`status` | 文本动态已真实创建/回读;媒体受 `ossId` 阻塞;编辑无产品入口 | +| `FamilyFeedCommentBody` | F03 | `commentContent` | `parentCommentId` | 一级评论已真实创建/回读;回复 UI 未设计 | +| `ArticleBody` | F06 | `articleTitle`、`articleContent` | `categoryId`、`articleSummary`、`coverOssId`、`authorName`、`sortOrder`、`status` | 已真实创建/回读;分类条目 DTO、封面 `ossId` 阻塞 | +| `AlbumBody` | F07 | `albumName` | `albumDesc`、`coverOssId`、`sortOrder`、`status` | 已真实创建/回读;封面 `ossId` 阻塞 | +| `AlbumPhotoBody` | F09 | `ossId` | `photoTitle`、`photoDesc`、`photographer`、`shootTime`、`sortOrder`、`status` | 上传回执成功,但 `ossId` JSON 类型冲突,不能创建照片记录 | +| `CeremonyBody` | R07 | `ceremonyType`、`ceremonyTitle` | `ceremonyDesc`、`ceremonyTime`、`location`、`locationAddress`、`longitude`、`latitude`、`coverOssId`、`sortOrder`、`status` | 已真实创建/回读;封面受 `ossId` 阻塞 | +| `CeremonyGiftBody` | R06 | `giftAmount` | `giverName`、`giftMessage` | 礼仪详情/献礼读取已接;写入需从真实礼仪 ID 进入 | +| `GrowthRecordBody` | R08 | `recordTitle` | `lineagePersonId`、`recordType`、`recordContent`、`recordDate`、`remindTime`、`mediaOssIds`、`sortOrder`、`status` | 无真实 personId 时人物归属不能填;非人物字段已真实创建/回读 | +| `MemoBody` | R10 | `memoTitle` | `memoContent`、`remindTime`、`completed`、`mediaOssIds`、`sortOrder`、`status` | 已真实创建/回读;附件受 `ossId` 阻塞 | +| `RelativeRecordBody` | R04 | `relativeName` | `relationName`、`eventName`、`eventTime`、`giftAmount`、`recordContent`、`mediaOssIds`、`sortOrder`、`status` | 已真实创建/回读;附件受 `ossId` 阻塞 | +| `MeritRecordBody` | R11 | `donorName`、`meritTitle` | `meritType`、`meritContent`、`amount`、`meritTime`、`sortOrder`、`status` | 已真实创建/回读 | +| `FeedbackBody` | M07 | `feedbackContent` | `feedbackType`、`contactInfo` | 已真实提交/读取 | +| `VipOrderBody` | M09 | `packageId` | `genealogyId`、`payType` | 创建订单属于支付,不执行 | +| `CeremonyInviteesBody` | R07/R06 | `inviteeUserIds` | — | 缺 `appUserId` 候选列表;不能用 member/person ID 猜代 | +| `CeremonyInvitationResponseBody` | 我的活动邀请页缺失 | `inviteStatus` | — | 枚举仅 `ACCEPTED`/`DECLINED`;需要产品入口和真实邀请 | + +## 后端需要优先确认的统一规则 + +1. 所有 `int64` 在 APP JSON 请求/响应中是否统一为十进制字符串;至少要覆盖 `ossId`、所有资源 ID、`appUserId`、`memberId`、`personId`、`categoryId` 和 `packageId`。 +2. 所有“建议使用字典值”的字段必须在 Apifox 给出 enum 或独立 options operation,特别是 `sex`、`personStatus`、`roleType`、`status`、`visibility`、`joinMode`、`payType`。 +3. 任何需要选择内部用户/成员/人物的操作必须返回明确且同类型的候选 ID,禁止要求客户端用不同资源的 ID 猜代。 +4. 上传完成回执和所有消费 `ossId` 的 DTO 必须同版修复;否则上传成功并不等于业务图片创建成功。 +5. 分片上传 init/complete 的 Apifox 导出字段必须与实际服务端校验一致:当前实测要求 `uploadId` 与 `totalSize`,而导出写为 `fileSize` 且未声明 `uploadId`;不得要求客户端同时发送两套互斥字段。 diff --git a/docs/APP表单控件与接口字段审计-2026-07-26.md b/docs/APP表单控件与接口字段审计-2026-07-26.md new file mode 100644 index 0000000..424b234 --- /dev/null +++ b/docs/APP表单控件与接口字段审计-2026-07-26.md @@ -0,0 +1,63 @@ +# APP 表单控件与接口字段审计 + +更新日期:2026-07-26 +依据:桌面 Apifox APP 合同、`家谱.openapi.json`、当前页面真实表单代码。 + +## 判定规则 + +| 字段性质 | 页面控件 | +| --- | --- | +| 自由文本、标题、说明、地点、姓名 | 文本输入或多行输入 | +| `date`、明确日期 | 日期选择器 | +| 明确日期时间 | 日期与时间选择器,页面组合后提交标准时间字符串 | +| 金额、排序 | 数字输入 | +| 地区、分类、人物、业务用户、成员等资源 ID | 从真实候选列表选择;禁止手填内部 ID | +| `sex`、`status`、是否农历等字典值 | value-label 选择器;必须有 Apifox enum 或字典 options 来源 | +| `ossId`、封面、头像、媒体 | 真实上传组件;禁止手填文件 ID | + +## 已确认需要调整的表单 + +| 页面 | 字段 | 当前控件/状态 | 正确控件 | 依赖或阻塞 | +| --- | --- | --- | --- | --- | +| T04、T05 | `birthLunar`、`deathLunar` | 文本输入,且提示为“按家谱记载填写” | “是否农历”选择器 | B15:Apifox 只有字典值描述,无 value-label enum | +| T04、T05 | `sex`、`personStatus` | 页面缺入口 | 字典选择器 | B15 | +| T04、T05 | `fatherId`、`motherId`、`appUserId` | 页面不能从候选选择 | 人物/业务用户候选选择器 | B06、B09/B13;首位成员不能填父母 | +| T04、T05 | `avatarOssId` | 页面未接头像上传 | 真实上传组件 | B08:19 位 `ossId` 类型冲突 | +| M02 | `sex` | 文本输入 | 字典选择器 | B15 | +| M02 | `provinceCode`、`cityCode`、`districtCode` | 页面缺入口 | 行政区划级联选择器 | B12 的接口 DTO 已可读;需接入页面 | +| G08 | `inviterUserId` | 数字输入,要求用户手填内部 ID | 真实邀请人候选选择器 | B09:没有业务用户候选/映射,不能手填 | +| F06 | `categoryId` | 页面缺入口 | 谱文分类选择器 | B11:测试谱分类为空,合同没有条目字段定义 | +| F02 | `feedType` | 自由文本输入 | 动态类型选择器或固定默认类型 | 合同仅给默认 `text`,没有完整类型 enum;不能让用户随意输入未声明类型 | +| F09 | `shootTime` | 手工文本时间 | 日期与时间选择器 | 合同字段是拍摄时间;可直接改页面控件 | +| R04 | `eventTime` | 手工文本时间 | 日期与时间选择器 | 合同字段是事项时间;可直接改页面控件 | +| R07 | `ceremonyTime` | 手工文本时间 | 日期与时间选择器 | 合同字段是活动时间;可直接改页面控件 | +| R08 | `recordDate`、`remindTime` | 手工文本时间 | 日期选择器;提醒用日期与时间选择器 | 合同字段分别是记录日期、提醒时间 | +| R11 | `meritTime` | 手工文本时间 | 日期与时间选择器 | 合同字段是功德时间 | + +## 已经正确的控件 + +| 页面 | 字段 | 当前控件 | +| --- | --- | --- | +| G03、G11 | `regionCode` | 行政区划级联选择器 | +| G03、G11 | `visibility`、`joinMode` | 已封装为访问规则选择 | +| M02 | `birthday` | 日期选择器 | +| R10 | `remindTime` | 日期和时间选择器组合 | +| 所有上传表单 | `coverOssId`、`mediaOssIds`、`ossId` | 真实上传回执,不提供手填文件 ID | +| R04、R11 | 金额 | 数字输入 | +| 各编辑页 | `sortOrder` | 数字输入 | + +## 不应为了“字段齐全”强行显示的字段 + +| 字段 | 原因 | +| --- | --- | +| 首位成员的 `fatherId`、`motherId`、`relationName` | 与“首位成员/世系起点”语义冲突;不能伪造已有成员 ID | +| `personNo` | 合同明确不传时服务端生成,不应要求用户填写 | +| `avatarOssId`、`coverOssId`、`mediaOssIds` | 必须由上传组件产生,不允许文本输入 | +| `status`、`completed` 等服务端有默认值的可选字段 | 无业务需求和明确字典时不新增裸代码控件 | + +## 实施顺序 + +1. 先把 F09、R04、R07、R08、R11 的明确时间字段改为选择器并进行浏览器真实提交回读。 +2. 再接入 M02 的地区级联选择。 +3. B15、B09、B11、B06/B08 由后端补齐枚举、候选或 ID 合同后,再补世系、性别、分类、邀请人等选择器。 + diff --git a/docs/MuMu全页样式复核-2026-07-26.md b/docs/MuMu全页样式复核-2026-07-26.md new file mode 100644 index 0000000..56d5273 --- /dev/null +++ b/docs/MuMu全页样式复核-2026-07-26.md @@ -0,0 +1,57 @@ +# MuMu 全页样式复核(2026-07-26) + +## 范围与方法 + +- 设备:MuMu Android(SM-A5560),实际运行中的 APP WebView,900 × 1600 截图。 +- 范围:`pages.json` 的 52 个页面路由,以及测试账号可进入的列表、空态、错误态、新建表单和原生选择器状态。 +- 评审:三份独立结论——MuMu 实机逐页评审 1 份、核心表单代码/交互评审 1 份、家族与记录模块代码/交互评审 1 份。只采纳能由真实页面或已声明接口契约证实的结论。 +- 不使用 mock、fixture 或本地伪造成功。全部截图在 `tmp/mumu-visual-audit-20260726/`。 + +## 逐页复核结果 + +已逐页打开并复核以下 52 条路由: + +- 认证:A01、A04、A05。 +- 家谱:G01、G03、G05、G06、G08、G09、G10、G11、G12。 +- 世系:T01、T03、T04、T05、T06、T07、T08。 +- 家族:F01、F02、F03、F04、F05、F06、F07、F08、F09、F10。 +- 记录:R01、R02、R03、R04、R05、R06、R07、R08、R09、R10、R11。 +- 消息:N01、N02。 +- 我的:M01、M02、M03、M04、M05、M06、M07、M08、M09、M10。 + +结论:已进入的页面均保持朱红、金线、宣纸背景和卷轴按钮这套既有视觉系统;未发现横向页面滚动、标题遮挡、内容与底部导航重叠。长内容、空态、接口错误态和可进入的新建表单均已按实际数据复核。 + +## 已落地且在 MuMu 复验的改动 + +1. G01 当前家谱的超长名称改为单行省略,避免末尾数字孤立换行。 +2. G05 长简介限制为两行,恢复被遮挡的世系树、字辈谱、审核、设置四张功能卡;家谱统计信息收回深色头图区域。 +3. F02 动态类型固定显示为“文字动态”,不再把接口枚举值暴露成可编辑输入框;上传按钮增至可触控尺寸。 +4. F03 未返回点赞状态时使用中性说明,不再把未知状态渲染成失败样式。 +5. F10、R09、N02、M03、M05、G12、M08 移除面向用户的接口、operation、Mock 等内部术语,改为可理解的功能状态说明。 +6. M01 默认头像使用项目真实 `auth-login-outline` 图;性别代码不再直接展示。M02 在没有服务端选项字典时显示“待确认 / 选项待提供”,不伪造选择项。 +7. R02 的“人生大事”改为明确的“暂未开放”状态,不再提供无效可点击入口。 +8. F02、F06、F07、F09、R07、R08、R10 的图片上传入口最小高度统一到 88rpx。 +9. R08 成长日志的记录、提醒时间,R11 功德记录的记录时间,全部改为系统日期/时间选择器;提交时仍使用接口既有 `YYYY-MM-DD HH:mm:ss` 文本字段。 +10. R08、R10、R11 的表单底部“取消 / 提交”按钮改为容器内的可收缩网格。修复了 MuMu 窄屏上右侧提交按钮被裁切的问题;三页都已实机复验。 + +## 选择器与输入原则 + +- 日期、时间:使用实际 Android 原生选择器,未改变后端字段名或格式。 +- 有确定自由文本语义的字段仍使用输入框,例如姓名、标题、地点、人物简介、备注。 +- 性别、人物状态、农历等需要枚举/字典的字段,当前接口未提供可用选项来源;未猜测中文标签或构造假选项。 + +## 不能通过前端补齐的状态 + +| 范围 | 真实结果 | 前端处理 | +| --- | --- | --- | +| 首位成员创建 | `POST /genealogy/app/genealogies/{genealogyId}/lineage/persons` 使用最小合法 body 仍返回业务 `code:500` | 不伪造成员;T03–T08、R01–R02 的真实成员读取/写入闭环仍需后端修复或提供可读测试成员。 | +| 性别、人物状态、农历等选择项 | 接口契约未提供枚举/字典来源 | 保留真实已返回值;未知值显示“待确认”,不将代码当中文含义解释。 | +| 消息详情 | 当前测试数据没有可读的消息详情标识 | N02 复核了无详情状态和返回路径,未伪造详情内容。 | + +## 本轮验证 + +- `powershell -NoProfile -ExecutionPolicy Bypass -File tests/compile-audit.ps1`:通过。 +- `powershell -NoProfile -ExecutionPolicy Bypass -File tests/r-business-flow-contract.ps1`:通过。 +- `powershell -NoProfile -ExecutionPolicy Bypass -File tests/active-page-business-ownership-contract.ps1`:通过。 +- MuMu 实机复验:R08、R10、R11 新建表单及其底部按钮;R08 日期选择器;G01、G05、F02、F03、F10、G12、N02、M01、M02、M03、M05、M08 的改动后状态。 +- 最后一轮 MuMu 路由复测:52/52 条路由均已重新截图完成,结果位于 `tmp/mumu-visual-audit-20260726/final-routes/`。 diff --git a/docs/全量页面接口联调交接说明-2026-07-24.md b/docs/全量页面接口联调交接说明-2026-07-24.md index 6461261..53934cd 100644 --- a/docs/全量页面接口联调交接说明-2026-07-24.md +++ b/docs/全量页面接口联调交接说明-2026-07-24.md @@ -1,6 +1,6 @@ # 全量页面接口联调交接说明 -更新日期:2026-07-24 +更新日期:2026-07-26 适用项目:家谱 APP 前端(`jiapuapp`) ## 一、交接结论 @@ -9,6 +9,8 @@ 不能把本轮结论简化成“所有接口均已联调通过”。世系首位成员创建目前由服务端返回业务 `code: 500`,导致依赖真实成员的详情、编辑与关系链路没有可用测试实体,仍是明确阻塞项。 +2026-07-26 的浏览器真实上传复测还确认了第二个合同阻塞:H5 已能从页面选择本地图片并完成 `init/chunk/complete` 上传,但 `complete.ossId` 是 19 位字符串,而照片/封面等业务 DTO 仍要求数值 `int64`。前端不能安全地把该值转成 JavaScript number,因此目前不会伪造照片创建成功。完整请求、响应和后端所需统一字段合同见 `docs/后端接口缺口与首位成员阻塞清单-2026-07-24.md` 的“八”。 + ## 二、本轮已验证的范围 ### 2.1 页面路由与接线 @@ -85,7 +87,7 @@ HTTP 返回成功,但业务响应为 `code: 500`,未创建成员;之后读 | F10 家族视频 | 明确提示暂未开放 | 仅见删除 operation,未见视频列表、详情、播放、发布等读取/写入合同。 | | R09 人生事件 | 明确提示暂未开放 | APP 目录中无独立人生事件资源。 | | N02 消息详情 | 明确提示暂未开放 | 只有通知列表和标已读,没有单条详情读取 owner。 | -| M03 安全概览 | 不展示假账号/设备摘要 | 无安全概览、设备、登录记录读取合同。 | +| M03 安全概览 | 已接真实 profile 只读概览 | 2026-07-26 实测 `GET /auth/profile` 返回 `phone`、`userNo`、`status`;页面仅展示掩码手机号、账号编号和状态。设备与登录记录仍无独立读取 operation,不展示或推断。 | | M05 换绑手机号 | 等待人工 TAC/短信验证 | 缺少可安全消费的当前手机号 DTO 与已核实验证码链路。 | | 删除、退出、改密、支付 | 未执行 | 本轮测试范围禁止敏感操作。 | diff --git a/docs/后端接口缺口与首位成员阻塞清单-2026-07-24.md b/docs/后端接口缺口与首位成员阻塞清单-2026-07-24.md new file mode 100644 index 0000000..c51a1d3 --- /dev/null +++ b/docs/后端接口缺口与首位成员阻塞清单-2026-07-24.md @@ -0,0 +1,535 @@ +# 后端接口缺口与首位成员阻塞清单 + +更新日期:2026-07-24 +适用范围:家谱 APP 前端(`jiapuapp`)与 APP API 合同维护 + +## 目的与边界 + +本文供后端确认接口 owner、DTO 和测试数据,不要求前端猜测字段或用本地数据绕过接口。下文的“需要”分为两种: + +- **已有接口但不可闭环**:路径和当前参数已存在,需修复服务端行为或补全可消费的 DTO。 +- **缺少业务 owner**:现有 APP OpenAPI 没有该页面所需的读取/写入 operation;需要后端和产品先确定资源归属,再发布合同。 + +除非下文明确写为“当前已声明字段”,字段名、枚举、分页形式和响应结构都由后端在 APP OpenAPI 中一次性定义;前端不会从其他模块、PC 接口或示例数据推断。 + +## 通用请求约束 + +当前已声明的受保护 APP 接口均需要当前测试账号的认证会话和 `clientid` 请求头。若请求体含 `clientId`,其值必须与该 Header 一致。本文不记录真实 token、手机号或生产数据。 + +后端交付一个新 operation 或 DTO 时,请同时在 APP OpenAPI 的 YAML/JSON 中补齐:路径、HTTP 方法、鉴权、所有 path/query/body 参数、成功响应、可预期的 4xx/5xx 业务码及字段必填性。仅有 HTTP 200 而业务 `code` 为失败,不能视为接口成功。 + +## 一、缺少业务 operation 的页面 + +### F10 家族视频:缺视频读取 owner + +现状:`pages/family/f10-video-list.vue` 已明确关闭。当前 OpenAPI 只声明: + +```text +DELETE /genealogy/app/genealogies/{genealogyId}/videos/{videoId} +``` + +没有视频列表、详情、播放地址或发布 operation。删除接口不能反推出读取和播放合同。 + +当前页面入口可提供的业务上下文只有: + +| 参数语义 | 来源 | 状态 | +| --- | --- | --- | +| `genealogyId` | 页面路由 | 已有,必需 | +| 当前登录账号 | 认证会话 | 已有,必需 | +| `clientid` | APP 请求头 | 已有,必需 | + +请后端/产品先确认视频资源是否归属某个家谱。若是,最小可用的首个 operation 应是“读取该家谱可见视频列表”,并明确: + +1. 是否分页;若分页,统一选择并声明 `pageNum/pageSize` 或 `cursor/pageSize` 之一,不能让前端猜测。 +2. 每条记录的稳定视频标识,以及当前页面用于渲染列表所需的明确字段和字段类型。 +3. 当前账号无权、资源不存在、空列表的响应语义。 + +视频详情、播放、发布、编辑、评论、点赞和分享目前没有对应页面表单或页面数据模型;若产品要开放这些能力,请为每项单独给出 operation 与 DTO,不要用相册或动态接口兼容代替。 + +### R09 人生大事:缺独立资源与归属定义 + +现状:`pages/records/r09-life-events.vue` 已明确关闭;APP/PC OpenAPI 中都未发现独立“人生事件/life event”资源。 + +从 R02 人物详情跳转 R09 时会传入 `genealogyId` 和 `personId`,但 R09 当前没有读取或写入表单,因此不能从 UI 推断事件到底是“人物专属”还是“家谱公共”。请后端/产品先在以下两种归属中选择一种: + +| 资源归属 | 最少识别参数 | +| --- | --- | +| 人物专属事件 | `genealogyId`、`personId` | +| 家谱公共事件 | `genealogyId` | + +归属确定后,请提供对应的列表/详情/创建或编辑 operation(以产品实际要开放的页面为准),并定义事件的必填业务字段、时间字段、排序/分页、权限和错误语义。归属未确定前,前端不会把成长记录、备忘或人物简介映射成人生大事。 + +### N02 消息详情:缺单条详情读取 operation + +现状:当前已声明: + +```text +GET /genealogy/app/notifications +POST /genealogy/app/notifications/{notificationId}/read +POST /genealogy/app/notifications/read-all +``` + +没有单条详情读取 owner,因此 `pages/notification/n02-message-detail.vue` 不展示 fixture 正文,也不从通知字段推断业务跳转。 + +建议后端补一个单条详情读取 operation;具体路径可由后端确定,但其最小输入和输出语义应为: + +| 位置 | 需要项 | +| --- | --- | +| path 参数 | `notificationId`;应与现有“标记已读”接口使用同一通知标识语义 | +| 请求上下文 | 当前登录账号、`clientid` | +| 响应必需内容 | 通知标题、完整纯文本正文、发布时间、已读状态,以及供服务端内部定位的稳定通知标识 | +| 错误语义 | 当前账号无权/通知不存在时应明确失败,不能返回其他账号的通知 | + +业务跳转不是本次最低需求。若后端未来要返回业务目标,需另行提供受权限约束的 `bizType → 前端路由键 + 必填参数 + 失效语义` 闭合字典;前端不会由 `bizId`、标题或正文猜路由。 + +## 二、已有路径但缺少可安全消费合同的页面 + +### M03 账号与安全:已解除(真实 profile 只读概览) + +2026-07-26 已用测试账号真实读取 `GET /genealogy/app/auth/profile`。响应含 `phone`、`userNo`、`status`;M03 已接入并只显示本地掩码手机号、账号编号和账号状态。页面不把明文手机号写入路由、日志或缓存。 + +“设备列表”“登录记录”“安全等级”仍没有独立读取 owner,也不属于当前 M03 的数据模型;页面不会展示或推断这些内容。建议后端仍在 Apifox 为 profile 发布明确 DTO;如产品要展示上述资源,应分别定义 operation、字段、权限和保留期。 + +### M05 换绑手机号:已有最终 PUT,但缺可验证的安全链路与读取 DTO + +当前 OpenAPI 已声明最终写入接口: + +```text +PUT /genealogy/app/auth/phone +``` + +当前已声明的请求参数如下;它们是现状记录,不是前端新增猜测: + +| 位置 | 字段 | 约束 | +| --- | --- | --- | +| Header | `clientid` | 必填 | +| Body | `clientId` | 必填,必须等于 Header `clientid` | +| Body | `phone` | 必填,新手机号 | +| Body | `smsCode` | 必填;当前导出合同为 4 位数字 | +| 请求上下文 | 当前登录账号 | 必填 | + +当前 OpenAPI 还声明通用发码接口: + +```text +POST /genealogy/app/auth/sms/code +``` + +其当前 body 必填字段为 `clientId`、`tenantId`、`grantType`、`sceneCode`、`phone`、`validToken`;换绑场景为 `APP_PHONE_CHANGE`。当前 profile 已能只读获取并掩码显示手机号,但没有经过人工确认的换绑专用 TAC/发码/校验闭环;M05 因而继续保持关闭,且不在无人值守时发送短信或换绑。 + +请后端确认并在同一版本交付: + +1. `APP_PHONE_CHANGE` 的 `validToken` 获取、绑定手机号主体校验、过期和一次性消费语义;如通用发码接口不适合作为换绑 owner,请提供专用 operation,而不是要求前端复用未知场景。 +2. 最终 PUT 成功、验证码错误/过期/重复、手机号已占用、无权限和结果未知时的明确 HTTP 与业务码语义。 + +本问题单只要求合同和人工可控的测试条件,不授权前端执行短信、换绑或其他账号安全写入。 + +## 三、已有接口但被服务端 `code:500` 阻断的成员页面 + +这不是“缺接口”,而是首位成员无法创建导致没有真实 `personId`。阻断请求为: + +```text +POST /genealogy/app/genealogies/{genealogyId}/lineage/persons +``` + +当前已验证的最小请求: + +| 位置 | 参数 | 值/约束 | +| --- | --- | --- | +| Path | `genealogyId` | 可访问的测试家谱 ID | +| Header | `clientid` | 必填 | +| 请求上下文 | 当前测试账号 | 必填 | +| Body | `name` | 当前 `LineagePersonBody` 唯一 required 字段;非空成员姓名 | +| Body | `generation` | 首位成员固定为整数 `1` | + +2026-07-26 又从 T04 页面填写了可安全收集的全量资料并真实提交:姓名、人物编号、示例字典值、排序、别名、字辈、生卒日期、生卒地、安葬地、简介、备注;HTTP `200`,业务仍返回 `code:500 / 发生未知异常,请联系管理员`。首位成员不应伪填 `fatherId`、`motherId`、`relationName`;`appUserId` 缺同类型候选映射,头像 `avatarOssId` 仍受 19 位 `ossId` 类型冲突阻断。`sex`、`birthLunar`、`deathLunar`、`personStatus` 虽为可选,但 Apifox 仅称项目字典值、无枚举,页面不能把猜测的代码或农历文本作为正式交互。 + +已复现结果:最小请求与上述全字段页面请求均为 HTTP 层成功、业务响应 `code:500`;随后读取世系树仍为空。 + +请后端处理: + +1. 查询该请求的异常栈,以及空世系创建首位成员所需的家谱/租户初始化条件。 +2. 修复后使该最小合法请求返回业务成功,并返回可读取的成员结果(至少可取得稳定 `personId`)。 +3. 以同一测试家谱验证后续读取:世系树、成员目录分页、成员详情均能读到该成员。 +4. 若暂不能修复,请提供当前测试账号有访问权限的测试 `genealogyId` 与真实 `personId`,供只读和允许写入的回读验证;不要提供生产数据。 + +解锁范围为 T03–T08、R01–R02:成员详情、首位成员/亲属创建、编辑、排行、成员目录与成员状态等。已有读取/写入路径应在取得真实 `personId` 后按页面顺序验证,不应改成 mock 或本地假成功。 + +## 四、后端回传模板 + +请每个问题按下列项目回复,便于前端不重做已接线部分: + +```text +页面/问题: +资源 owner: +APP operation(HTTP 方法 + 路径): +鉴权和 clientid 要求: +path/query/body 参数(字段、类型、必填、枚举/范围): +成功响应 DTO(字段、类型、必填): +错误响应(HTTP 状态 + 业务 code + 可展示文案): +分页/排序/权限语义(如适用): +可使用的测试 genealogyId / personId(仅测试数据): +OpenAPI YAML/JSON 更新版本: +``` + +前端收到更新后的合同和测试数据后,会先做最小浏览器真实请求,并以服务端成功响应后的列表/详情回读作为验收依据;样式问题另在模拟器复核。 + +## 五、A01 密码登录:已解除 + +2026-07-25 已在桌面版 Apifox 核对并用测试账号直连验证。最新已发布合同如下: + +| 位置 | 字段 | +| --- | --- | +| Header | `clientid` | +| Body | `grantType=password`、`tenantId`、`phone`、32 位 MD5 `password`;`validToken` 仅在验证中心策略开启时必传 | + +`clientId` 与 `sceneCode` 不再属于密码登录 body:后端根据 `POST /genealogy/app/auth/login` 绑定 `APP_PASSWORD_LOGIN` 场景。前端先查询 `GET /genealogy/app/auth/verification/password-login/require`;服务端返回 `required:false` 时不伪造滑块或票据,直接提交上述 body;返回 `required:true` 时才完成验证中心 challenge/verify 并提交真实 `validToken`。 + +实际结果:`password-login/require` 返回 `HTTP 200 / code:200 / required:false`;随后密码登录返回 `HTTP 200 / code:200`,响应含 `access_token`。本项不再是后端阻塞。 + +## 六、2026-07-25 Apifox 认证动作合同更新(已接入) + +Apifox 当前“家谱”项目新增并发布了下列 APP 认证动作接口;本地旧 `APP.openapi.json/yaml` 尚未同步这次导出,不能再作为认证链的 source of truth: + +```text +GET /genealogy/app/auth/verification/{operationCode}/require +POST /genealogy/app/auth/verification/{operationCode}/challenge +POST /genealogy/app/auth/verification/{operationCode}/verify +POST /genealogy/app/auth/sms/{operationCode}/code +``` + +`operationCode` 的已发布枚举为: + +```text +password-login | sms-login | register | forgot-password | phone-change | account-deactivate +``` + +合同要点: + +| 操作 | Header | path/query/body | +| --- | --- | --- | +| 查询策略 | `clientid` | path `operationCode`;query `tenantId`、`subject` | +| 获取挑战 | `clientid` | path `operationCode`;body `tenantId`、`subject` | +| 校验挑战 | `clientid` | path 必须与挑战使用同一 `operationCode`;body `tenantId`、`subject`、`challengeId` 和验证码证据 | +| 发送短信 | `clientid` | path `operationCode`;body `tenantId`、`phone`、`grantType=sms`;仅服务端策略开启时提交 `validToken` | + +所有新接口都明确禁止前端再提交 `clientId` 或 `sceneCode`;服务端按 APP 路由的当前激活绑定解析验证场景。 + +已做的最小真实验证(仅测试手机号、未发送短信): + +| 请求 | 结果 | +| --- | --- | +| `GET …/verification/APP_SMS_LOGIN/require` | `HTTP 200`,业务 `code:500`:不支持的认证业务动作 | +| `GET …/verification/sms-login/require` | `HTTP 200`,业务 `code:200`,`required:true`,`TIANAI/SLIDER` | +| `GET …/verification/password-login/require` | `HTTP 200`,业务 `code:200`,`required:false`,服务端绑定 `sceneCode: APP_LOGIN` | + +前端已将 A01/A04/A05 的验证、挑战、校验和发码链接到新路径;A01 密码登录使用 `password-login`,策略为 `required:false` 时直接调用最新密码登录 body;返回 `required:true` 时携带真实 `validToken`。短信、注册、找回仍只在服务端返回 `required:true` 后完成 TAC 并携带真实 `validToken`。 + +已用测试账号验证 A01 密码登录的真实成功响应;本轮未发送短信、注册、改密、换绑、退出或执行其他敏感操作。 + +## 七、2026-07-25 桌面 Apifox 复核:仍缺 owner 与分页响应异常 + +以下结论直接来自桌面版 Apifox 当前 APP 目录,前端不以其他资源替代: + +| 页面 | Apifox 当前 operation | 缺口 | +| --- | --- | --- | +| F10 家族视频 | 仅 `DELETE /genealogy/app/genealogies/{genealogyId}/videos/{videoId}` | 缺视频列表、详情、播放地址、发布/上传和编辑 owner | +| R09 人生大事 | “族务记录”目录仅有成长记录、备忘、亲友记录、功德记录四类 operation | 缺人生事件的列表、详情、创建、编辑、删除 owner | +| N02 消息详情 | 通知目录只有列表、单条标已读、未读数、全部标已读 | 缺单条消息详情读取 owner | + +另发现一个后端响应异常,不影响当前 F01(F01 使用非分页 `GET …/feeds`,已真实返回数组),但会使已发布分页 operation 无法使用: + +```text +GET /genealogy/app/genealogies/{genealogyId}/feeds/page?pageNum=1&pageSize=20 +``` + +桌面 Apifox 将成功响应声明为 `PageResult`;同一测试账号实际得到 `HTTP 200 / code:200`,却完全省略 `data`。客户端的分页合同需要 `data.rows`(数组)和 `data.total`(整数),因此当前会精确拒绝为分页响应无效,不能把空 envelope 当成空列表。 + +请后端: + +1. 为 F10、R09、N02 分别提供独立 APP operation 与完整 DTO;不要让前端复用相册、成长记录或通知列表字段。 +2. 修复 `feeds/page` 成功响应,使 `data.rows` 和 `data.total` 与 Apifox `PageResult` 一致;若该 operation 已废弃,请在 Apifox 明确下线,避免保留可调用但无结果的合同。 + +## 八、2026-07-26 真实 H5 图片上传:`ossId` 跨接口类型冲突 + +本项使用测试家谱的 F09「添加照片」页面、浏览器文件选择器和本地真实图片 +`static/assets/foundation/transparent/auth-login-outline.png` 验证;没有使用 mock、fixture 或手工伪造上传回执。 + +实际请求链路均返回 `HTTP 200 / code:200`: + +```text +POST /genealogy/app/files/resumable/init +POST /genealogy/app/files/resumable/chunk +POST /genealogy/app/files/resumable/complete +``` + +`complete` 的真实响应为(URL 已省略): + +```json +{ + "code": 200, + "data": { + "fileName": "auth-login-outline.png", + "ossId": "2081232520259612673" + } +} +``` + +该 `ossId` 是 19 位雪花标识,必须以字符串原样传递。它大于 JavaScript `Number.MAX_SAFE_INTEGER`;转换为 number 会丢失精度。当前照片创建 DTO 却把 `ossId` 声明为 `int64` 数值,F09 无法在不篡改标识的前提下提交:页面已经明确提示并停止在关联业务数据之前。 + +受影响的是所有“上传回执 → 业务表单”页面,而不只是 F09:动态配图、谱文封面、相册封面/照片、家谱封面、礼仪图片、备忘附件等。前端不会把字符串转为不安全 number,也不会提交截断后的 ID。 + +请后端在同一 APP OpenAPI 版本统一文件标识合同: + +| 位置 | 需要确定的合同 | +| --- | --- | +| 上传完成响应 | `ossId` 保持十进制字符串(现网已如此返回),并明确为稳定文件标识 | +| 所有消费该标识的 APP 请求 DTO | 对应字段统一接受同一十进制字符串;不要在 JSON 中要求前端传 `int64` 数值 | +| 响应 DTO 与字段示例 | 同步明确字符串类型、正整数词法约束和真实大于 `2^53-1` 的示例 | +| Apifox 发布内容 | 更新照片创建、封面、附件等所有相关 operation,避免上传接口和业务接口各自定义一套类型 | + +后端发布统一合同后,请提供仅测试数据可写入的家谱上下文。前端会重新从 F09 页面选择真实文件,完成“上传 → 创建照片 → 相册列表/详情回读”验收;在此之前不会猜测创建照片接口是否私下兼容字符串。 + +## 九、2026-07-26 Apifox 全量复核:贺礼邀约缺少受邀人候选来源 + +桌面 Apifox 当前已发布以下 4 个“贺礼邀约” operation: + +```text +PUT /genealogy/app/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitees +GET /genealogy/app/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitations +PUT /genealogy/app/genealogies/{genealogyId}/ceremonies/{ceremonyId}/invitations/me +GET /genealogy/app/genealogies/ceremony-invitations/mine +``` + +其中请求合同已明确,前端不会补猜字段: + +| operation | 必填参数 | 可选参数 | 备注 | +| --- | --- | --- | --- | +| 替换活动受邀人 | path `genealogyId`、`ceremonyId`;body `inviteeUserIds` | 无 | `inviteeUserIds` 是唯一业务用户 ID 的完整数组;空数组表示取消全部尚未响应的邀请 | +| 查询活动邀请名单 | path `genealogyId`、`ceremonyId` | 无 | 返回邀请记录列表 | +| 响应当前用户邀请 | path `genealogyId`、`ceremonyId`;body `inviteStatus` | 无 | 枚举仅允许 `ACCEPTED` 或 `DECLINED` | +| 查询我的活动邀请 | 无业务 path/body | 无 | 当前登录业务用户的有效邀请列表 | + +当前 `CeremonyInvitationVo` 可读取字段为:`invitationId`、`genealogyId`、`ceremonyId`、`inviteeUserId`、`inviteStatus`(`PENDING` / `ACCEPTED` / `DECLINED` / `CANCELED`)、`inviteVersion`、`deliveredTime`、`readTime`、`responseTime`、`ceremonyTitle`、`ceremonyTime`、`location`、`locationAddress`、`longitude`、`latitude`。这些 ID 均为 `int64`,真实雪花 ID 必须统一允许十进制字符串传递,不能要求 H5 转为不安全的 JavaScript number。 + +### 阻塞点 + +R07 需要的是 `inviteeUserIds`(业务用户 ID),但当前 APP 家谱成员列表没有声明可供选择的业务用户 ID,也没有“可受邀用户候选列表” operation。`memberId`、`lineagePersonId` 和 `inviteeUserId` 是不同资源标识,前端不会用任一个替代另一个,也不会提供让用户手输未知业务 ID 的伪流程。 + +请后端在同一版 APP OpenAPI 中二选一,并提供仅测试数据: + +1. 在现有“家谱成员列表/选项”成功 DTO 中明确返回稳定的 `appUserId`(或等价字段),并声明该字段就是 `inviteeUserIds` 的合法值;或 +2. 新增“查询活动可邀请用户候选列表” operation,至少返回业务用户 ID、展示名、是否可邀请、不可邀请原因。 + +同时请确认 `int64` 业务用户 ID 在所有 APP JSON 请求/响应中使用十进制字符串合同,避免与第八节 `ossId` 相同的精度丢失问题。候选来源和字符串 ID 合同发布前,前端只接入邀请读取/响应的精确 API 方法,不会把“替换受邀人”伪装成可用页面功能。 + +## 十、2026-07-26 Apifox 路径修正:行政区划 + +桌面 Apifox 当前正式路径为: + +```text +GET /genealogy/region/children?parentCode={parentCode} +GET /genealogy/region/path/{regionCode} +GET /genealogy/region/search?keyword={keyword}&level={level?}&limit={limit?} +GET /genealogy/region/{regionCode} +``` + +其中 `search.keyword` 必填;`search.level`、`search.limit` 可选;其余 `regionCode` path 参数必填,`children.parentCode` 可选。G03 原先错误使用了 `/genealogy/app/region/children`,已改为正式路径并在 H5 浏览器验证省、市、区三级均返回业务 `code:200`。其余 3 个 operation 尚无当前页面交互入口,待桌面 Apifox 补齐可消费的响应 DTO 后按实际页面需求接入;不会以 `additionalProperties` 的导出占位结构猜字段。 + +## 十一、2026-07-26 F03 真实点赞后缺少当前用户状态 + +测试账号通过 F03 动态详情页面执行了一次真实“点赞”(没有执行取消点赞或其他删除操作): + +```text +POST /genealogy/app/genealogies/{genealogyId}/feeds/{feedId}/likes +``` + +该请求返回 `HTTP 200`。随后页面重新读取详情,`likeCount` 从 `0` 变为 `1`,说明服务端已写入点赞;但详情响应没有给出可用的当前用户点赞状态,前端读取到的 `likedByMe` 仍为 `false`/缺失,按钮继续显示“点赞”而不是“取消点赞”。 + +这不是前端可以用本地布尔值补齐的缺口:页面刷新后必须以服务端状态决定下一次应该调用 `POST /likes` 还是 `DELETE /likes`。当前合同会导致重复点击语义不明确,也无法安全展示已点赞状态。 + +请后端在以下 APP 读取 DTO 中统一提供当前登录用户维度的必填布尔字段 `likedByMe`(或同义且明确的字段),并与点赞写入立即一致: + +```text +GET /genealogy/app/genealogies/{genealogyId}/feeds +GET /genealogy/app/genealogies/{genealogyId}/feeds/page +GET /genealogy/app/genealogies/{genealogyId}/feeds/{feedId} +``` + +字段必须是 `boolean`,不能以点赞总数、空值或仅他人点赞记录代替。后端发布后,前端会再从 F03 页面验证“点赞 → 详情回读为已点赞 → 仅在用户主动操作时取消点赞”的完整闭环。 + +前端已将缺失/非布尔值保留为“状态未知”,并在 F03 禁用点赞按钮、显示明确原因;不会再把它转换为 `false` 后重复发送点赞请求。 + +## 十二、2026-07-26 谱文分类列表缺少可消费的条目 DTO + +桌面 Apifox 的正式 operation 为: + +```text +GET /genealogy/app/genealogies/{genealogyId}/article-categories +``` + +请求仅要求 path `genealogyId`(以及认证 `clientid`),成功响应在当前桌面文档中标注为通用 `ListResult`,没有声明列表项字段模型、字段示例或 `categoryId` 的类型。F06 的创建谱文请求可选 `categoryId`,但前端不能在不知道“分类标识”和“分类展示名”字段的情况下把任意对象渲染成选择器。 + +请后端把该 operation 的 `data[]` 明确为具名 DTO,至少包含: + +| 字段 | 合同要求 | +| --- | --- | +| `categoryId` | 稳定分类标识;若为 int64,APP JSON 统一使用十进制字符串 | +| `categoryName` | 非空展示名称 | +| `status` / `enabled` | 明确是否允许在新建谱文中选择;若不需要则明确只返回可选项 | +| `sortOrder` | 可选的稳定排序字段;若服务端已排序则在文档明确 | + +在条目 DTO 发布前,F06 继续允许按现有创建合同不带 `categoryId` 发布,不会把导出文件里的通用对象或本地硬编码分类当作真实分类数据。 + +## 十三、当前全部已确认阻塞总表 + +本表是当前 APP 136 operation 审计的唯一汇总入口;详情、请求字段和回传模板见本文件对应章节,以及 `APP-136请求参数字段核对表-2026-07-26.md`。这里的“阻塞”不等于前端缺少调用代码:可能是 operation 根本不存在、响应 DTO 无法消费、服务端真实失败,或 H5 不存在安全参数来源。 + +| 编号 | 类别 | operation / 合同 | 影响页面 | 已验证事实 | 后端解除条件 | +| --- | --- | --- | --- | --- | --- | +| B01 | 缺 operation | 视频仅有 `DELETE /videos/{videoId}` | F10 | 无列表、详情、播放地址、创建、上传、编辑等 owner | 发布视频读取与发布完整 operation/DTO | +| B02 | 缺 operation | 人生大事无资源 owner | R09 | 族务目录只有成长、备忘、亲友、功德 | 明确人物专属或家谱公共归属后发布列表/详情/写入 DTO | +| B03 | 缺 operation | 通知仅有列表和标已读 | N02 | 无 `notificationId` 单条详情读取 | 发布单条详情读取及授权/错误语义 | +| B05 | 安全链路缺失 | `PUT /auth/phone` | M05 | 只有最终 PUT,缺经确认的当前账号读取、TAC/发码闭环 | 发布专用换绑验证合同与读 DTO;敏感写入仍需人工测试许可 | +| B06 | 服务端异常 | `POST /genealogies/{genealogyId}/lineage/persons` | T03–T08、R01–R02 | 2026-07-26 从 T04 页面分别以最小首位成员数据、以及包含编号/示例字典值/排序/生卒信息/简介备注的全字段资料真实提交,均为 HTTP `200` 但业务 `code:500 / 发生未知异常`,未生成本地或伪造 `personId`;同谱只读 `GET .../lineage/persons/options` 实测 `200 / data: []`,因此也没有可读取的真实人物候选 | 修复最小合法 body,返回并可读取真实 `personId` | +| B08 | ID 类型冲突 | 上传 complete 与所有消费 `ossId` 的 DTO | F02/F06/F07/F09/G03/G11/R04/R07/R08/R10/T04/M02 | 真实 complete 返回 19 位字符串,消费者声明 `int64` number | APP JSON 统一十进制字符串 `ossId`;同版更新示例与所有消费者 | +| B09 | 候选 ID 缺失 | `PUT .../ceremonies/{ceremonyId}/invitees` | R06/R07、我的邀请入口 | body 需要 `inviteeUserIds`,现有 member/person DTO 无同类型候选 | 公开 `appUserId` 对应关系或新增可邀请用户候选 operation | +| B10 | 当前用户状态缺失 | 动态列表/分页/详情 `likedByMe` | F03 | 实际点赞后数量变更,详情未给当前用户布尔状态 | 三个读取 DTO 均返回同步的 required boolean `likedByMe` | +| B11 | 条目 DTO 缺失 | `GET .../article-categories` | F06 | 只有通用 `ListResult`,无分类 ID/名称/状态/排序字段 | 发布 `categoryId`、`categoryName`、可选状态与排序 DTO | +| B12 | 页面交互待确认 | 行政区划 path/search/detail | G03/G11 后续回填、搜索 | 2026-07-26 复测已返回 `value`、`label`、`regionCode`、层级等真实条目;但当前产品页面没有搜索/路径回填入口 | 产品确认地区搜索与路径回填交互后接入页面;接口 DTO 不再阻塞 | +| B13 | 资源语义不能混用 | `members` 与 `lineage/persons` | 成员管理、R06/R07 | `memberId`、`personId`、`appUserId` 是不同资源,当前无独立成员管理页 | 提供成员管理页需求与稳定候选/映射 DTO,不得前端猜代 | +| B14 | 业务绑定 owner 缺失 | `POST /files/reference` | 所有附件业务 | 需要 `bizTable`、`bizId`、`bizField`,页面没有可信业务表/字段来源 | 后端为各业务资源声明文件绑定 owner,或明确由创建 DTO 原子绑定 | +| B15 | 枚举/候选缺失 | 多个 body 中的 `sex`、`birthLunar`、`deathLunar`、`personStatus`、`roleType`、`status`、`payType` 等 | T04/T05 及 T/M/G/R/F 写入扩展 | 2026-07-26 复核 `LineagePersonBody`:`birthLunar`、`deathLunar` 的含义是“日期是否农历”的项目字典值,不是农历日期文本;性别和人物状态也都是字典值。导出与桌面 Apifox 只给出示例 `0`,没有 value-label 枚举或字典 options operation,因此不能把输入框、农历日期文本或猜测的 `0/1` 映射留给用户 | 在 operation 详情给出完整 value-label enum,或发布字典 options operation;前端收到确定合同后统一改为选择器,并同步详情页的字典值展示 | +| B16 | 测试边界,不是接口缺失 | 短信、改密、换绑、注销、退出、删除、审核、支付 | A/M/G/F/R/N 全域 | 当前任务禁止执行这些敏感动作 | 仅在用户另行授权、测试账号与可回收数据齐备后执行真实写入 | +| B17 | 导出与实际校验冲突 | `POST /genealogy/app/files/resumable/init`(并影响 complete) | F02/F06/F07/F09/G03/G11/R04/R07/R08/R10/T04/M02 | 2026-07-26 真实 H5 上传:省略 `uploadId` 返回“上传ID不能为空”;以导出字段 `fileSize` 替代 `totalSize` 返回“文件大小不能为空”。实际可用请求为 `uploadId + totalSize`;秒传 `instant:true` 会合法返回 `uploadId:null` 与真实 `ossId` | 在桌面 Apifox 与可导出 OpenAPI 中统一请求/响应 DTO、required、示例及秒传分支;明确普通分片与秒传的 `uploadId` 可空规则 | + +## 十四、真实浏览器请求与返回记录(供后端排查) + +说明:以下均在测试账号、测试家谱中从 APP 页面发起;未记录账号密码、令牌或完整 `clientid`。所有请求均已携带运行时 `clientid`、测试账号登录态和 `genealogyId=2081191846772518914`。 + +### 1. B06:首位成员创建 + +接口: + +```text +POST /genealogy/app/genealogies/2081191846772518914/lineage/persons +``` + +最小请求(2026-07-26,页面 T04): + +```json +{ + "name": "首位成员最小请求验证", + "generation": 1 +} +``` + +实际返回: + +```text +HTTP 200 +{"code":500,"msg":"发生未知异常,请联系管理员","data":null} +``` + +页面结果:`保存失败 / 首位成员尚未保存 / 发生未知异常,请联系管理员`。 + +后端称已修复后的复测(2026-07-26,重新登录测试账号后从 T04 提交): + +```json +{ + "name": "后端修复验证首位成员", + "generation": 1 +} +``` + +返回仍为: + +```text +HTTP 200 +{"code":500,"msg":"发生未知异常,请联系管理员","data":null} +``` + +因此 B06 仍未解除;本次不是会话过期导致的结论。首次请求得到 `code:401` 后已通过 A01 真实密码登录,重试才得到上述业务 `code:500`。 + +全字段诊断请求(同一页面、同一测试家谱;用于验证可选字段是否会改变异常,不是字段字典的最终验收): + +```json +{ + "name": "LineageFullBody0726", + "generation": 1, + "personNo": "P20260726001", + "sex": "0", + "generationName": "FullGen", + "aliasName": "FullBodyAlias", + "birthLunar": "1900-01-01", + "birthPlace": "TestBirthPlace", + "deathLunar": "2000-01-01", + "deathPlace": "TestDeathPlace", + "burialPlace": "TestBurialPlace", + "biography": "Full body browser submission verification", + "remark": "test only", + "personStatus": "0", + "birthDate": "2026-07-26", + "deathDate": "2026-07-26", + "sortOrder": 1 +} +``` + +实际返回同样为: + +```text +HTTP 200 +{"code":500,"msg":"发生未知异常,请联系管理员","data":null} +``` + +特别说明:该诊断请求暴露出旧页面把 `birthLunar`、`deathLunar` 当作农历日期文本;Apifox 的真实语义是“是否农历”的字典值。不能把这一请求视为正确字典值示例,也不能要求前端据此猜选项。首位成员不应伪填 `fatherId`、`motherId`、`relationName`;`appUserId` 没有同类型候选,`avatarOssId` 被 B08 阻断。 + +随后读取: + +```text +GET /genealogy/app/genealogies/2081191846772518914/lineage/persons/options +HTTP 200 / code:200 / data: [] +``` + +因此 T01 没有成员数据并非前端空白占位:服务端尚未成功写入任何真实成员。 + +### 2. 初次验证记录(历史结果;以 2.1 的复测结论为准) + +| 编号 | 接口与页面 | 实际请求值/返回值 | 结论 | +| --- | --- | --- | --- | +| B07 | `GET /genealogy/app/genealogies/{genealogyId}/feeds/page`,F01/F03 | 已按页面分页参数真实读取;HTTP `200`、业务 `code:200`,但响应没有 Apifox `PageResult` 所需的 `data.rows`、`data.total` | 无法安全分页或把空对象伪装为无动态 | +| B08 | `POST /genealogy/app/files/resumable/init`,F09 等上传页面 | 省略 `uploadId`:返回“上传ID不能为空”;将 `totalSize` 改为导出字段 `fileSize`:返回“文件大小不能为空”;实际可用字段为 `uploadId`、`fileName`、`fileMd5`、`totalSize`、`totalChunks`、`chunkSize`。秒传返回 `{"uploadId":null,"instant":true,"ossId":"2081232520259612673"}` | 文件服务返回 19 位字符串 ID,而业务 DTO 写 `int64` 数字,前端不能无损作为 JSON 数字提交 | +| B10 | 动态点赞,F03 | 已真实点赞,点赞数量变化;详情/列表未返回 `likedByMe` | 不能据数量推断当前用户是否已点赞,页面已禁用不确定状态下的再次写入 | +| B11 | `GET /genealogy/app/genealogies/{genealogyId}/article-categories`,F06 | HTTP `200`、业务 `code:200`、`data: []` | 无分类候选可供选择;同时合同没有分类条目字段定义 | +| B12 | 行政区划 children,G03/G11 | `GET /genealogy/region/children` 已可真实读取;path/search/detail 没有可消费的条目 DTO 和页面交互归属 | 不猜地区回填字段或新增搜索行为 | +| B17 | 分片上传初始化,F09 等 | 见 B08 的实际请求和返回;导出与服务端校验字段冲突 | Apifox 与导出必须先统一为唯一合同 | + +### 2.1 后端称修复后的复测结果(2026-07-26) + +| 编号 | 复测结果 | 当前结论 | +| --- | --- | --- | +| B06 | 重新登录后,T04 最小 body 仍返回 HTTP `200`、业务 `code:500`、`data:null` | 未修复 | +| B07 | `GET .../feeds/page?pageNum=1&pageSize=20` 现在返回根级 `total:1` 和 `rows:[...]`,不再是空 envelope | 分页数据已出现;但 `likedByMe` 返回字符串 `"1"`,B10 仍未修复 | +| B08 | F09 从真实图片文件上传到“已上传”成功;保存照片时前端收到真实 19 位文件 ID 后仍报“超出 APP 安全整数范围”,未发出照片创建请求 | 未修复,需统一字符串 `ossId` 合同 | +| B09 | 礼仪列表可读取,`GET .../ceremonies/{ceremonyId}/invitations` 与 `GET .../ceremony-invitations/mine` 均返回 `data:[]`;没有可提交的 `inviteeUserIds` 候选 | 未修复,仍缺 `appUserId` 候选/映射 | +| B11 | 谱文分类仍返回 `HTTP 200 / code:200 / data:[]` | 测试谱没有分类;仍需分类 DTO 与可用测试候选才能验证选择/写入 | +| B12 | `GET /genealogy/region/path/110101`、`GET /genealogy/region/110101`、`GET /genealogy/region/search?keyword=北京&limit=10` 均返回包含 `value`、`label`、`regionCode`、层级等字段的真实数据 | 后端已补齐可消费条目;待产品确认搜索/回填交互后接入页面 | + +### 3. 未发起写入请求的原因(不是漏测) + +| 编号 | 接口/功能 | 未发起的原因 | 后端需要提供 | +| --- | --- | --- | --- | +| B01 | 视频 F10 | 仅发布删除接口;没有列表、详情、播放、创建、编辑或上传 owner | 完整资源 operation 与 DTO | +| B02 | 人生大事 R09 | 没有资源 operation | 明确人物或家谱归属后的列表、详情、写入 DTO | +| B03 | 通知详情 N02 | 只有列表、标已读;没有按 `notificationId` 读取详情 | 单条详情接口及授权语义 | +| B05/B16 | 短信、改密、换绑、退出、注销、删除、审核、支付 | 属于本轮明确禁止的敏感动作 | 另行授权、测试账号和可回收数据后再测 | +| B09/B13 | 礼仪受邀人、成员绑定 | 请求需要 `appUserId`,现有 `memberId`、`personId`、`appUserId` 不能互相替代;没有候选映射 | 同类型候选接口或稳定映射 DTO | +| B14 | 文件业务引用 | 请求需要 `bizTable`、`bizId`、`bizField`,没有页面可信 owner | 各资源的文件绑定归属合同 | +| B15 | 性别、人物状态、出生/逝世是否农历等 | 需要选择器,但 Apifox 仅给示例 `0`,没有枚举、中文标签或字典 options 接口 | value-label 枚举或字典 options operation | + + +### 不应误标为阻塞的已完成项 + +- A01 密码登录、G03 创建家谱、家族动态/评论、谱文、相册、亲友往来、礼仪、成长记录、备忘、功德均已做过真实创建后的列表或详情回读。 +- M03 账号与安全已真实读取当前 profile,并仅展示掩码手机号、账号编号和账号状态;设备与登录记录不在当前页面范围内。 +- 4 条行政区划 operation 都已有 API owner;`children` 已在 G03/G11 页面真实读取,其余三条的阻塞是没有可安全消费 DTO/产品交互,不是路径不存在。 +- 136 条 `method + path` 均可在桌面 Apifox 当前目录找到;数量差异不是“导出漏了 path”,而是页面数、功能入口、DTO 和安全参数来源不同。 diff --git a/index.html b/index.html index 2483001..925db73 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + diff --git a/pages/auth/a01-entry.vue b/pages/auth/a01-entry.vue index 3b86db0..0a8bd7f 100644 --- a/pages/auth/a01-entry.vue +++ b/pages/auth/a01-entry.vue @@ -234,7 +234,7 @@ import { isRequestCancelled, } from "@/utils/api.js"; import { - AUTH_TAC_SCENE, + AUTH_VERIFICATION_OPERATION, createTacRenderContext, isAuthPhone, isSmsDeliveryOutcomeUnknown, @@ -276,7 +276,7 @@ const authenticationNavigationFailure = "登录已完成,但暂时无法进入家谱,请再次点击进入"; const authRequestController = createRequestController(); const smsCooldown = createAuthSmsCooldown({ - sceneCode: AUTH_TAC_SCENE.SMS_LOGIN, + operationCode: AUTH_VERIFICATION_OPERATION.SMS_LOGIN, onChange: (seconds) => { cooldownSeconds.value = seconds; }, @@ -407,22 +407,33 @@ const prepareGetCode = async () => { if (!validatePhone() || !requireAgreement()) return; sendingCode.value = true; try { - const sceneCode = AUTH_TAC_SCENE.SMS_LOGIN; + const operationCode = AUTH_VERIFICATION_OPERATION.SMS_LOGIN; const requestedPhone = phone.value; const response = await appApi.getCaptchaRequirement( - { sceneCode, subject: requestedPhone }, + { operationCode, subject: requestedPhone }, { requestController: authRequestController }, ); if (!pageActive) return; if (phone.value !== requestedPhone) throw new Error("手机号已变化,请重新获取验证码"); - const requirement = normalizeCaptchaRequirement(response, sceneCode); + const requirement = normalizeCaptchaRequirement(response); + if (!requirement.required) { + await appApi.sendSmsCode( + { operationCode, phone: requestedPhone }, + { requestController: authRequestController }, + ); + if (!pageActive) return; + sentPhone.value = requestedPhone; + smsCooldown.start(); + showFeedback("验证码已发送"); + return; + } tacSequence += 1; tacContext.value = createTacRenderContext({ requestId: `a01-sms-${tacSequence}`, baseUrl: runtimeConfig.baseUrl, clientId: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, - sceneCode, + operationCode, subject: requestedPhone, requirement, }); @@ -445,25 +456,33 @@ const preparePasswordLogin = async () => { const passwordHash = calcMD5(password.value); submitting.value = true; try { - // 产品规则:密码登录也必须先完成滑动验证。当前登录接口不消费 - // validToken,因此票据只作为本次前端验证成功的完成信号,不写入登录体。 - const sceneCode = AUTH_TAC_SCENE.SMS_LOGIN; + const operationCode = AUTH_VERIFICATION_OPERATION.PASSWORD_LOGIN; const response = await appApi.getCaptchaRequirement( - { sceneCode, subject: requestedPhone }, + { operationCode, subject: requestedPhone }, { requestController: authRequestController }, ); if (!pageActive) return; if (phone.value !== requestedPhone) { throw new Error("手机号已变化,请重新登录"); } - const requirement = normalizeCaptchaRequirement(response, sceneCode); + const requirement = normalizeCaptchaRequirement(response); + if (!requirement.required) { + await appApi.loginWithPassword( + { phone: requestedPhone, passwordHash }, + { requestController: authRequestController }, + ); + if (!pageActive) return; + authenticationCommitted.value = true; + await enterAuthenticatedRoot(); + return; + } tacSequence += 1; tacContext.value = createTacRenderContext({ requestId: `a01-password-${tacSequence}`, baseUrl: runtimeConfig.baseUrl, clientId: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, - sceneCode, + operationCode, subject: requestedPhone, requirement, }); @@ -504,6 +523,7 @@ const completeTac = async (result) => { { phone: action.phone, passwordHash: action.passwordHash, + validToken: ticket.validToken, }, { requestController: authRequestController }, ); @@ -521,7 +541,7 @@ const completeTac = async (result) => { sendingCode.value = true; await appApi.sendSmsCode( { - sceneCode: AUTH_TAC_SCENE.SMS_LOGIN, + operationCode: AUTH_VERIFICATION_OPERATION.SMS_LOGIN, phone: expectedContext.subject, validToken: ticket.validToken, }, diff --git a/pages/auth/a04-register.vue b/pages/auth/a04-register.vue index 9ac4a5b..6dcca2a 100644 --- a/pages/auth/a04-register.vue +++ b/pages/auth/a04-register.vue @@ -263,7 +263,7 @@ import { isRequestCancelled, } from "@/utils/api.js"; import { - AUTH_TAC_SCENE, + AUTH_VERIFICATION_OPERATION, createTacRenderContext, isAuthPhone, isSmsDeliveryOutcomeUnknown, @@ -321,7 +321,7 @@ const registrationNavigationFailure = "注册已完成,但暂时无法进入家谱,请再次点击进入"; const authRequestController = createRequestController(); const smsCooldown = createAuthSmsCooldown({ - sceneCode: AUTH_TAC_SCENE.REGISTER, + operationCode: AUTH_VERIFICATION_OPERATION.REGISTER, onChange: (seconds) => { cooldownSeconds.value = seconds; }, @@ -428,22 +428,33 @@ const prepareGetCode = async () => { } sendingCode.value = true; try { - const sceneCode = AUTH_TAC_SCENE.REGISTER; + const operationCode = AUTH_VERIFICATION_OPERATION.REGISTER; const requestedPhone = phone.value; const response = await appApi.getCaptchaRequirement( - { sceneCode, subject: requestedPhone }, + { operationCode, subject: requestedPhone }, { requestController: authRequestController }, ); if (!pageActive) return; if (phone.value !== requestedPhone) throw new Error("手机号已变化,请重新获取验证码"); - const requirement = normalizeCaptchaRequirement(response, sceneCode); + const requirement = normalizeCaptchaRequirement(response); + if (!requirement.required) { + await appApi.sendSmsCode( + { operationCode, phone: requestedPhone }, + { requestController: authRequestController }, + ); + if (!pageActive) return; + sentPhone.value = requestedPhone; + smsCooldown.start(); + showFeedback("验证码已发送"); + return; + } tacSequence += 1; tacContext.value = createTacRenderContext({ requestId: `a04-register-${tacSequence}`, baseUrl: runtimeConfig.baseUrl, clientId: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, - sceneCode, + operationCode, subject: requestedPhone, requirement, }); @@ -467,7 +478,7 @@ const completeTac = async (result) => { sendingCode.value = true; await appApi.sendSmsCode( { - sceneCode: AUTH_TAC_SCENE.REGISTER, + operationCode: AUTH_VERIFICATION_OPERATION.REGISTER, phone: expectedContext.subject, validToken: ticket.validToken, }, diff --git a/pages/auth/a05-reset-password.vue b/pages/auth/a05-reset-password.vue index a7ad81f..0ee38e3 100644 --- a/pages/auth/a05-reset-password.vue +++ b/pages/auth/a05-reset-password.vue @@ -219,7 +219,7 @@ import { isRequestCancelled, } from "@/utils/api.js"; import { - AUTH_TAC_SCENE, + AUTH_VERIFICATION_OPERATION, createTacRenderContext, isAuthPhone, isSmsDeliveryOutcomeUnknown, @@ -274,7 +274,7 @@ let tacSequence = 0; let pageActive = true; const authRequestController = createRequestController(); const smsCooldown = createAuthSmsCooldown({ - sceneCode: AUTH_TAC_SCENE.FORGOT_PASSWORD, + operationCode: AUTH_VERIFICATION_OPERATION.FORGOT_PASSWORD, onChange: (seconds) => { cooldownSeconds.value = seconds; }, @@ -356,22 +356,33 @@ const prepareGetCode = async () => { fieldErrors.value.phone = ""; sendingCode.value = true; try { - const sceneCode = AUTH_TAC_SCENE.FORGOT_PASSWORD; + const operationCode = AUTH_VERIFICATION_OPERATION.FORGOT_PASSWORD; const requestedPhone = phone.value; const response = await appApi.getCaptchaRequirement( - { sceneCode, subject: requestedPhone }, + { operationCode, subject: requestedPhone }, { requestController: authRequestController }, ); if (!pageActive) return; if (phone.value !== requestedPhone) throw new Error("手机号已变化,请重新获取验证码"); - const requirement = normalizeCaptchaRequirement(response, sceneCode); + const requirement = normalizeCaptchaRequirement(response); + if (!requirement.required) { + await appApi.sendSmsCode( + { operationCode, phone: requestedPhone }, + { requestController: authRequestController }, + ); + if (!pageActive) return; + sentPhone.value = requestedPhone; + smsCooldown.start(); + showFeedback("验证码已发送"); + return; + } tacSequence += 1; tacContext.value = createTacRenderContext({ requestId: `a05-forgot-${tacSequence}`, baseUrl: runtimeConfig.baseUrl, clientId: runtimeConfig.clientId, tenantId: runtimeConfig.tenantId, - sceneCode, + operationCode, subject: requestedPhone, requirement, }); @@ -395,7 +406,7 @@ const completeTac = async (result) => { sendingCode.value = true; await appApi.sendSmsCode( { - sceneCode: AUTH_TAC_SCENE.FORGOT_PASSWORD, + operationCode: AUTH_VERIFICATION_OPERATION.FORGOT_PASSWORD, phone: expectedContext.subject, validToken: ticket.validToken, }, diff --git a/pages/family/f02-publish-feed.vue b/pages/family/f02-publish-feed.vue index 7dba84f..00e3e65 100644 --- a/pages/family/f02-publish-feed.vue +++ b/pages/family/f02-publish-feed.vue @@ -14,7 +14,10 @@ 动态类型 - + + 文字动态 + 当前仅支持此类型 + @@ -65,7 +68,7 @@ import { isImagePickCancelled, pickAndUploadImage } from "@/utils/resumable-imag import { goBack, handleBackPress, returnTo, runBackGuard } from "@/utils/navigation.js"; const genealogyId = ref(""); -const form = reactive({ feedContent: "", feedType: "", sortOrder: "" }); +const form = reactive({ feedContent: "", feedType: "text", sortOrder: "" }); const mediaReceipts = ref([]); const publishState = ref("form"); const isSubmitting = ref(false); @@ -76,7 +79,7 @@ const discardVisible = ref(false); const requestController = createRequestController(); const mediaOssIds = computed(() => mediaReceipts.value.map((item) => item.ossId).join(",")); const isDirty = computed(() => Boolean( - form.feedContent.trim() || form.feedType.trim() || form.sortOrder.trim() || mediaReceipts.value.length, + form.feedContent.trim() || form.sortOrder.trim() || mediaReceipts.value.length, )); const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value)); const resultCopy = computed(() => ({ @@ -139,7 +142,7 @@ const submit = async () => { mediaOssIds: mediaOssIds.value, sortOrder: form.sortOrder, }, { requestController }); - Object.assign(form, { feedContent: "", feedType: "", sortOrder: "" }); + Object.assign(form, { feedContent: "", feedType: "text", sortOrder: "" }); mediaReceipts.value = []; publishState.value = "success"; } catch (error) { @@ -191,10 +194,13 @@ onUnmounted(() => { .required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; line-height: 1; transform: translateY(-3rpx); } .publish-field input, .publish-field textarea { display: block; box-sizing: border-box; width: 100%; margin-top: 12rpx; color: $ink; font-size: 24rpx; } .publish-field input { min-height: 64rpx; } +.publish-field__value--readonly { display:flex; min-height:76rpx; align-items:center; justify-content:space-between; margin-top:12rpx; padding:0 20rpx; border:1rpx solid rgba(128,89,49,.22); border-radius:10rpx; background:rgba(255,252,245,.48); } +.publish-field__value--readonly text:first-child { color:$ink; font-size:24rpx; } +.publish-field__value--readonly text:last-child { color:$ink-muted; font-size:20rpx; } .publish-field--content textarea { min-height: 200rpx; line-height: 1.7; } .publish-field--media { display: grid; gap: 12rpx; } .publish-field__hint { display: block; margin-top: 6rpx; color: $ink-muted; font-size: 20rpx; line-height: 1.45; } -.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; } +.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; } .upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; } .publish-placeholder { color: #8e806e; } .publish-error { margin-top: 12rpx; color: $brand-red; font-size: 22rpx; line-height: 1.5; } diff --git a/pages/family/f03-feed-detail.vue b/pages/family/f03-feed-detail.vue index 57019f9..f3b56ca 100644 --- a/pages/family/f03-feed-detail.vue +++ b/pages/family/f03-feed-detail.vue @@ -18,6 +18,15 @@ 发布:{{ feed.publisher }} {{ feed.likeCount }} 次点赞 · {{ feed.commentCount }} 条评论 + + + @@ -82,6 +91,8 @@ const comments = ref([]); const commentDraft = ref(""); const commentError = ref(""); const isSubmittingComment = ref(false); +const isTogglingLike = ref(false); +const likeError = ref(""); const controller = createRequestController(); let pageActive = true; const feedTypeLabel = (type) => String(type || "").trim().toLowerCase() === "text" ? "文字动态" : "家族动态"; @@ -89,6 +100,7 @@ const feedTypeLabel = (type) => String(type || "").trim().toLowerCase() === "tex const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(feedId.value), ); +const hasLikeState = computed(() => typeof feed.value?.likedByMe === "boolean"); const stateCopy = computed(() => { if (!hasValidContext.value) { return { @@ -151,6 +163,23 @@ const refresh = async () => { if (feedState.value === "ready") await loadComments(); }; +const toggleLike = async () => { + if (isTogglingLike.value || !hasValidContext.value || !feed.value || !hasLikeState.value) return; + const nextLiked = !feed.value.likedByMe; + isTogglingLike.value = true; + likeError.value = ""; + try { + await appApi.setFeedLike(genealogyId.value, feedId.value, nextLiked, { requestController: controller }); + if (!pageActive) return; + await loadFeed(); + } catch (error) { + if (!pageActive || isRequestCancelled(error)) return; + likeError.value = error?.message || "点赞操作失败,请稍后重试"; + } finally { + if (pageActive) isTogglingLike.value = false; + } +}; + const submitComment = async () => { if (isSubmittingComment.value || !hasValidContext.value) return; const commentContent = commentDraft.value.trim(); @@ -208,6 +237,10 @@ onBackPress((event) => handleBackPress(event, requestBack)); .feed-card__heading text:last-child, .comment-card__heading text:last-child { color: $ink-muted; font-size: 21rpx; text-align: right; } .feed-card__content { display: block; margin-top: 20rpx; color: $ink; font-size: 29rpx; line-height: 1.7; white-space: pre-wrap; } .feed-card__meta { margin-top: 22rpx; color: $ink-muted; font-size: 21rpx; } +.feed-card .app-button { margin-top: 18rpx; } +.like-note, .like-error { display: block; margin-top: 10rpx; font-size: 22rpx; } +.like-note { color: $ink-muted; } +.like-error { color: $brand-red; } .comment-section { margin-top: 18rpx; padding: 28rpx; } .section-title { display: block; color: $ink; font-family: STKaiti, KaiTi, serif; font-size: 32rpx; font-weight: 700; } .comment-list { margin-top: 18rpx; } diff --git a/pages/family/f06-article-editor.vue b/pages/family/f06-article-editor.vue index d56f75a..c98d0b5 100644 --- a/pages/family/f06-article-editor.vue +++ b/pages/family/f06-article-editor.vue @@ -208,7 +208,7 @@ onUnmounted(() => { .editor-field--cover { display: grid; gap: 12rpx; padding: 18rpx 22rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); } .editor-field--cover .editor-field__label { margin: 0; } .editor-field__hint { display: block; color: $ink-muted; font-size: 20rpx; line-height: 1.45; } -.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; } +.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; } .upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; } .editor-placeholder { color: #9e8e79; } .editor-save-error { display: block; margin: 12rpx 8rpx 0; color: $brand-red; font-size: 24rpx; line-height: 34rpx; text-align: center; } diff --git a/pages/family/f07-album-list.vue b/pages/family/f07-album-list.vue index f0a1867..b0b4e54 100644 --- a/pages/family/f07-album-list.vue +++ b/pages/family/f07-album-list.vue @@ -101,5 +101,5 @@ onUnmounted(() => { active = false; controller.abort(); }); diff --git a/pages/family/f09-media-upload.vue b/pages/family/f09-media-upload.vue index 3bfea57..744a43f 100644 --- a/pages/family/f09-media-upload.vue +++ b/pages/family/f09-media-upload.vue @@ -27,9 +27,17 @@ 拍摄人 - + + 拍摄日期 + + {{ form.shootDate || '请选择' }} + + + 拍摄时间 - + + {{ form.shootClock || '请选择' }} + 排序值 @@ -79,13 +87,14 @@ const uploadError = ref(""); const submitError = ref(""); const uploading = ref(false); const submitting = ref(false); -const form = reactive({ photoTitle: "", photoDesc: "", photographer: "", shootTime: "", sortOrder: "" }); +const form = reactive({ photoTitle: "", photoDesc: "", photographer: "", shootDate: "", shootClock: "", sortOrder: "" }); const controller = createRequestController(); const discardVisible = ref(false); const confirmation = createDiscardConfirmation((visible) => { discardVisible.value = visible; }); const hasValidContext = computed(() => /^[1-9]\d*$/.test(genealogyId.value) && /^[1-9]\d*$/.test(albumId.value)); -const isDirty = computed(() => receipt.value || Object.values(form).some((value) => value.trim())); +const isDirty = computed(() => Boolean(receipt.value) || Object.values(form).some((value) => value.trim())); +const shootTime = computed(() => form.shootDate ? `${form.shootDate} ${form.shootClock || "00:00"}:00` : ""); const stateCopy = computed(() => pageState.value === "success" ? { title: "照片已提交服务端", copy: "服务端已返回成功结果。", action: "返回相册" } : { title: "照片入口无效", copy: "没有取得有效家谱或相册标识。", action: "返回上一页" }); @@ -110,6 +119,8 @@ const selectPhoto = async () => { uploading.value = false; } }; +const selectShootDate = (event) => { form.shootDate = event.detail.value || ""; submitError.value = ""; }; +const selectShootClock = (event) => { form.shootClock = event.detail.value || ""; submitError.value = ""; }; const submitPhoto = async () => { if (uploading.value || submitting.value || !hasValidContext.value) return; @@ -120,8 +131,10 @@ const submitPhoto = async () => { submitting.value = true; submitError.value = ""; try { + const { shootDate, shootClock, ...photoForm } = form; await appApi.createAlbumPhoto(genealogyId.value, albumId.value, { - ...form, + ...photoForm, + shootTime: shootTime.value, ossId: toConsumerOssId(receipt.value.ossId), }, { requestController: controller }); pageState.value = "success"; @@ -167,11 +180,13 @@ onUnmounted(() => { .required-mark { display: inline-block; margin-right: 4rpx; color: $brand-red; font-size: 26rpx; line-height: 1; transform: translateY(-3rpx); } .media-field input, .media-field textarea { @include adaptive.adaptive-family-field; box-sizing: border-box; display: block; width: 100%; color: $ink; font-size: 24rpx; } .media-field input { min-height: 78rpx; padding: 0 22rpx; } +.media-field--picker picker { display: block; min-height: 78rpx; } +.media-field--picker picker > view { display: flex; min-height: 78rpx; align-items: center; padding: 0 22rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); color: $ink; font-size: 24rpx; } .media-field textarea { min-height: 116rpx; padding: 18rpx 22rpx; line-height: 1.55; } .placeholder { color: #9e8e79; } .media-field--upload { display: grid; gap: 12rpx; padding: 18rpx 20rpx; border: 1rpx solid rgba(128, 89, 49, .34); border-radius: 12rpx; background: rgba(255, 252, 245, .7); } .media-field--upload .media-field__label { margin: 0; } -.upload-button { justify-self: start; min-height: 60rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; } +.upload-button { justify-self: start; min-height: 88rpx; margin: 0; padding: 0 20rpx; border: 1rpx solid rgba(159, 23, 15, .42); border-radius: 8rpx; background: transparent; color: $brand-red; font-size: 22rpx; } .upload-receipt { color: $ink-muted; font-size: 21rpx; overflow-wrap: anywhere; } .field-error { margin-top: 10rpx; color: $brand-red; font-size: 22rpx; line-height: 1.45; } .media-panel .app-button { margin-top: 28rpx; } diff --git a/pages/family/f10-video-list.vue b/pages/family/f10-video-list.vue index 09e0082..2547b06 100644 --- a/pages/family/f10-video-list.vue +++ b/pages/family/f10-video-list.vue @@ -1,6 +1,6 @@ diff --git a/pages/genealogy/g05-genealogy-overview.vue b/pages/genealogy/g05-genealogy-overview.vue index 46eea80..4308359 100644 --- a/pages/genealogy/g05-genealogy-overview.vue +++ b/pages/genealogy/g05-genealogy-overview.vue @@ -381,10 +381,10 @@ const applyToJoin = () => { } .overview-ready { display: flex; - min-height: min(483px, calc(100vw * 1.3422)); + min-height: min(540px, calc(100vw * 1.5)); flex-direction: column; gap: 34rpx; - padding: 58rpx 0 42rpx; + padding: 10rpx 0 42rpx; box-sizing: border-box; } .overview-hero { @@ -410,14 +410,21 @@ const applyToJoin = () => { margin-top: 6rpx; color: #e9dcc1; font-size: 23rpx; + line-height: 1.55; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } .overview-hero__stats { - display: flex; + display: inline-flex; gap: 18rpx; margin-top: 12rpx; color: #dbc990; font-size: 22rpx; + transform: translateY(-18rpx); } +.overview-hero__stats + .overview-hero__stats { margin-left: 18rpx; } .overview-actions { display: grid; min-height: 386rpx; @@ -543,7 +550,7 @@ const applyToJoin = () => { } .overview-public { display: flex; - min-height: min(483px, calc(100vw * 1.3422)); + min-height: min(540px, calc(100vw * 1.5)); flex-direction: column; gap: 0; padding: 58rpx 0 28rpx; diff --git a/pages/genealogy/g12-generation-poems.vue b/pages/genealogy/g12-generation-poems.vue index 662ac3d..598c1a5 100644 --- a/pages/genealogy/g12-generation-poems.vue +++ b/pages/genealogy/g12-generation-poems.vue @@ -7,12 +7,12 @@ - + {{ genealogyName }} · 传承字序 {{ generationRangeTitle }} - 接口未声明“当前世代”字段,页面只按后端返回的世代、文字和状态展示。 + 按已记录的世代和字辈展示传承字序。 第 {{ item.generationNo }} 世 @@ -136,8 +136,8 @@ const previewSummary = computed(() => { return `服务端预览:新增 ${preview.value.createCount} 条,更新 ${preview.value.updateCount} 条,保留 ${preview.value.keepCount} 条,停用 ${preview.value.disableCount} 条。`; }); const stateCopy = computed(() => poemState.value === "empty" - ? "正常字辈读取未返回条目。维护列表由后端单独鉴权,点击后才会判断是否可维护。" - : "未取得可消费的服务端响应,页面不会用本地字辈或本地权限替代。", + ? "当前尚未添加可用的字辈,可进入维护页面查看。" + : "暂未读取到可展示的字辈,请稍后重试。", ); const isDirty = computed(() => Boolean( poemState.value === "edit" && editorSnapshot.value && ( diff --git a/pages/notification/n02-message-detail.vue b/pages/notification/n02-message-detail.vue index 6427ffc..7140c17 100644 --- a/pages/notification/n02-message-detail.vue +++ b/pages/notification/n02-message-detail.vue @@ -1,6 +1,6 @@ + diff --git a/pages/profile/m05-change-phone.vue b/pages/profile/m05-change-phone.vue index d415fc3..cf80815 100644 --- a/pages/profile/m05-change-phone.vue +++ b/pages/profile/m05-change-phone.vue @@ -1,6 +1,6 @@