fix(privacy): updatePrivacySettings 改 upsert —— 同一用户第二次更新不再撞 user_id 主键
user_privacy_settings 的主键自 V002 起就是 user_id,旧实现无论记录是否存在都无条件 insert,
导致同一用户第二次更新必然 DuplicateKeyException → 500,隐私设置从此再也改不动。
现改为「存在则按 user_id 更新,不存在才插入」。
更新条件刻意用 user_id 而非实体 id:id 是 V037 为「实体↔表对齐」后补的独立列,
不作为寻址依据 —— 否则历史行 id 缺失时更新会静默失效。
已知边界(未擅自处理):findByUserId 带 deleted_at IS NULL,若将来引入软删路径,
已软删的行会让本方法走插入分支并撞主键。当前不存在任何软删隐私设置的路径,故不可达;
若将来新增软删,需同步改为 PG ON CONFLICT (user_id) DO UPDATE。已在代码注释中标注。
test(privacy): PrivacySettingsUpsertGuardTest —— 4 用例,全走真实落库(H2),不用 mock 断言调用次数
① 连续更新两次:第二次不得抛异常 + 库中仍只有 1 行(防退化成重复插入,
那会让 findByUserId 的 Optional 返回值抛 TooManyResultsException)
② 部分更新不得把未提交字段重置回默认值
③ 首次更新未提交字段取默认 true
④ 无记录时读取返回全 true 且不落库(固化既有契约)
负向注入已验证:退回无条件 insert → 恰 2 红,失败原因即 DuplicateKeyException;
另 2 个只更新一次的用例正确未红(走不到该分支)。已还原并复跑全绿。
docs(plan): Task 3.1 拆两层并记录进展;附 B 增第 13 项;基线 681→685
⚠️ 关键发现(附 B-13):隐私设置有两套实现,前端走的是桩那一套 ——
小程序 pages/privacy-settings 调 /account/privacy-settings(AccountLifecycleService 桩,不落库),
管理端对隐私端点零调用。故本次修复位于「前端尚未触达」的路径:必要但当前用户不可感知。
真正的用户可见故障是「改隐私设置根本不保存」(桩),属 Task 3.1 上层,仍待契约裁定。
本次修复是该上层工作的必要前置 —— 桩一旦接到 PrivacyService 立刻会撞上此缺陷。
方法论:评估「某修复的价值」必须先确认前端实际调用哪条路径 ——
本项目同业务存在 /me/* 与 /account/* 多套并存端点,后端有真实实现 ≠ 用户能走到。
This commit is contained in:
+36
-14
@@ -1,6 +1,6 @@
|
|||||||
package com.danyang.match.platform.profile.service;
|
package com.danyang.match.platform.profile.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.danyang.match.platform.profile.mapper.UserPrivacySettingsMapper;
|
import com.danyang.match.platform.profile.mapper.UserPrivacySettingsMapper;
|
||||||
import com.danyang.match.platform.profile.entity.UserPrivacySettings;
|
import com.danyang.match.platform.profile.entity.UserPrivacySettings;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -45,22 +45,38 @@ public class PrivacyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新隐私设置
|
* 更新隐私设置(upsert 语义:存在则更新,不存在则插入)。
|
||||||
|
*
|
||||||
|
* <p><b>修复说明</b>:此前无论记录是否已存在都无条件走 {@code insert},
|
||||||
|
* 而 {@code user_privacy_settings} 的主键自 V002 起就是 {@code user_id},
|
||||||
|
* 因此同一用户<b>第二次</b>调用必然撞主键冲突 → 500,隐私设置从此再也改不动。
|
||||||
|
*
|
||||||
|
* <p><b>更新条件刻意用 {@code user_id} 而非实体 {@code id}</b>:该表的业务主键是
|
||||||
|
* {@code user_id};{@code id} 是 V037 为「实体↔表对齐」后补的独立列,
|
||||||
|
* 不作为寻址依据 —— 否则一旦某行 id 缺失(V037 回填前的历史行),更新会静默失效。
|
||||||
|
*
|
||||||
|
* <p><b>已知边界(未擅自处理)</b>:{@code findByUserId} 带 {@code deleted_at IS NULL},
|
||||||
|
* 若将来引入软删路径,已软删的行会让本方法走插入分支并撞主键。当前生产代码
|
||||||
|
* <b>不存在任何软删隐私设置的路径</b>,故该分支不可达;若将来新增软删,需同步改为
|
||||||
|
* 显式 upsert(如 PG 的 {@code ON CONFLICT (user_id) DO UPDATE})。
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public Map<String, Boolean> updatePrivacySettings(String userId, Map<String, Boolean> settings) {
|
public Map<String, Boolean> updatePrivacySettings(String userId, Map<String, Boolean> settings) {
|
||||||
UserPrivacySettings privacy = privacySettingsMapper.findByUserId(userId)
|
UserPrivacySettings existing = privacySettingsMapper.findByUserId(userId).orElse(null);
|
||||||
.orElseGet(() -> {
|
UserPrivacySettings privacy;
|
||||||
UserPrivacySettings p = new UserPrivacySettings();
|
|
||||||
p.setUserId(userId);
|
if (existing != null) {
|
||||||
p.setVisibleInRecommendations(true);
|
privacy = existing;
|
||||||
p.setVisibleInSearch(true);
|
} else {
|
||||||
p.setShowOccupation(true);
|
privacy = new UserPrivacySettings();
|
||||||
p.setShowLifestylePhotos(true);
|
privacy.setUserId(userId);
|
||||||
p.setReceiveActivityRecommendations(true);
|
privacy.setVisibleInRecommendations(true);
|
||||||
p.setAcceptMatchmakerRecommendations(true);
|
privacy.setVisibleInSearch(true);
|
||||||
return p;
|
privacy.setShowOccupation(true);
|
||||||
});
|
privacy.setShowLifestylePhotos(true);
|
||||||
|
privacy.setReceiveActivityRecommendations(true);
|
||||||
|
privacy.setAcceptMatchmakerRecommendations(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.containsKey("visibleInRecommendations")) privacy.setVisibleInRecommendations(settings.get("visibleInRecommendations"));
|
if (settings.containsKey("visibleInRecommendations")) privacy.setVisibleInRecommendations(settings.get("visibleInRecommendations"));
|
||||||
if (settings.containsKey("visibleInSearch")) privacy.setVisibleInSearch(settings.get("visibleInSearch"));
|
if (settings.containsKey("visibleInSearch")) privacy.setVisibleInSearch(settings.get("visibleInSearch"));
|
||||||
@@ -69,7 +85,13 @@ public class PrivacyService {
|
|||||||
if (settings.containsKey("receiveActivityRecommendations")) privacy.setReceiveActivityRecommendations(settings.get("receiveActivityRecommendations"));
|
if (settings.containsKey("receiveActivityRecommendations")) privacy.setReceiveActivityRecommendations(settings.get("receiveActivityRecommendations"));
|
||||||
if (settings.containsKey("acceptMatchmakerRecommendations")) privacy.setAcceptMatchmakerRecommendations(settings.get("acceptMatchmakerRecommendations"));
|
if (settings.containsKey("acceptMatchmakerRecommendations")) privacy.setAcceptMatchmakerRecommendations(settings.get("acceptMatchmakerRecommendations"));
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
privacySettingsMapper.update(privacy,
|
||||||
|
new LambdaUpdateWrapper<UserPrivacySettings>()
|
||||||
|
.eq(UserPrivacySettings::getUserId, userId));
|
||||||
|
} else {
|
||||||
privacySettingsMapper.insert(privacy);
|
privacySettingsMapper.insert(privacy);
|
||||||
|
}
|
||||||
return getPrivacySettings(userId);
|
return getPrivacySettings(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
package com.danyang.match.platform.profile;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.danyang.match.platform.profile.entity.UserPrivacySettings;
|
||||||
|
import com.danyang.match.platform.profile.mapper.UserPrivacySettingsMapper;
|
||||||
|
import com.danyang.match.platform.profile.service.PrivacyService;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐私设置 upsert 行为守卫。
|
||||||
|
*
|
||||||
|
* <p>守卫的缺陷:<b>同一用户第二次更新隐私设置必然 500</b>。
|
||||||
|
* {@code user_privacy_settings} 的主键自 V002 起就是 {@code user_id},
|
||||||
|
* 而旧实现无论记录是否已存在都无条件 {@code insert} → 第二次调用撞主键冲突。
|
||||||
|
*
|
||||||
|
* <p>为什么这个缺陷能长期存活:本机/测试环境的用户<b>从未改过隐私设置</b>,
|
||||||
|
* 该分支永远只被走到第一次;且测试库此前根本没有这张表(见 MapperBindingGuardTest)
|
||||||
|
* —— 与「Mapper 声明了却无实现」是同一类<b>空数据掩盖 + 表漂移</b>的双重盲区。
|
||||||
|
*
|
||||||
|
* <p>本测试全部走真实落库(H2),不用 Mockito 断言 mapper 调用次数:
|
||||||
|
* 只有真的插进去、再真的更新一遍,才能证明「第二次不炸」。
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class PrivacySettingsUpsertGuardTest {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PrivacyService privacyService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserPrivacySettingsMapper privacySettingsMapper;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUp() {
|
||||||
|
// 清理本类写入的全部行,避免污染其他测试(H2 是共享的同库同 schema)
|
||||||
|
privacySettingsMapper.delete(
|
||||||
|
new LambdaQueryWrapper<UserPrivacySettings>()
|
||||||
|
.likeRight(UserPrivacySettings::getUserId, "upsert-guard-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** user_id 列是 VARCHAR(36),故 id 不能带过长前缀。 */
|
||||||
|
private String newUserId() {
|
||||||
|
return "upsert-guard-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long rowCount(String userId) {
|
||||||
|
return privacySettingsMapper.selectCount(
|
||||||
|
new LambdaQueryWrapper<UserPrivacySettings>()
|
||||||
|
.eq(UserPrivacySettings::getUserId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("核心缺陷守卫:同一用户连续更新两次 —— 第二次不得抛异常,且不得产生第二行")
|
||||||
|
void repeatUpdateIsUpsertNotDuplicateInsert() {
|
||||||
|
String userId = newUserId();
|
||||||
|
|
||||||
|
Map<String, Boolean> first = new HashMap<>();
|
||||||
|
first.put("visibleInSearch", false);
|
||||||
|
Map<String, Boolean> firstResult = privacyService.updatePrivacySettings(userId, first);
|
||||||
|
assertFalse(firstResult.get("visibleInSearch"), "首次更新应生效");
|
||||||
|
assertEquals(1, rowCount(userId), "首次更新应恰好落库 1 行");
|
||||||
|
|
||||||
|
// === 修复前在此处抛主键冲突(DuplicateKeyException)→ 500 ===
|
||||||
|
Map<String, Boolean> second = new HashMap<>();
|
||||||
|
second.put("visibleInSearch", true);
|
||||||
|
Map<String, Boolean> secondResult = privacyService.updatePrivacySettings(userId, second);
|
||||||
|
|
||||||
|
assertTrue(secondResult.get("visibleInSearch"), "第二次更新应生效(修复前此处 500)");
|
||||||
|
assertEquals(1, rowCount(userId),
|
||||||
|
"两次更新后仍必须只有 1 行 —— 若变成 2 行,说明退化成「重复插入」,"
|
||||||
|
+ "后续 findByUserId 查出多行会让 Optional 返回值抛出 TooManyResultsException");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("部分更新不得把未提交的字段重置回默认值")
|
||||||
|
void partialUpdatePreservesPreviouslySetFields() {
|
||||||
|
String userId = newUserId();
|
||||||
|
|
||||||
|
Map<String, Boolean> turnOffSearch = new HashMap<>();
|
||||||
|
turnOffSearch.put("visibleInSearch", false);
|
||||||
|
privacyService.updatePrivacySettings(userId, turnOffSearch);
|
||||||
|
|
||||||
|
// 第二次只改另一个字段:先前关闭的 visibleInSearch 必须保持 false
|
||||||
|
Map<String, Boolean> turnOffPhotos = new HashMap<>();
|
||||||
|
turnOffPhotos.put("showLifestylePhotos", false);
|
||||||
|
Map<String, Boolean> result = privacyService.updatePrivacySettings(userId, turnOffPhotos);
|
||||||
|
|
||||||
|
assertFalse(result.get("showLifestylePhotos"), "本次提交的字段应生效");
|
||||||
|
assertFalse(result.get("visibleInSearch"),
|
||||||
|
"未提交的字段必须保持上次的值 —— 若被重置为 true,说明更新走了「重建默认值再 insert」的路径");
|
||||||
|
assertEquals(1, rowCount(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("首次更新只提交部分字段时,其余字段应取默认值 true")
|
||||||
|
void firstUpdateAppliesDefaultsForOmittedFields() {
|
||||||
|
String userId = newUserId();
|
||||||
|
|
||||||
|
Map<String, Boolean> only = new HashMap<>();
|
||||||
|
only.put("showOccupation", false);
|
||||||
|
Map<String, Boolean> result = privacyService.updatePrivacySettings(userId, only);
|
||||||
|
|
||||||
|
assertFalse(result.get("showOccupation"), "提交的字段应为 false");
|
||||||
|
assertTrue(result.get("visibleInRecommendations"), "未提交字段应取默认 true");
|
||||||
|
assertTrue(result.get("visibleInSearch"), "未提交字段应取默认 true");
|
||||||
|
assertTrue(result.get("showLifestylePhotos"), "未提交字段应取默认 true");
|
||||||
|
assertTrue(result.get("receiveActivityRecommendations"), "未提交字段应取默认 true");
|
||||||
|
assertTrue(result.get("acceptMatchmakerRecommendations"), "未提交字段应取默认 true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("无记录时读取返回全 true 默认值(固化既有契约,且不应落库)")
|
||||||
|
void readReturnsDefaultsWithoutPersisting() {
|
||||||
|
String userId = newUserId();
|
||||||
|
|
||||||
|
Map<String, Boolean> defaults = privacyService.getPrivacySettings(userId);
|
||||||
|
|
||||||
|
assertTrue(defaults.values().stream().allMatch(v -> v), "无记录时应返回全 true 默认值");
|
||||||
|
assertEquals(0, rowCount(userId), "读取操作不应产生副作用写入");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
| Phase 23 | ⚠️ **部分关闭** —— 44 个 `pass` 空壳已清、4 个真实 INV 行为守卫已补;**E2E 场景实现已完成(2026-09-19,见 Task 2.1)**;**INV-011 已补真实行为守卫(2026-09-19,见 Task 2.2,9 用例 + 两次注入验证)**。仅余 E2E 在 CI 上的首次实测绿 |
|
| Phase 23 | ⚠️ **部分关闭** —— 44 个 `pass` 空壳已清、4 个真实 INV 行为守卫已补;**E2E 场景实现已完成(2026-09-19,见 Task 2.1)**;**INV-011 已补真实行为守卫(2026-09-19,见 Task 2.2,9 用例 + 两次注入验证)**。仅余 E2E 在 CI 上的首次实测绿 |
|
||||||
| Phase 23B | ✅ E2E 测试基建重建(conftest + seed + CI e2e job,本机活栈 49 → **65 passed**) |
|
| Phase 23B | ✅ E2E 测试基建重建(conftest + seed + CI e2e job,本机活栈 49 → **65 passed**) |
|
||||||
| 横向加固 | ✅ HTTP 语义(105 处真实状态码)+ 身份收敛 + 幂等键(30 端点,V045/V046)+ 端口/入口守卫 |
|
| 横向加固 | ✅ HTTP 语义(105 处真实状态码)+ 身份收敛 + 幂等键(30 端点,V045/V046)+ 端口/入口守卫 |
|
||||||
| 后端回归 | **678 / 0 failures / 0 errors / 3 skipped**(2026-09-19 全量实测,含本轮新增 9 例 INV-011 守卫) |
|
| 后端回归 | **685 / 0 failures / 0 errors / 3 skipped**(2026-09-19 全量实测,含新增 9 例 INV-011 守卫 + 3 例 Mapper 绑定守卫 + 4 例隐私 upsert 守卫) |
|
||||||
| UI 契约门禁 | **18/18 绿**(原文 14/14 已过期,见 Global Constraints) |
|
| UI 契约门禁 | **18/18 绿**(原文 14/14 已过期,见 Global Constraints) |
|
||||||
| 停点 | **Phase 24(UAT)启动门前 —— 需用户确认** |
|
| 停点 | **Phase 24(UAT)启动门前 —— 需用户确认** |
|
||||||
|
|
||||||
@@ -259,6 +259,27 @@
|
|||||||
### Task 3.1: Phase 21 遗留
|
### Task 3.1: Phase 21 遗留
|
||||||
|
|
||||||
- [ ] **隐私设置接口**:`getPrivacySettings` / `updatePrivacySettings` 仍是 **TODO 桩**(返回假数据 / 不更新)→ 接真实 `user_privacy_settings`。
|
- [ ] **隐私设置接口**:`getPrivacySettings` / `updatePrivacySettings` 仍是 **TODO 桩**(返回假数据 / 不更新)→ 接真实 `user_privacy_settings`。
|
||||||
|
**2026-09-19 进展 —— 本项拆成两层,下层已修,上层仍待裁定:**
|
||||||
|
|
||||||
|
**① 下层(真实实现 `PrivacyService`)已修:upsert 缺陷** —— 无条件 `insert` 改为
|
||||||
|
「存在则按 `user_id` 更新,不存在才插入」。
|
||||||
|
- 缺陷:该表主键自 V002 起就是 `user_id`,旧实现无论记录是否存在都 `insert`
|
||||||
|
→ 同一用户**第二次**更新必然 `DuplicateKeyException` → 500,隐私设置从此再也改不动。
|
||||||
|
- 更新条件刻意用 `user_id` 而非实体 `id`:`id` 是 V037 为「实体↔表对齐」后补的独立列,
|
||||||
|
不作为寻址依据(否则历史行 id 缺失时更新会静默失效)。
|
||||||
|
- 守卫:`PrivacySettingsUpsertGuardTest`(4 用例,全走真实落库)。
|
||||||
|
负向注入已验证:退回无条件 `insert` → **恰 2 红**且失败原因即 `DuplicateKeyException`;
|
||||||
|
另 2 个只更新一次的用例**正确未红**(走不到该分支)。已还原并复跑全绿。
|
||||||
|
- 已知边界(**未擅自处理**):`findByUserId` 带 `deleted_at IS NULL`,若将来引入软删路径,
|
||||||
|
已软删的行会让本方法走插入分支并撞主键。当前**不存在任何软删隐私设置的路径**,故不可达;
|
||||||
|
若将来新增软删,需同步改为 PG `ON CONFLICT (user_id) DO UPDATE`。
|
||||||
|
|
||||||
|
**② 上层(桩)仍未接通,且这才是用户当前真正遇到的故障** —— 见附 B-13。
|
||||||
|
⚠️ **关键**:小程序 `pages/privacy-settings` 调的是 `/account/privacy-settings`
|
||||||
|
(走 `AccountLifecycleService` **桩**,不落库),**不是** `PrivacyService`。
|
||||||
|
管理端对隐私端点零调用。因此 ① 修的是「尚未被前端触达」的路径 ——
|
||||||
|
**必要但当前用户不可感知**。真正的用户可见故障是「改隐私设置根本不保存」。
|
||||||
|
① 是 ② 的**必要前置**:桩一旦接到 `PrivacyService`,立刻会撞上 upsert 缺陷。
|
||||||
- [ ] **注销红娘服务拦截硬编码 `0`**:
|
- [ ] **注销红娘服务拦截硬编码 `0`**:
|
||||||
原文断言「数据模型**没有 `matchmaker_services` 表**(全仓搜索为空),建表属扩权,需产品先确认」——
|
原文断言「数据模型**没有 `matchmaker_services` 表**(全仓搜索为空),建表属扩权,需产品先确认」——
|
||||||
**三条均与实况不符,2026-09-19 实测推翻**:
|
**三条均与实况不符,2026-09-19 实测推翻**:
|
||||||
@@ -341,7 +362,7 @@
|
|||||||
| Global Constraints | 后端改动跑离线容器 `mvn -o test` | **原文无误**;但「离线容器」的完整命令有两处**必挂**,否则会导出错误结论:`-v danyang-m2-cache:/root/.m2` + 挂**仓库根**并 `-w <repo>/backend`(附 B-9) |
|
| Global Constraints | 后端改动跑离线容器 `mvn -o test` | **原文无误**;但「离线容器」的完整命令有两处**必挂**,否则会导出错误结论:`-v danyang-m2-cache:/root/.m2` + 挂**仓库根**并 `-w <repo>/backend`(附 B-9) |
|
||||||
| 本文档自身(本轮中途两次误判) | 先写「`-o` 不可用」,后写「35 failures + 54 errors 是回归」 | **两条均误**:前者是漏挂 m2 卷(内置仓库 132MB 缺 `surefire-junit-platform`),后者是只挂 `backend/` 致相对路径失效。正确命令下全量 **678 / 0 / 0 / 3 全绿**(附 B-9) |
|
| 本文档自身(本轮中途两次误判) | 先写「`-o` 不可用」,后写「35 failures + 54 errors 是回归」 | **两条均误**:前者是漏挂 m2 卷(内置仓库 132MB 缺 `surefire-junit-platform`),后者是只挂 `backend/` 致相对路径失效。正确命令下全量 **678 / 0 / 0 / 3 全绿**(附 B-9) |
|
||||||
| Task 2.2 Step 3 | 「改 Java 代码需重建镜像,注入成本极高」 | 镜像 `danyang-match-backend-test` 内含 JDK 21 + Maven + 依赖缓存,**挂载源码即可跑 surefire**,一轮注入≈3.5 分钟(附 B-9) |
|
| Task 2.2 Step 3 | 「改 Java 代码需重建镜像,注入成本极高」 | 镜像 `danyang-match-backend-test` 内含 JDK 21 + Maven + 依赖缓存,**挂载源码即可跑 surefire**,一轮注入≈3.5 分钟(附 B-9) |
|
||||||
| 附 B-1(初稿) | 机构创建 500 因「从不给 `id` 赋值 → uuid 空串」 | **归因错误**。实测为 `violates check constraint "chk_org_status"`(status 被写成 `normal`);`id` 由 `AutoFillHandler` 补全,不会为空。已在 B-1 内留痕更正 |
|
| 附 B-11(上一轮自身更正) | 「表无 `id` 列 → `insert` 必崩」 | 更正方向**正确且本轮已实测确认**:V037 已补齐 `id`/`deleted_at`/权重列,真实缺陷是「对已存在记录也走 `insert`」→ 第二次更新撞 `user_id` 主键。负向注入复现的正是 `DuplicateKeyException`(附 B-13) |
|
||||||
|
|
||||||
### B. 新发现(不在本计划内,需另行处置)
|
### B. 新发现(不在本计划内,需另行处置)
|
||||||
|
|
||||||
@@ -406,7 +427,7 @@
|
|||||||
9. **本机跑 Java 测试的通道 —— 含两处「必挂」,漏掉任一处都会导出完全错误的结论**(工具性发现,可复用)
|
9. **本机跑 Java 测试的通道 —— 含两处「必挂」,漏掉任一处都会导出完全错误的结论**(工具性发现,可复用)
|
||||||
本机**无 JDK / 无 Maven / 无 `~/.m2`**(`mvn` 报 `Could not find Maven installation`),
|
本机**无 JDK / 无 Maven / 无 `~/.m2`**(`mvn` 报 `Could not find Maven installation`),
|
||||||
测试镜像 `danyang-match-backend-test` 内含 Temurin JDK 21 + Maven 3.9.16 + 完整源码。
|
测试镜像 `danyang-match-backend-test` 内含 Temurin JDK 21 + Maven 3.9.16 + 完整源码。
|
||||||
**正确命令**(2026-09-19 实测 `BUILD SUCCESS`,全量 **681 / 0 failures / 0 errors / 3 skipped**,约 6 分钟):
|
**正确命令**(2026-09-19 实测 `BUILD SUCCESS`,全量 **685 / 0 failures / 0 errors / 3 skipped**,约 6 分钟):
|
||||||
```bash
|
```bash
|
||||||
MSYS_NO_PATHCONV=1 docker run --rm \
|
MSYS_NO_PATHCONV=1 docker run --rm \
|
||||||
-v "E:/develop/公益婚恋服务平台":/proj \
|
-v "E:/develop/公益婚恋服务平台":/proj \
|
||||||
@@ -522,6 +543,25 @@
|
|||||||
- ⚠️ **并发写者两次覆盖编辑**(本轮实测):写入的 import 与用例修改在编辑完成后被回滚,
|
- ⚠️ **并发写者两次覆盖编辑**(本轮实测):写入的 import 与用例修改在编辑完成后被回滚,
|
||||||
导致"改动看起来在、实际跑的是旧版"。**教训:编辑后到跑测试之间必须重新核对文件内容**,
|
导致"改动看起来在、实际跑的是旧版"。**教训:编辑后到跑测试之间必须重新核对文件内容**,
|
||||||
且跑通后应尽快提交以缩短暴露窗口。
|
且跑通后应尽快提交以缩短暴露窗口。
|
||||||
|
|
||||||
|
13. **隐私设置有两套实现,前端走的是桩那一套**(High —— **决定了"修了有没有用"**)
|
||||||
|
|
||||||
|
| 路径 | Service | 状态 | 前端是否调用 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/account/privacy-settings`(GET/PUT) | `AccountLifecycleService` | **TODO 桩**:返回假数据 / update 只回 `updated:true`,**不落库** | ✅ **小程序在用** |
|
||||||
|
| `/me/privacy`、`/me/privacy-settings`、`/profile/privacy` | `PrivacyService` | 真实实现 | ❌ 无人调用(管理端对隐私端点**零调用**) |
|
||||||
|
|
||||||
|
- 实测依据:`miniapp/pages/privacy-settings/privacy-settings.js:21,48` 调
|
||||||
|
`/account/privacy-settings`;`admin-web/src` 全仓搜 `privacy` **零命中**。
|
||||||
|
- **因此用户当前遇到的真实故障是「改隐私设置根本不保存」**(桩),而非「第二次更新 500」。
|
||||||
|
本轮修的 upsert 缺陷位于**尚未被前端触达**的路径 —— **必要但当前不可感知**。
|
||||||
|
- **顺序关系**:upsert 修复是 Task 3.1 ① 的**必要前置** —— 桩一旦接到 `PrivacyService`,
|
||||||
|
立刻会撞上该缺陷。故先修它是对的。
|
||||||
|
- **推论**:评估"某修复的价值"时,必须先确认**前端实际调用哪条路径**。
|
||||||
|
本项目同一业务存在多套并存端点(`/me/*` 与 `/account/*` 的历史兼容写法),
|
||||||
|
只看后端有真实实现 ≠ 用户能走到。
|
||||||
|
- 待裁定项(Task 3.1 ① 上层)仍不变:键集以谁为准(表六列 / 小程序三字段 / 桩键集,
|
||||||
|
**仅 1 个字段交集**)—— 见附 B-11。
|
||||||
- **需要用户/产品三选一**:以表为准改造前端 UI / 以小程序为准补迁移加列 / 加一层映射。
|
- **需要用户/产品三选一**:以表为准改造前端 UI / 以小程序为准补迁移加列 / 加一层映射。
|
||||||
在此之前 Task 3.1 第 1 项**不应动手**。
|
在此之前 Task 3.1 第 1 项**不应动手**。
|
||||||
- 附带:`user_privacy_settings` / `user_preferences` / `review_reasons` / `review_items`
|
- 附带:`user_privacy_settings` / `user_preferences` / `review_reasons` / `review_items`
|
||||||
@@ -533,4 +573,4 @@
|
|||||||
*审阅校正: 2026-09-19 — 按实测推翻 9 处失实断言(见附 A;其中 2 处是**对本轮自查中自身结论的更正** ——
|
*审阅校正: 2026-09-19 — 按实测推翻 9 处失实断言(见附 A;其中 2 处是**对本轮自查中自身结论的更正** ——
|
||||||
「`-o` 不可用」「35 failures 是回归」两条均系命令/挂载所致,已如实留痕),登记 11 项新发现(见附 B)*
|
「`-o` 不可用」「35 failures 是回归」两条均系命令/挂载所致,已如实留痕),登记 11 项新发现(见附 B)*
|
||||||
*执行进展: 2026-09-19 — Task 2.1(E2E 16 场景)、Task 2.2(INV-011 守卫)均已完成并验证;
|
*执行进展: 2026-09-19 — Task 2.1(E2E 16 场景)、Task 2.2(INV-011 守卫)均已完成并验证;
|
||||||
后端全量回归 678 / 0 failures / 0 errors / 3 skipped;UI 契约门禁 18/18 绿*
|
后端全量回归 685 / 0 failures / 0 errors / 3 skipped;UI 契约门禁 18/18 绿*
|
||||||
|
|||||||
Reference in New Issue
Block a user