WorkBuddy
52e11a6c65
fix(notification): Phase 11 复验 —— 修复通知类型违反生产 CHECK 约束(生产故障)/ 幂等无库兜底 / 婚前辅导通知未接线
...
复验模式:KEEP-VERIFY/EXTEND(不轻信历史 ✅ ,逐条核对运行时路径与生产约束)。
== D3(Critical · 生产故障)通知 type 全量违反生产 CHECK 约束 ==
NotificationService 的 10 个业务模板传入复合 type
(appointment_submitted / counseling_assigned / le_verified / referral_created 等),
而生产 V020__extend_notification_types.sql 的 chk_notification_type 白名单只允许 12 个短名。
真实 PostgreSQL 下这些通知全部落库失败;测试库 schema-h2.sql 未复刻该约束,故历史 334 测试全绿 → 假绿。
修复:
- type 归一为白名单短名,业务细分交由 businessType/title 承担:
appointment_submitted|_confirmed|_changed -> appointment
counseling_assigned|_scheduled|_completed -> counseling
premarital_scheduled -> premarital
le_need_evidence|le_verified|le_rejected -> local_eligibility
referral_created -> matchmaker
- 重写 buildPrivacySafeCopy:原以复合 type 为 key 的 Map.ofEntries 归一后产生重复 key 会抛
IllegalArgumentException(运行期崩溃),改为按短名 switch。
- schema-h2.sql 补齐生产 CHECK 约束(测试保真度下沉)。
== D1(High)通知幂等仅 SELECT-then-INSERT,无 DB 兜底 ==
NotificationMapper.findExisting 为查后插,并发下可重复落库。
修复:新增 V036__add_notification_idempotency_index.sql,建 partial unique index
uk_notification_business(user_id, type, business_type, business_id, content)
WHERE business_id IS NOT NULL(先按 created_at ASC 去重存量数据)。
schema-h2.sql 同步唯一索引(H2 无 partial index,标准 UNIQUE 对 NULL 多行共存语义等价)。
== D2(Medium)婚前辅导通知未接线 ==
premaritalScheduled 零调用;CounselingServiceApp.scheduleCounseling(P13A-T22 管理端路径)完全不发通知。
修复:CounselingService.schedule 与 CounselingServiceApp.scheduleCounseling 均按 service_type 分流,
premarital 走 premaritalScheduled,其余走 counselingScheduled;
管理端路径包 try/catch —— 通知失败不回滚排期事实。
== 测试(基线 334 → 346,+12) ==
- NotificationPhase11IntegrationTest(5):回滚不发通知(Gate 前半)、提交后可见、
顺序重试幂等、绕过 findExisting 由唯一索引兜底抛异常且最终仅 1 条、business_id IS NULL 多行共存。
- NotificationPrivacyCopyTest(5):反射校验 4 类隐私文案,咨询类不含「咨询」且标题为「服务通知」,
不泄露姓名/时间/证件类型;未知 type 安全回退;ALLOWED_TYPES 守卫。
- NotificationTemplateTypeGuardTest(2):关键 —— 调用模板方法本身(而非直接传字面量 send),
断言 10 个模板全部以白名单短名落库。
- MigrationValidationTest:LATEST_MIGRATION 35 -> 36,DDL 校验补 CREATE UNIQUE INDEX 识别。
== 负向验证(两轮,含第一轮失效教训) ==
第一轮:注入复合 type 后 NotificationPhase11IntegrationTest 仍全绿 ——
因该测试直接 send(..., "appointment", ...) 传字面量,不经过模板方法,属无效守卫。
第二轮:改为 NotificationTemplateTypeGuardTest 调用模板本身,注入后精准抛
DataIntegrityViolation,守卫有效性得证。教训已写入报告:守卫必须经负向验证证明会变红。
== 证据 ==
全量回归 346 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS
(离线 Docker:danyang-match-backend-test:latest + 卷 danyang-m2-cache)
迁移校验 12/12;定向 19/19。
== Gate 判定 ==
回滚事务不发通知 ✅ | 通知失败不回滚核心事实 ✅ | 隐私安全文案 INV-010 ✅
已提交事务通知失败可重试且不产生重复业务记录 ✅ (修复后成立)
== 遗留 gap(登记不自动实现) ==
sendExternal 在事务内同步执行外部副作用,当前为占位实现;接入真实微信推送前必须改为
@TransactionalEventListener(AFTER_COMMIT) / REQUIRES_NEW —— 全仓当前无任何 afterCommit 机制。
2026-09-16 21:50:56 +08:00
WorkBuddy
fd7195ecb0
fix(safety): Phase 10 复验 —— 修复图片上传魔数校验死代码(生产故障)/ 高风险词写入日志
...
对 Phase 10(Image & Chat Safety)执行 KEEP-VERIFY 复验:不采信历史 ✅ ,逐条核对
图片私有访问、类型/大小/魔数校验、审核与水印、风险识别/举报/违规处理、日志脱敏。
Gate 判定通过,发现并修复 2 处缺陷,并推翻旧报告 2 项安全声明。
D1(Critical,生产故障)图片上传的类型与魔数校验整条是死代码
- PhotoService.uploadAndSave 用 `header = new byte[Math.min(12, stream.available())]`
仅分配零值缓冲,从不调用 stream.read(...) → validateMagicNumber 对任何文件都收到
12 个 0x00 → 恒返回 false → 恒抛"无效的图片文件头"。
- 影响链(已确认线上触发):小程序 local-eligibility-supplement.ts 以
photoType='local_eligibility' 调 POST /photos/upload → 本地资格佐证上传 100% 失败。
- 次生问题:isAllowedMimeType(image-only 白名单)在 local_eligibility 分支之后
无条件执行,即使魔数修好,application/pdf 仍被挡,而 FeatureFlagConfig
的 localEvidenceAllowedTypes 明确允许 pdf;另有未 mark 的 stream.reset() 无效调用。
- 测试为何没发现:PhotoServiceTest 只对 validateMagicNumber 传手写字节做单测,
从不经过 uploadAndSave,该路径缺陷完全无覆盖。
- 修复:改用 try(InputStream){ header = s.readNBytes(12); } 真正读取;
MIME 校验按 photoType 分流(local_eligibility 走自己的白名单含 pdf);
魔数校验仅对图片执行、PDF 跳过;移除死 reset();上传用全新流与校验流隔离;
generateFileKey 重载按 Content-Type 推断扩展名,PDF 不再拿到误导性 .jpg key。
D2(Low)高风险词写入普通日志
- ChatSafetyService.scanContent 把命中的高风险词原样写入 log.warn,
与同文件 CONTACT 分支"只记 pattern 不记内容"矛盾,且违反本阶段 Gate。
- 修复:改为 log.warn("Chat content matched HIGH_RISK keyword (blocked)")。
推翻的旧声明(docs/Phase-10-Completion-Report.md)
- 「伪装恶意图片被拒(Magic Number校验)」不成立:该校验运行时恒失败,
连合法图片也一律被拒(fail-closed 到不可用),安全属性并非由它提供。
- 「EXIF清理预留(PhotoService.stripExif占位)」不存在:全仓库无该符号。
且 getUploadToken 客户端直传路径服务端无法介入清理 EXIF。
- 已在旧报告顶部加状态注记,指向本次复验报告。
测试
- 新增 PhotoUploadValidationTest(6 用例),用 MockMultipartFile 真正走一遍
uploadAndSave:合法 JPEG/PNG 通过且确实调用 putObject、伪造 Content-Type 被魔数挡、
PDF 佐证不被 image-only 挡且 key 以 .pdf 结尾、非法 MIME 被拒、超大被拒。
- 负向验证:临时把魔数守卫改为 if(false && ...) → 恰好 spoofedContentTypeRejectedByMagic
一个用例变红,其余 5 个仍绿;随后还原并 grep 确认无 NEGATIVE-CHECK-TEMP 残留。
- 全量回归(离线容器 danyang-match-backend-test,mvn -o test):
334 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(基线 328,+6)。
登记 gap(不自动实现,待产品确认)
- 图片水印:全仓库无实现;主计划未列硬需求,仅"开发执行计划修订版"提"高级动态水印"。
- EXIF 清理:旧声明不存在;客户端直传路径服务端无法介入,需改为接收侧重编码。
- 直传凭证路径(getUploadToken)无服务端内容校验且不落 DB 记录 → 后续 getSignedUrl
归属校验会查不到 UserPhoto 行,属契约层缺口,超出本阶段安全 Gate 范围。
- 违规自动升级:ChatSafetyService 注释明确属 Phase 17 范围,当前 high 仅拦截不落库。
文档:新增 Phase-10-V11-Recovery-Completion-Report-2026-09-16.md;
旧报告加状态注记;主计划 Phase 10 复选框置 ✅ ;STATUS / JIRA_BACKLOG 同步(334 tests)。
Phase 11 未启动,按计划每阶段出报告后停止等待确认。
2026-09-16 21:00:52 +08:00
WorkBuddy
03a03c7c11
fix(chat): Phase 09 复验 —— 修复 WebSocket 下行死链路 / Unmatch 后可发消息 / Block 未落库 / 聊天 IDOR
...
对 Phase 09(Text Chat)执行 KEEP-VERIFY/EXTEND 复验:不采信历史 ✅ ,逐条核对
REST 发送 + WebSocket 接收、幂等、重连、未读计数、非 active Match 禁聊、Unmatch/
Block 即时禁聊、活动双确认会话复用。共发现并修复 4 处真实缺陷 + 1 处规范不符。
D1(Critical)WebSocket 下行整条链路是死代码
- 无 HandshakeInterceptor,无任一环节把 userId 写入 session attributes,
extractUserId 恒返回 null;sendToUser/sendToConversation 全仓库零调用点。
- 新增 ChatHandshakeInterceptor:从 ?token= 或 Authorization: Bearer 解析 JWT,
校验通过后写 session attr userId;失败 fail-closed 拒绝握手;仅接受 type=access。
- 重写 ChatWebSocketHandler:按规范 {event,data} 信封推送;未认证连接直接
close(NOT_ACCEPTABLE);同账号重连由新连接接管、旧连接 close(NORMAL);
上行仅支持 ping->pong,禁止客户端自订阅。
- ChatService.sendText 落库+递增未读后推送 message.new;RiskNotice 同推。
- 移除 WebSocketConfig 的 .withSockJS()(SockJS 改写端点致原生 WS 无法直连)。
D2(Critical)Unmatch 后仍可发消息
- unmatch 只改 Match,不联动会话;sendText 仅校验 conv.status==active。
- 新增 ChatService.assertMatchAlive(conv):按 matchId 查 Match,状态非
{active,getting_to_know} 抛 "匹配已解除,无法发送消息"。sendText 在 conv.status
校验之后调用。
- MatchController.unmatch 落库后调 chatService.syncFromMatch 推 conversation.closed。
- syncFromMatch 重写:alive 恢复 active;非 alive 只推 conversation.closed,
不改 status(会话仍以灰显态留在列表,属未授权产品决策,登记待决)。
D3(Critical)RelationService.block 调静态 MatchService.unmatch 只改内存态不落库
- 结果:Block 后 matches.status 仍为 active。
- 补 matchMapper.updateById(m) 持久化 + chatService.closeConversationForMatch(
CONVERSATION_STATUS_BLOCKED) 通知对端。RelationshipService.block 同步补会话关闭通知。
D4(Critical 安全)聊天 4 个接口无成员校验 -> IDOR 越权读私聊
- MessageController.load/markRead、ConversationController.detail/unread 补
chatService.isMember(conversationId, userId) 校验;非成员返回
ApiResponse.notFound()(body 级 success=false code=NOT_FOUND,HTTP 200,
与 MatchController.authorizedMatch 同约定),不区分"不存在"与"无权"以防探测。
D5(Medium)上行订阅/信封不符规范
- REST API 接口详细设计规范.md 第124-126章要求禁止客户端自订阅,事件名
message.new/message.read/conversation.closed,信封 {event,data}。已对齐。
测试保真度 + 测试补齐
- schema-h2.sql 新增 uk_message_client_id(sender_id, client_message_id),
对齐生产 V004:71;H2 不支持过滤索引故省略 WHERE(NULL 语义等价)。
否则 service 的 SELECT-then-INSERT 竞态在测试中无 DB 兜底,形成测试盲区。
- 新增 ChatPhase09IntegrationTest(13 用例):幂等重试不重复投递、推送信封字段、
未读增减清零、Match unmatched 拒发、Unmatch/Block 端点即时禁聊+收 closed 事件、
活动双确认复用同一消息体系、未形成 Match 无法建会话、非成员读消息/详情/未读
被拒且不泄露正文、DB 唯一索引兜底重复 clientMessageId、无幂等键系统消息可共存。
- 新增 ChatWebSocketHandlerTest(7 用例):userId 路由、信封格式、未认证关连、
断开不推、重连替换旧连接、上行仅 ping。
验证证据
- 全量回归(离线容器 danyang-match-backend-test,mvn -o test):
328 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(基线 308,+20)。
- 负向验证:临时注释 assertMatchAlive + 两处 isMember,恰好 3 个用例变红,
证明守卫有效;随后还原并 grep 确认无残留。
文档
- 新增 Phase-09-V11-Recovery-Completion-Report-2026-09-16.md(完整报告)。
- Phase-09-V11-Completion-Report.md 顶部加状态注记,推翻其 4 条 WebSocket 结论。
- 计划/STATUS/JIRA_BACKLOG 同步 Phase 09 完成态与测试总数(328)。
Phase 10 未启动,按计划每阶段出报告后停止等待确认。
2026-09-16 16:42:48 +08:00
WorkBuddy
4e478ce6ba
fix(invariants): Phase 08 复验 —— 修复 INV-004 搜索/详情绕过 Block + matches.source 取值越界
...
复验(KEEP-VERIFY)在既有 Phase 08 实现中发现 3 处真实缺陷并修复:
D1(严重)DiscoverService.searchUsers 不含查看者身份,查询条件既无排除自己也无任何 Block 条件,被拉黑者仍可被搜到并点开 —— INV-004「搜索不可见」整体失效。改为接收 viewerUserId,排除自己,并用新增的 UserRelationMapper.findBlockedUserIdsBothWays(UNION 双向)一次取回后 notIn 批量排除,避免 N+1。
D2 详情路径(DiscoverService / UserProfileService)均无 Block 判断,知道 userId 即可读到被拉黑者资料。两条路径统一加双向判断并返回 null,由 Controller 转 404(不用「已隐藏」以免被用来探测存在);UserProfileService 中该判断排在隐私设置之前,拉黑不能被对方「允许搜索可见」覆盖。
D3(潜伏,仅 PostgreSQL 暴露)RelationshipService.like() 把关系类型值 "liked" 当 match source 写入 matches.source,违反 V003:91 的 chk_match_source。测试库 schema-h2.sql 该列是裸 VARCHAR(32)、无约束,故长期静默通过。改传 MATCH_SOURCE_USER_LIKE。canonical 路径 RelationService.like 传值本就正确,线上未触发。
测试保真度对齐:schema-h2.sql 补 chk_match_source(与 V003 逐字对齐)与 user_profiles 表(此前测试库无此表,资料查询无法端到端测试)。
新增 DiscoverBlockInvisibilityIntegrationTest(9 用例,@SpringBootTest 真实 H2 + 真实 SQL),覆盖正向/反向拉黑、对称性、排除自己、详情 404 与查自己不受限。P0FixVerificationTest 补 source 回归守卫。DiscoverServiceTest 同步构造签名。
负向验证:临时还原 D3 修复后,4 个用例因 CHK_MATCH_SOURCE 约束违反报错,证明守卫有效。
证据:后端全量 308 tests / 0 failures / 0 errors / 3 skipped(基线 299);UI 契约 13/13 GREEN。报告 docs/Phase-08-V11-Recovery-Completion-Report-2026-09-16.md。
同步 STATUS.md / JIRA_BACKLOG.md(EPIC-P08-V11 置 ✅ )/ 客户需求基线计划 Phase 08 复选框;首轮报告加状态注记(其「Known Issues 无」已被推翻)。.gitignore 忽略 .workbuddy/。
2026-09-16 15:11:38 +08:00
Codex
bc4531a734
feat(api): 补齐 /me/* 命名空间,恢复 17 个 404 调用 + 修 /me/status 过期桩
...
背景:小程序 services/*.js 按 REST 规范(§16 / §22-§40 / §80 / §83-§84)统一调用
/me/*,但后端从未注册 /me 基路径,个人资料、照片、择偶条件、隐私设置、关系状态、
我的举报/申诉等约 17 个调用全部 404。
后端
- 新增 me/controller/MeController:把既有 service 能力暴露到规范路径(纯委托,不复制
业务规则,AGENTS.md #26)。旧路径(/profile/*、/photos、/relationship/*、
/account/privacy-settings、/risk/reports/my、/appeals/my)全部保留。
- 响应形状按小程序 JSDoc 适配(小程序被 86 页引用,是权威契约):
· /me/relationship-status 返回 status(旧 /relationship/status 返回 relationshipStatus,
字段名不同,前端读的是 result.status)
· /me/profile/review-status 返回 {status, items, submittedAt, reviewedAt},时间统一 epoch 毫秒
· /me/profile/submit-review 返回 {reviewStatus, submittedAt}
- 小程序三条历史写法一并兼容:/me/privacy-settings、/me/photos/{id}/main、/me/appeals
修复
- AuthService.getMeStatus:verified / profileStatus / reviewStatus 三项写死为
false / not_started / not_submitted,注释标注"Phase 03/04/05 填充",但那三个 Phase
早已交付,导致 /me/status 长期返回错误的实名与审核状态。改为 capabilityService
+ userProfileMapper + reviewTaskMapper 的真实来源(与首页聚合同一判定)。
- PhotoController.deletePhoto:原用 ((Map<?,?>) p).get("id") 强转 UserPhoto 实体,
必然抛 ClassCastException,删除照片恒定 500。改用实体主键比对。
- PhotoService.setMainAvatar(新增):数据模型以 photo_type='avatar' 标识头像(见
UserPhotoMapper#findAvatar),无 is_main 字段,故"设主图"= 换类型并保证至多一张。
注意:插入该方法时曾把 deletePhoto 原有的 @Transactional 挤到新方法上,导致
"Transactional is not a repeatable annotation" 并引发 Lombok 全面停摆(满屏
找不到 getter/setter),已修正注解归属。
验证
- 后端全量:299 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(+2 为新增契约测试)
- 新增 MeRouteContractTest(2 例):同时断言 HTTP 方法,锁定 21 条 /me/* 与 13 条旧路径
- 小程序 tsc --noEmit 通过;UI 契约门禁 13/13
文档
- STATUS.md:/me/* 从"待裁决"改为"已完成";核心待清债务 1 项 → 0 项
- API_INVENTORY.md:顶部"待收敛"警告改为"已收敛",补全 /me/* 端点表
2026-09-16 12:54:39 +08:00
Codex
3244e02e7e
refactor(api): 关系/匹配域采用 2026-09-14 canonical 路径,消除三代并存
...
## 问题:同一能力存在三代路径,前端指向的是已废止的一代
按 mtime 排查后发现"最新"并非上一轮以为的 /relations/*,而是 2026-09-14 的 match 模块:
| 代际 | 位置 | 日期 | 路径 |
|---|---|---|---|
| canonical | platform.match: Like/Block/MatchController | 09-14 12:13~12:14 | /likes /blocks /matches |
| 二代 | platform.relationship: Relationship(Status)Controller | 09-10 10:59 | /relations/* /relationship/* |
| 一代(规范) | REST API 接口详细设计规范.md | 08-28 | /relationships/*(多一个 hips) |
| 前端 | miniapp/services/matchService.js | 08-29 | 混用二代 + 一代 |
后果:缘分三榜、黑名单页、成功牵手流程全部 404;另有两份重复 success 实现 + 一份桩实现。
## 裁决:采用 2026-09-14 最新路径,前端与二代让位(用户指令)
### 补齐 canonical 模块缺口(此前只有 javadoc 声明或服务方法,无控制器)
- RelationService.unlike() + LikeController `DELETE /likes/{targetUserId}`
- 新增 PassController `POST /passes`(RelationService.pass 在 09-14 模块内却无控制器,补齐与 /likes、/blocks 对称)
- BlockController `GET /blocks`(javadoc 早声明"我的 Block 列表"但未实现,mapper findBlockedByUser 已就绪)
- MatchController `POST /matches/{id}/success/request|confirm|decline`(javadoc 标"Phase 12 预留"),
委派真实实现 RelationshipStatusService;authorizedMatch 对"不存在"与"无权"统一回 404,避免探测他人 Match
- LikeResponse 对齐前端 LikeResult:{ relationStatus, mutualMatch, matchedConversationId, matchId }
Mutual 时调 ChatService.getOrCreateConversation 取会话 id → "双向喜欢→跳聊天"链路真正打通
- LikeController/PassController/BlockController 补 targetUserId 空值校验
### 移除被取代的路由与桩实现
- RelationshipController 移除 like/cancelLike/pass/block/unblock/matches/unmatch/success,仅保留 /fate/*
- RelationshipStatusController 移除 /success/*(已由 /matches/{id}/success/* 取代),保留关系状态生命周期
- RelationshipService 删除 requestSuccess/confirmSuccess 桩 ——
前者自述 "TODO: 查询 relationship_success_requests 表" 却返回硬编码 status="pending",
等于绕过 AGENTS 硬规则 6「成功牵手必须双确认」
- 注意:RelationshipService.like/block/pass 被 P0FixVerificationTest /
RelationshipBlockPriorityPhase08Test 直接测试,方法体保留
### 列表信封统一
- CursorPage 从 chat.dto 提升到 shared.dto(消除 match 模块反向依赖 chat DTO)
- /relations/fate/mutual|liked|likers 与 GET /matches 改返回 { items, nextCursor }
### 前端对齐
- matchService.js 全部路径改到 canonical;补 declineSuccess
- matchStore 补 _decorateBlockedList,修复"模板渲染 blockedAtText 但全仓从未计算"的缺陷
(与上一轮 lastMessageAtText 同源,同类字段一律在 store 装饰一次)
### 文档
- docs/api/API_INVENTORY.md 关系/缘分/匹配三节重写为 canonical 路径;
顶部加 /me/* 路径族显式警告(小程序按规范写 /me/*,后端实为 /profile/*、/photos、/relationship/*,
约 15 个端点会 404,收敛方向未裁决,防止后续照抄踩坑)
- docs/STATUS.md:Backend 回归债务由「待清(277 tests 4F/8E)」更新为「已清偿(297 tests 全绿)」
## 验证
- 新增 CanonicalRouteContractTest(3 例):用 RequestMappingHandlerMapping 断言
canonical 路径已注册、被取代的旧路径不再注册、关系状态生命周期保留 —— 路由再漂移即红
- 后端 297 tests, 0 failures, 0 errors, 3 skipped, BUILD SUCCESS
- 小程序 tsc --noEmit 通过
2026-09-15 23:16:27 +08:00
Codex
687a156350
fix(chat): 会话/消息响应契约对齐小程序,修复聊天页字段断链
...
## 问题(三处真实断链)
后端 chat 模块字段名与小程序契约不一致,但自动化测试此前未覆盖,故一直未被发现:
1. 会话 DTO 用 otherUserId/otherDisplayName/otherAvatarUrl,
而前端(types/index.d.ts + pages/chat/chat.ts + components/conversation-item)
只读 targetUserId/targetDisplayName/targetAvatarUrl。
→ chat.ts 取不到对方 userId(无兜底,落到空串),
导致"查看资料 / 拉黑 / 举报"点击后提示"暂无更多信息"并失效。
2. 消息响应只给 createdAt(ISO 字符串),前端读 Message.sentAt: number(epoch 毫秒)。
→ chat.ts 时间分隔 ts 恒为 0,消息时间分隔与时间文案不显示。
3. 会话对象缺 relationStatus,前端 conversation-item 依赖它做灰显与"已拉黑"提示。
## 裁决依据
按"以文件修改时间为准"的裁决统一到前端约定:
- 前端 types/index.d.ts 被 86 个页面引用,是声明式契约
- pages/chat/chat.ts(09-04)是最新的真实消费者
- targetUserId 已是项目内既有惯例(ActivityMatchItemDTO)
- admin-web 不消费 /conversations,无第二方受影响
## 变更
- ConversationDTO:字段改名 target*;新增 relationStatus;lastMessageAt 改 epoch 毫秒
- ChatService.toConversationDTO:同步映射;relationStatus 按 Block > unmatched > mutual_match > neutral 推导
- Constants:新增 RELATION_STATUS_NEUTRAL / MUTUAL_MATCH / UNMATCHED(不硬编码)
- MessageResponse:新增 sentAt(epoch 毫秒)
- miniapp/utils/datetime.js(新增):收敛时间格式化,
补上模板依赖但全仓从未计算的 lastMessageAtText
- messageStore:会话进入 store 时统一装饰 lastMessageAtText
- chat.ts:_formatTime 改为复用共享工具;移除已失效的 otherDisplayName 兜底
## 验证
- 新增 ChatContractJsonTest(4 例)锁定字段名、sentAt 数值类型、
并断言 other*/status 旧字段名不再出现
- 后端 294 tests, 0 failures, 0 errors(原 290 + 新增 4)
- 小程序 tsc --noEmit 通过
遗留(未在本轮处理):平台尚未实现头像 URL 解析(user_photos 只存 fileKey),
targetAvatarUrl 保持 null,前端 ui-avatar 回退昵称首字,不影响展示。
2026-09-15 22:45:46 +08:00
Codex
eaf8fdf195
refactor(backend): P0 阻断修复 + 按最新修改时间收敛重复模块与契约
...
## P0 阻断修复
- 编译:修复实体类与服务类同名导致 Lombok 注解轮次中止的问题
(MatchmakerService/CounselingService -> *Record),196 个编译错误归零
- 权限矩阵:platform_admin 按设计文档收紧为拒绝 5 项高危权限(不改测试阈值)
- 启动:修复 8 组 MyBatis-Plus 同名 bean 冲突(ConflictingBeanDefinition)
与 URL 路由歧义(Ambiguous mapping),应用可正常启动
## 模块收敛(裁决规则:以文件修改时间为准)
- messaging 包整体删除,能力迁移至 chat 包
* 会话列表/详情/未读端点、ConversationDTO、CursorPage
* 消息游标分页统一为 { items, nextCursor }
- relationship 的 Match/UserRelation entity 与 mapper 合并进 match 包
- MatchmakerUserController 合并进 MatchmakerController,删除冗余 Controller
- 补齐 INV-004:chat sendText 增加双向 Block 校验,拉黑后禁止发送
## 契约对齐
- 红娘服务提交请求兼容前端 { requirement } 字段
- 消息与会话分页契约前后端统一
## 测试
- 迁移 6 个风控用例至 ChatSafetyServiceTest
- 清理 messaging 测试包遗留:6 个真实常量断言归位 chat 包,
移除 2 个 assertTrue(true) 占位假测试(Block 真实行为已由
P0FixVerificationTest / RelationshipBlockPriorityPhase08Test 覆盖)
- schema-h2.sql 对齐 PostgreSQL 真实表结构(matches 补列、
conversation_members 改复合主键)
结果:290 tests, 0 failures, 0 errors, 3 skipped, BUILD SUCCESS
2026-09-15 22:29:26 +08:00
Codex
918368159c
feat(phase-13A): CounselingService — INV-012/013 + 婚恋咨询 + 婚前辅导
...
Phase 13A Counseling — ADD new domain module per AGENTS.md rule #21/#22.
counseling module (5 files):
- Entity: CounselingService -> counseling_services
- Entity: CounselingFollowup -> counseling_followups
- Mapper: CounselingServiceMapper (findFull + findSanitized INV-013)
- Service: CounselingService
INV-012: assertAssigned() guards schedule/complete
INV-013: getDetail() returns sanitized for matchmaker/operator,
full for user/assigned-counselor/manager
- Controller: 7 endpoints (user/apply/list/detail, counselor/assigned/schedule/complete, manager/assign)
Permission model (Service layer guarded):
User(self) -> full detail
Assigned Counselor -> full detail
counseling_manager -> full detail
Matchmaker/Operator -> sanitized only (no user_request_summary)
service_type: counseling(婚恋咨询) / premarital(婚前辅导)
2026-09-14 14:51:25 +08:00
Codex
5cbd931fa2
feat(phase-13): MatchmakerService — referral + manual recommendation + user confirmation
...
Phase 13 Matchmaker — EXTEND per JIRA. P13-V11-T01 through P13-V11-T05.
New matchmaker module (5 files):
- Entity: MatchmakerService (request), MatchmakerRecommendation (manual rec)
- Mapper: MatchmakerServiceMapper, MatchmakerRecommendationMapper
- Service: MatchmakerService
T01 createReferral(type=counseling) — referral only, does NOT submit on behalf
T02 createReferral(type=activity)
T03 createReferral(type=service_station)
T04 referral_source追溯: every referral carries operator_id + service_id + type
T05 用户拥有最终决定权: acceptRecommendation() calls RelationService.like()
as the USER, not the matchmaker. Matchmaker NEVER creates Match directly.
If mutual, updates Match.source='matchmaker' for traceability.
- Controller: user endpoints (submit/list/pending-recommendations/accept/pass/viewed)
Core AGENTS.md #5 compliance:
- Matchmaker can only 'recommend', NEVER 'like on behalf of'
- User must actively accept recommendation (calls acceptRecommendation -> RelationService.like)
- createRecommendation creates pending_view record + notification, nothing else
- confirm mutual only when BOTH users independently Like (existing Like flow)
Status machines:
- matchmaker_services: submitted -> assigned -> processing -> completed
- matchmaker_recommendations: pending_view -> viewed -> liked/passed/confirmed_mutual
2026-09-14 14:48:23 +08:00
Codex
0418856ce3
feat(phase-12): RelationshipStageService — home service suggestions
...
Phase 12 Relationship — KEEP/EXTEND per JIRA. 不增加复杂状态.
New RelationshipStageService (match module):
P12-V11-T01 Successful match -> premarital_suggestion
P12-V11-T02 GettingToKnow match -> matchmaker + counseling
P12-V11-T03 7-day silence in GettingToKnow -> counseling_recommendation
Rules are simple, based on existing Match.status + lastActivityAt:
- successful -> 婚前辅导建议
- getting_to_know -> 红娘服务 + 婚恋咨询建议
- getting_to_know + 7d silence -> 婚恋咨询促活建议
- no Match -> 默认红娘 + 婚恋课堂
Integration point: HomeApplicationService.buildRecommendations()
just inject RelationshipStageService and append suggestions to home DTO.
2026-09-14 14:40:13 +08:00
Codex
e2a8a8c85f
feat(phase-11): NotificationService — 10 business templates + privacy-safe external copy
...
Phase 11 Notification — EXTEND per JIRA. P11-V11-T01 through P11-V11-T11.
New notification module (4 files):
- Entity: Notification -> notifications (existing DB table)
- Mapper: findByUser/countUnread/markRead/markAllRead + findExisting(idempotent)
- Service: NotificationService with 10 business templates
T01 appointmentSubmitted T02 appointmentConfirmed T03 appointmentChanged
T04 counselingAssigned T05 counselingScheduled T06 counselingCompleted
T07 premaritalScheduled T08 leNeedEvidence T09 leVerified
T10 leRejected
T11 buildPrivacySafeCopy -> external lock-screen copy NEVER reveals
sensitive counseling type names (INV-010 compliance)
- Controller: GET list, GET unread, PUT markRead/{id}, PUT readAll
Design invariants:
- sendExternal() wrapped in try/catch -> failure does NOT rollback core service
- Idempotency via (user, type, business_type, business_id, content) dedup
- External copy uses neutral words '服务/预约' for counseling events, not '咨询'
2026-09-14 14:33:37 +08:00
Codex
cf3a84e725
feat(phase-10): Chat Safety — content scan + rate limit + RiskNotice
...
Phase 10 Chat Safety — KEEP per JIRA. P10-V11-T01/P10-V11-T02.
New ChatSafetyService (chat module):
- scanContent(): HIGH_RISK keyword + CONTACT regex (phone/wechat/QQ/scan)
- checkRateLimit(): Redis counter + fail-open fallback, stricter for new Match (first 5 min)
- buildRiskNoticeBody(): system message JSON with anti-fraud targetPage
ChatService.sendText() integrated Phase 10:
1. Redis rate limit check (newMatch-aware)
2. Content scan -> risk_level on Message
3. high/critical -> throw, block send
4. warning -> peer gets RiskNotice system message (P10-V11-T01: anti-fraud entry)
(P10-V11-T02: RiskNotice -> anti-fraud content page)
5. Log enriched with risk_level
Constants already had RISK_LEVEL_* and MESSAGE_TYPE_SYSTEM — no new constants needed.
2026-09-14 14:16:47 +08:00
Codex
eda2ce19a8
feat(phase-09): Text Chat full backend + WebSocket
...
Phase 09 Chat — KEEP per JIRA. P09-V11-T01 Activity Match source compatibility.
New chat module (11 files):
- Entity: Conversation, ConversationMember (复合主键), Message
- Mapper: ConversationMapper, ConversationMemberMapper, MessageMapper
- Service: ChatService (getOrCreateConversation/sendText/loadMessages/markAsRead/syncFromMatch)
- Controller: ConversationController (POST /from-match, GET /{id}, GET list)
MessageController (POST send, GET load history, PUT mark read)
- WebSocket: WebSocketConfig + ChatWebSocketHandler (/ws/chat, JSON protocol)
Core invariants:
- INV-005: ChatService requires Match status active/getting_to_know
- Conversation.status sync with Match.status (syncFromMatch)
- Member.canSend + Conversation.status + sender membership all validated
- Client-side idempotency via client_message_id
- P09-V11-T01: Conversation binds only match_id — works identically for
user_like / activity_mutual / matchmaker Match sources
WebSocket note: Memory-based session map; multi-node will need Redis Pub/Sub.
2026-09-14 14:00:04 +08:00
Codex
8b62b5b5df
feat(phase-08): Like / Match / Block full backend implementation
...
Phase 08 Like/Match/Block — KEEP per JIRA, incremental P08-V11-T01.
New entities (match module):
- UserRelation -> user_relations (liked/passed/blocked)
- Match -> matches (source: user_like/activity_mutual/matchmaker, episode-aware)
New mappers:
- UserRelationMapper: findOne/countBlockedEitherWay/countReverseLike
- MatchMapper: findActiveBetween/findLatestBetween/findActiveByUser/findHistoryByUser
New services:
- RelationService: like/pass/block/unblock + mutual detection + capability guard
- INV-004: Block priority highest (auto-unmatch, mutual block check)
- INV-005: Like only creates Match on Mutual
- MatchService (static): createOrReuseMatch factory
- AGENTS.md #19 : existing active Match -> reuse
- AGENTS.md #20 : prior unmatched -> new Match with episode+1
- source-aware: user_like / activity_mutual / matchmaker
New controllers:
- POST /api/v1/likes
- POST /api/v1/blocks, DELETE /api/v1/blocks/{target}
- GET /api/v1/matches, GET /api/v1/matches/{id}, POST /api/v1/matches/{id}/unmatch
Integrates with CapabilityService for base eligibility checks on Like.
2026-09-14 12:15:27 +08:00
Codex
dd58ab6eae
feat(capability): extract CapabilityService — AGENTS.md rule #26 compliance
...
New module backend/capability/:
- CapabilityService: getCapabilities(entry point for frontend),
plus public helpers (isAccountNormal/isIdentityVerified/isProfileApproved/
isSearching/isBaseEligible/hasLocalEligibility/entry builder)
- CapabilityController: GET /api/v1/capabilities endpoint
HomeApplicationService refactored:
- Injects CapabilityService, delegates buildCapabilities()
and QuickEntry construction + recommendation eligibility checks
- Removes duplicated private methods (isSearching/isIdentityVerified/
isProfileApproved/entry) — single source of truth now in CapabilityService
Contracts updated to check CapabilityService.java for capability-related
assertions (setDisabledReason/isIdentityVerified/ACCOUNT_STATUS_NORMAL)
2026-09-14 11:38:30 +08:00
Codex
ea2bb2439d
chore: complete infrastructure config, contracts, reference images and ui redesign artifacts
2026-09-14 10:02:34 +08:00
Codex
63cbbff2de
feat: implement miniapp C-group pages with real API calls and design tokens
2026-09-14 10:02:22 +08:00
Codex
bb380d40f6
feat: enhance home aggregate, review atomicity, local eligibility and match engine (phase 04-07)
2026-09-14 10:02:07 +08:00
Codex
10887f40ed
feat: add V034-V035 flyway migrations and phase 04-07 test classes
2026-09-14 10:01:50 +08:00
admin
b8c33b9a0c
chore: remove legacy build artifacts
2026-09-11 10:19:22 +08:00
admin
ac2327b499
chore: checkpoint before switching to main
2026-09-11 09:07:41 +08:00
admin
c20301ffff
fix: restore miniapp public startup flows
2026-09-10 22:57:52 +08:00
admin
d6f5f25c35
fix: repair admin page api contracts
2026-09-10 21:31:38 +08:00
admin
7994e081a6
fix: rebuild latest demo database
2026-09-10 20:28:29 +08:00
admin
5b51d5b3f1
feat: complete phase 02 RBAC and data scope recovery
2026-09-10 10:58:43 +08:00
admin
b46b48eb33
chore: establish V1.1 recovery baseline
2026-09-09 22:05:00 +08:00