UAT 走查(docs/UAT-WALKTHROUGH-2026-09-23.md)发现 4 项硬阻塞,本次闭环其中 3 项 (#2 微信登录、#5 照片直传不在本轮范围),并顺带修掉走查中新发现的跨端字段契约缺陷。 后端 - #6 推荐恒空:新增 MatchScoringService 作为唯一评分事实源(复用 UserPreference 既有 10 维权重,零新造常量);RecommendationEngineService.calculateScore/generateReasons 由 桩(return 50.0)改为委托;MatchEligibilityService 3 处桩全部去雷。此前 50 < 阈值 60 → GET /recommendations 恒返回空数组。 - #4 保存即失败:updateProfile / updateBasicProfile / savePreference 改为 「无则插入、有则更新」。注意 UserPreference 实体 @TableId=id 与库主键 user_id 不一致 (V037 后补的可空列),故按 user_id 定位更新而非 updateById(后者会静默不更新)。 gender / birth_date 为 NOT NULL 且向导不采集,缺失时从已通过实名认证的 user_identities 补齐,仍缺则抛 400 业务拒绝,不再让 DataIntegrityViolationException 渲染成 500。 GET /me/profile 的 preferences 补回 5 个此前未返回字段(regionPriority / maritalPriority / minEducationCode / educationPriority / childrenPriority)。 - #8 越权端点:POST /activities/{id}/interests/confirm 不再直调「建 Match 原语」 RelationshipService.confirmActivityInterest,改为与 /activities/{id}/mutual/* 同一条 二阶段同意链路(createMutualConsent + confirmConnection),单方调用不再可能建 Match (INV-014 / INV-015)。 - #10 文档不一致:MatchmakerController 类注释更正(operator 端点实际在 /operator/matchmaker)。 - 存量 500 家族:RuntimeExceptionThrowGuardTest 的正则只认 throw new RuntimeException(, 而 17 处裸抛写作 orElseThrow(() -> new RuntimeException(...)),整体绕过守卫(CI 全绿却 仍有 500 家族缺陷)→ 17 处全量改为 GlobalExceptionHandler.BusinessException; 守卫扩为「整段代码 + 该形态」,并加自检断言防止守卫退化成恒真空断言。 小程序(跨端字段名 / 取值契约) - 字段名对齐后端规范名:hasChildren→childrenStatus、occupationCategory→occupationCode、 workRegion→workRegionCode、ageMin/ageImportance→minAge/agePriority、 educationAccepted/maritalStatusAccepted→minEducationCode/maritalStatuses。 此前字段名对不上,后端读不到 → 字段被静默丢弃。 - 取值修 4 类违反 DB CHECK(写入直接 500)的项:has→yes;high_school→senior_high、 phd→doctor(chk_profile_edu);occasional→sometimes(chk_profile_smoking/drinking)。 - 预填改读服务端真实字段:此前读 profile.hasChildren / occupationCategory / workRegion, 而服务端返回的是 childrenStatus / occupationCode / workRegionCode → 表单永远回填不上。 - profile-confirm 完成度改为直接采用服务端 profileCompletion(AGENTS 第 9 条:DB 是 Source of Truth),废弃原先 22 条「读服务端不存在字段」的本地猜测式断言。 - 择偶条件页新增「展示形态 ↔ 服务端规范形态」适配器(toApiPayload / fromApiPayload)。 测试与门禁 - 新增 MatchScoringServiceTest、RecommendationEngineServiceBehaviorTest、 ProfileSaveUpsertBehaviorTest、ActivityInterestConfirmConsentBehaviorTest 与 ui-tests/miniapp-profile-contracts.mjs(桩 stores,真跑三个页面的事件处理与 onNext, 断言提交负载用规范字段名与枚举码)。 - MIN_GUARDS 21→23(并修正上一轮漏改:该值当时仍为 21,与守卫数不符,使「数量下限」 保护形同虚设)。 - 负向注入实测:把 #6 评分改回桩 50.0、#4 改回恒 insert、#8 改回绕开二阶段、并注入 orElseThrow 形态裸异常 → 4 个守卫全部变红(含精确定位到 RecommendationEngineService.java:282)。 - 后端:795 tests / 0 failures / 3 skipped;UI 守卫 23/23;语种审计 0 命中;tsc 通过; wcc/wcsc 真编译 88 WXSS + 86 WXML / 0 失败。 仍未修(需产品/数据决策,见报告 8.3) - regionScope 需 GB/T 2260 单值区划码,而小程序选项含「镇江其他辖区」这类非单值项 → 不臆造区划码(该约束默认 preferred 软约束,不误过滤); - occupationPreference 后端无对应列;settlementPreference 后端无取值字典。
207 lines
6.2 KiB
JavaScript
207 lines
6.2 KiB
JavaScript
/**
|
||
* 资料 / 择偶条件枚举码 → 中文简体 唯一字典
|
||
* ============================================================
|
||
* 铁律:界面上不允许出现任何英文枚举码(如 never_married / bachelor /
|
||
* government / any / accept / reject)。所有会展示给用户的状态码、选项码
|
||
* 都必须在本文件里有中文 label。
|
||
*
|
||
* 为什么要集中:
|
||
* - 小程序里最典型的缺陷是「后端枚举码直接当展示值」上屏(例如某卡片
|
||
* 直接渲染 educationCode=bachelor)。静态类型检查 / WXSS 编译 / 结构
|
||
* 守卫都发现不了,只有人眼在真机才看得见。
|
||
* - 后端 demo 种子与实际表单存的码存在历史不一致(表单用 occasional,
|
||
* 种子用 sometimes;表单用 yes/no,种子用 是/否)。labelOf 统一覆盖
|
||
* 两套并集,保证详情页无论后端返回哪种都显示中文。
|
||
*
|
||
* labelOf(group, code):
|
||
* - 空值返回 ''
|
||
* - 已知码返回中文
|
||
* - 未知码返回原值(便于排查;生产路径不应触发,否则说明字典需补)
|
||
*/
|
||
|
||
// ============ 婚姻状况 ============
|
||
const MARITAL = {
|
||
never_married: '未婚',
|
||
divorced: '离异',
|
||
widowed: '丧偶',
|
||
single: '未婚', // profile-confirm 旧别名
|
||
}
|
||
|
||
// ============ 子女情况 ============
|
||
const CHILDREN_STATUS = {
|
||
none: '无子女',
|
||
has: '有子女',
|
||
}
|
||
|
||
// ============ 学历 ============
|
||
// 覆盖表单值(high_school) 与种子实际值(senior_high/junior_high) 及别名
|
||
const EDUCATION = {
|
||
high_school: '高中/中专',
|
||
senior_high: '高中/中专',
|
||
junior_high: '初中',
|
||
college: '大专',
|
||
bachelor: '本科',
|
||
master: '硕士',
|
||
phd: '博士',
|
||
doctor: '博士', // profile-confirm 旧别名
|
||
}
|
||
|
||
// ============ 职业 ============
|
||
// 表单类目(生产路径)+ demo 种子实际值(自由英文词)
|
||
const OCCUPATION = {
|
||
government: '机关事业单位',
|
||
education: '教育',
|
||
medical: '医疗',
|
||
enterprise: '企业职员',
|
||
technical: '技术人员',
|
||
self_employed: '个体经营',
|
||
freelance: '自由职业',
|
||
service: '服务行业',
|
||
agriculture: '农业',
|
||
other: '其他',
|
||
// demo 种子实际值
|
||
clerical: '行政文职',
|
||
designer: '设计',
|
||
factory: '工厂生产',
|
||
nurse: '医护',
|
||
engineer: '工程师',
|
||
programmer: '程序员',
|
||
sales: '销售',
|
||
doctor: '医生',
|
||
researcher: '科研',
|
||
teacher: '教师',
|
||
}
|
||
|
||
// ============ 择偶条件:是否接受有孩(preference-editor 子女下拉) ============
|
||
const ACCEPT_CHILDREN = {
|
||
any: '不限',
|
||
accept: '可接受',
|
||
reject: '不接受',
|
||
}
|
||
|
||
// ============ 吸烟 / 饮酒 ============
|
||
// 覆盖表单值(occasional) 与种子实际值(sometimes)
|
||
const SMOKING = {
|
||
never: '不吸烟',
|
||
occasional: '偶尔吸烟',
|
||
sometimes: '偶尔吸烟',
|
||
often: '经常吸烟',
|
||
}
|
||
const DRINKING = {
|
||
never: '不喝酒',
|
||
occasional: '偶尔喝酒',
|
||
sometimes: '偶尔喝酒',
|
||
often: '经常喝酒',
|
||
}
|
||
|
||
// ============ 红娘服务状态(matchmaker 历史服务) ============
|
||
const MATCHMAKER_SERVICE_STATUS_LABEL = {
|
||
submitted: '已提交',
|
||
assigned: '已分配',
|
||
in_service: '服务中',
|
||
waiting_user: '待您确认',
|
||
completed: '已完成',
|
||
cancelled: '已取消',
|
||
}
|
||
|
||
const GROUPS = {
|
||
maritalStatus: MARITAL,
|
||
childrenStatus: CHILDREN_STATUS,
|
||
educationCode: EDUCATION,
|
||
occupationCode: OCCUPATION,
|
||
acceptChildren: ACCEPT_CHILDREN,
|
||
smokingStatus: SMOKING,
|
||
drinkingStatus: DRINKING,
|
||
}
|
||
|
||
/**
|
||
* 通用映射:已知码→中文,未知→原值
|
||
* @param {string} group GROUPS 的键
|
||
* @param {*} code 后端返回的原始码
|
||
* @returns {string}
|
||
*/
|
||
function labelOf(group, code) {
|
||
if (code === undefined || code === null || code === '') return ''
|
||
const map = GROUPS[group]
|
||
if (!map) return String(code)
|
||
return map[code] !== undefined ? map[code] : String(code)
|
||
}
|
||
|
||
/** 是/否类(长期居住丹阳等):兼容布尔、yes/no、是/否 三种来源 */
|
||
function yesNoLabel(v) {
|
||
if (v === true) return '是'
|
||
if (v === false) return '否'
|
||
if (v === 'yes') return '是'
|
||
if (v === 'no') return '否'
|
||
if (v === '是' || v === '否') return v
|
||
return v === undefined || v === null || v === '' ? '' : String(v)
|
||
}
|
||
|
||
/** 未来规划(未来是否愿意继续在丹阳):willing/uncertain/unwilling 或 是/否 */
|
||
function futureDanyangLabel(v) {
|
||
if (v === 'willing' || v === '是') return '愿意'
|
||
if (v === 'uncertain') return '不确定'
|
||
if (v === 'unwilling' || v === '否') return '不愿意'
|
||
return v === undefined || v === null || v === '' ? '' : String(v)
|
||
}
|
||
|
||
// ============ picker 选项数组(value/label 形式,供 wxml range + range-key 使用) ============
|
||
const ACCEPT_CHILDREN_OPTIONS = [
|
||
{ value: 'any', label: '不限' },
|
||
{ value: 'accept', label: '可接受' },
|
||
{ value: 'reject', label: '不接受' },
|
||
]
|
||
|
||
const MARITAL_OPTIONS = [
|
||
{ value: 'never_married', label: '未婚' },
|
||
{ value: 'divorced', label: '离异' },
|
||
{ value: 'widowed', label: '丧偶' },
|
||
]
|
||
|
||
const EDUCATION_OPTIONS = [
|
||
{ value: 'senior_high', label: '高中/中专' },
|
||
{ value: 'junior_high', label: '初中' },
|
||
{ value: 'college', label: '大专' },
|
||
{ value: 'bachelor', label: '本科' },
|
||
{ value: 'master', label: '硕士' },
|
||
{ value: 'doctor', label: '博士' },
|
||
]
|
||
|
||
// ============ 服务区域(GB/T 2260 行政区划代码) ============
|
||
// 仅收录有权威依据的单值区划码:321181=丹阳、321100=镇江(市区)来自 demo 种子,
|
||
// 320100=南京市。当前小程序选项里的「镇江其他辖区」「不限」不是单值区划码,
|
||
// 故不纳入本表 —— 待在 UAT 报告中确认区域模型后再补(不要臆造区划码)。
|
||
const REGION_CODES = {
|
||
'丹阳': '321181',
|
||
'镇江市区': '321100',
|
||
'南京': '320100',
|
||
}
|
||
|
||
function regionCodeOf(name) {
|
||
if (!name) return ''
|
||
return REGION_CODES[name] || ''
|
||
}
|
||
|
||
const OCCUPATION_OPTIONS = Object.keys(OCCUPATION).map((v) => ({ value: v, label: OCCUPATION[v] }))
|
||
|
||
module.exports = {
|
||
MARITAL,
|
||
CHILDREN_STATUS,
|
||
EDUCATION,
|
||
OCCUPATION,
|
||
ACCEPT_CHILDREN,
|
||
SMOKING,
|
||
DRINKING,
|
||
MATCHMAKER_SERVICE_STATUS_LABEL,
|
||
GROUPS,
|
||
labelOf,
|
||
yesNoLabel,
|
||
futureDanyangLabel,
|
||
REGION_CODES,
|
||
regionCodeOf,
|
||
ACCEPT_CHILDREN_OPTIONS,
|
||
MARITAL_OPTIONS,
|
||
EDUCATION_OPTIONS,
|
||
OCCUPATION_OPTIONS,
|
||
}
|