Phase 13A(婚恋咨询与婚前辅导)复验关闭。历史 ✅ 不可信:counseling 模块存在双实现,
线上权威 CounselingService(167 行)零测试,唯一被测的 CounselingServiceApp(280 行)
是孤儿类(无注入点、无 HTTP 端点可达,生产永不执行)→ 已合并并删除。
生产缺陷(D1~D9):
- D1 Critical 生产故障 · INV-013 泄露:咨询正文 userRequestSummary 原文被写入 WARN 日志
(同 Phase 10 D2 一族)→ 改为只记 serviceId + riskLevel,绝不记正文。
- D2 High 越权:申请 userId 取自请求体而非认证主体 → Controller 强制 principal.id()。
- D3 High 并发:分配用「先读后写」→ 两管理员并发分配静默覆盖;
改 assignIfPending 条件更新(WHERE status='pending' AND assigned_operator_id IS NULL),
affectedRows=0 明确报错。
- D4 High 越权读:counseling_manager 经 getDetail 可读 followups 正文 → INV-013 绕过通道;
manager 只加回 userRequestSummary,不装配 followups。
- D5 Medium:无状态机 → 「已完成」可被再取消;新增 ALLOWED_TRANSITIONS + assertTransition。
- D6 Medium 生产故障:GET /counseling/services/intro 后端不存在(且被 /services/{id} 吞掉)
→ 小程序 intro 页 100% 无数据;补端点并声明在变量路径之前。
- D7 Medium 契约:Admin 调用 5 条不存在的 /services/operator/*(404)+ 缺四态
→ 后端补 8 个运营端端点(@PreAuthorize),前端整体重写 + 注册 /counseling 路由。
- D8 Medium 契约:小程序字段名错(userRequestSummary/preferredServiceMode)+ 无幂等
+ my-counseling 未检查 success(403 静默空列表)+ 跳转不存在的页 + WXML 内调用方法。
- D9 Medium 测试保真度:测试库 counseling 两表零 CHECK 约束,
counseling_services 未列入 EntityMigrationParityTest 任何集合(= 零覆盖)。
测试基建缺陷(T1~T4,不修即恒真假绿):
- T1 守卫找不到 DDL:surefire workingDirectory = ${project.basedir}/.. 使 CWD 为仓库根,
且 backend/ 是子目录非祖先 → 改为候选后缀 + 逐级向上定位,失败大声抛异常。
- T2 全限定 @com.baomidou...TableField(exist=false) 漏解析(假漂移警报)→ 正则放宽 + 回归守卫。
- T3 OperatorEndpointPermissionCoverageTest 扫到私有辅助方法(假阳性)→ 只扫描真实端点 + 自检。
- T4 两处测试自身逻辑缺陷:陈旧内存快照比较、Instant 纳秒 vs DB 微秒精度。
迁移:新增 V039(counseling 幂等键 + 三个 CHECK 白名单显式固化),基线 38 → 39。
负向验证两轮(均已还原,grep -c NEG-VAL = 0):
- ① DDL 常量守卫:从 V039 移除 'scheduled' → 常量守卫精准变红
(证明解析的是真实 DDL 而非副本)。
- ② 行为守卫:注入「并发回退先读后写 / manager 读 followups / 状态机守卫失效」
→ 三个用例各自精准变红。
测试:新增 CounselingPhase13ATest(25)+ CounselingConstraintGuardTest(5,解析真实 DDL);
删除孤儿类及其 3 个 DTO、删除测死代码的 CounselingDataScopeTest。
全量回归:Tests run: 420, Failures: 0, Errors: 0, Skipped: 3 / BUILD SUCCESS(4:49)。
基线 391 → 420(净 +29)。admin-web vue-tsc EXIT=0;miniapp tsc EXIT=0。
Tag: phase13a-recovery-2026-09-17
Report: docs/Phase-13A-V11-Recovery-Completion-Report-2026-09-17.md
57 lines
2.7 KiB
TypeScript
57 lines
2.7 KiB
TypeScript
export {}
|
||
import { request } from '../../utils/requestPromise'
|
||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||
|
||
Page({
|
||
data: {
|
||
name: '', phone: '', hasCompanion: false, companionName: '', companionUserId: '',
|
||
preferredTime: '', summary: '', submitting: false, errorMsg: ''
|
||
},
|
||
onNameInput(e: any) { this.setData({ name: e.detail.value }); },
|
||
onPhoneInput(e: any) { this.setData({ phone: e.detail.value }); },
|
||
onCompanionChange(e: any) { this.setData({ hasCompanion: e.detail.value }); },
|
||
onCompanionNameInput(e: any) { this.setData({ companionName: e.detail.value }); },
|
||
onCompanionIdInput(e: any) { this.setData({ companionUserId: e.detail.value }); },
|
||
onTimeInput(e: any) { this.setData({ preferredTime: e.detail.value }); },
|
||
onSummaryInput(e: any) { this.setData({ summary: e.detail.value }); },
|
||
|
||
async onSubmit() {
|
||
const { name, phone, hasCompanion, companionName, companionUserId, preferredTime, summary } = this.data;
|
||
if (!name || !phone) {
|
||
wx.showToast({ title: '请填写姓名和联系方式', icon: 'none' });
|
||
return;
|
||
}
|
||
this.setData({ submitting: true, errorMsg: '' });
|
||
try {
|
||
const res = await request<any>({
|
||
url: `${app.globalData.apiBase}/counseling/services`,
|
||
method: 'POST',
|
||
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
||
data: {
|
||
serviceType: 'premarital_guidance',
|
||
preferredTime,
|
||
// 字段名与后端 ApplyRequest 对齐(此前用 userRequestSummary 导致后端拿到 null)
|
||
summary: summary || `申请人:${name},电话:${phone}${hasCompanion ? `,伴侣:${companionName}` : ''}`,
|
||
requestSource: 'user_self',
|
||
// INV-005:伴侣须**本人**确认。此处只登记受邀伴侣 ID,确认动作由伴侣本人另行完成。
|
||
companionUserId: hasCompanion ? (companionUserId || null) : null,
|
||
idempotencyKey: `premarital-${name}-${Date.now()}`
|
||
}
|
||
});
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
wx.showToast({ title: hasCompanion ? '已提交,伴侣需本人确认' : '申请已提交', icon: 'success' });
|
||
setTimeout(() => wx.redirectTo({ url: '/pages/my-counseling/my-counseling' }), 1800);
|
||
} else {
|
||
this.setData({ errorMsg: (res.data && res.data.message) || '提交失败,请重试' });
|
||
wx.showToast({ title: (res.data && res.data.message) || '提交失败', icon: 'none' });
|
||
}
|
||
} catch (e) {
|
||
this.setData({ errorMsg: '网络异常,请稍后重试' });
|
||
wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
|
||
} finally {
|
||
this.setData({ submitting: false });
|
||
}
|
||
}
|
||
});
|
||
|