Files
danyang-match/miniapp/pages/premarital-request/premarital-request.ts
T
WorkBuddy 7a2693b239 fix(counseling): close Phase 13A KEEP-VERIFY/EXTEND — 9 real defects + 4 test-infra defects
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
2026-09-17 01:25:12 +08:00

57 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
}
}
});