feat(privacy): 键集以小程序为准 —— V047 补列 + 接通 /account/privacy-settings 桩
产品裁定:隐私设置键集**以小程序为准**,用迁移补齐后端缺失列。
V047 新增 allow_activity_invitation(默认 TRUE)与 show_last_online(默认 FALSE)。
默认值**不对称**,与小程序 data 初值严格一致 —— showLastOnline 写成 TRUE 等于默认公开用户在线状态。
既有 6 列保留不动:UserProfileService 的资料详情 DTO 仍在全量返回它们(第 74-79 行),
那是仍在生效的独立读取出口。本迁移只做加法,不做替换。
AccountLifecycleService 的两个 TODO 桩改为委托 PrivacyService 真实读写。
这是**用户真正走的路径**(小程序调 /account/privacy-settings,而 /me/* 与 /profile/* 无人调用),
此前「保存成功」只是回 {updated:true} 而从不落库。
入参校验:已知键类型错误必须显式抛错,未知键宽容忽略 ——
若静默丢弃,用户会看到「保存成功」而设置从未生效,这类缺陷最难排查。
另容错 Boolean 的字符串形式("true"/"false")。
test(privacy): PrivacySettingsUpsertGuardTest 扩至 8 用例,其中 2 个是契约守卫
契约守卫**直接解析小程序源文件**的 PUT payload 与 data 块得出权威键集/默认值,
不抄契约副本(沿用 EntityMigrationParityTest 的纪律)—— 小程序改键即变红。
① 键集:后端返回键必须与小程序 PUT payload 键完全一致(不多不少)
② 默认值:后端默认值必须与小程序 data 初值一致(含 showLastOnline=false 这条不对称项)
③ 桩已接通:经 /account/privacy-settings 写入必须真落库且可读回
④ 已知键传错类型必须报错,不得静默丢弃
其余 4 个为 upsert 行为守卫(连续两次更新不炸且不产生第二行 / 部分更新不重置 / 默认值 / 读取无副作用)。
负向注入:把桩退回「不落库」→ 恰 2 红(③ 与 ④),其余 6 个正确未红。已还原并复跑全绿。
test(deployment): MigrationValidationTest 基线 46 → 47
新增迁移必须同步更新 LATEST_MIGRATION,否则全量回归报「版本号超出当前基线」。
该守卫同时断言「迁移文件总数 == 基线」(不允许缺号),是有效守卫 ——
正确做法是更新常量,不要为了让测试变绿而改断言方式。
This commit is contained in:
+45
-12
@@ -10,6 +10,7 @@ import com.danyang.match.platform.activity.mapper.ActivityRegistrationMapper;
|
||||
import com.danyang.match.platform.match.mapper.RecommendationRecordMapper;
|
||||
import com.danyang.match.platform.notification.service.NotificationService;
|
||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||
import com.danyang.match.platform.profile.service.PrivacyService;
|
||||
import com.danyang.match.platform.risk.mapper.ReportMapper;
|
||||
import com.danyang.match.platform.risk.mapper.RiskActionMapper;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
@@ -68,6 +69,7 @@ public class AccountLifecycleService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final UserProfileMapper userProfileMapper;
|
||||
private final PrivacyService privacyService;
|
||||
private final ActivityRegistrationMapper registrationMapper;
|
||||
private final RecommendationRecordMapper recommendationRecordMapper;
|
||||
private final ReportMapper reportMapper;
|
||||
@@ -342,26 +344,57 @@ public class AccountLifecycleService {
|
||||
// ==================== 隐私设置 ====================
|
||||
|
||||
/**
|
||||
* 获取隐私设置
|
||||
* 小程序 privacy-settings 页的契约键集(产品裁定:隐私设置键集<b>以小程序为准</b>)。
|
||||
* 用于入参校验:未知键宽容忽略,但<b>已知键类型错误必须显式报错</b> ——
|
||||
* 若静默丢弃,用户会看到"保存成功"而实际未生效,这类缺陷最难排查。
|
||||
*/
|
||||
private static final Set<String> PRIVACY_KEYS =
|
||||
Set.of("visibleInRecommendations", "allowActivityInvitation", "showLastOnline");
|
||||
|
||||
/**
|
||||
* 获取隐私设置。
|
||||
*
|
||||
* <p>原为 TODO 桩:永远返回写死的默认值、<b>不读库</b>,用户改过的设置读不回来。
|
||||
* 现委托 {@link PrivacyService} 读真实 {@code user_privacy_settings}。
|
||||
*/
|
||||
public Map<String, Object> getPrivacySettings(String userId) {
|
||||
// TODO: Phase 21 实现 user_privacy_settings 查询
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("visibleInRecommendations", true);
|
||||
settings.put("allowActivityInvitation", true);
|
||||
settings.put("showLastOnline", false);
|
||||
return settings;
|
||||
return new HashMap<>(privacyService.getPrivacySettings(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新隐私设置
|
||||
* 更新隐私设置。
|
||||
*
|
||||
* <p>原为 TODO 桩:只回 {@code {updated:true}}、<b>不落库</b>,
|
||||
* 用户看到"保存成功"后刷新即丢失。现委托 {@link PrivacyService} 真实 upsert。
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> updatePrivacySettings(String userId, Map<String, Object> settings) {
|
||||
// TODO: Phase 21 实现 user_privacy_settings 更新
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("updated", true);
|
||||
return result;
|
||||
return new HashMap<>(privacyService.updatePrivacySettings(userId, toBooleanMap(settings)));
|
||||
}
|
||||
|
||||
private Map<String, Boolean> toBooleanMap(Map<String, Object> settings) {
|
||||
Map<String, Boolean> typed = new HashMap<>();
|
||||
if (settings == null) {
|
||||
return typed;
|
||||
}
|
||||
for (Map.Entry<String, Object> e : settings.entrySet()) {
|
||||
String key = e.getKey();
|
||||
Object value = e.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Boolean b) {
|
||||
typed.put(key, b);
|
||||
} else if (value instanceof String s
|
||||
&& ("true".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s))) {
|
||||
// 容错:部分客户端会把 boolean 序列化成字符串
|
||||
typed.put(key, Boolean.valueOf(s));
|
||||
} else if (PRIVACY_KEYS.contains(key)) {
|
||||
throw new IllegalArgumentException("隐私设置字段类型错误: " + key);
|
||||
}
|
||||
// 未知键:宽容忽略
|
||||
}
|
||||
return typed;
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
|
||||
+5
@@ -17,4 +17,9 @@ public class UserPrivacySettings extends BaseEntity {
|
||||
private Boolean showLifestylePhotos;
|
||||
private Boolean receiveActivityRecommendations;
|
||||
private Boolean acceptMatchmakerRecommendations;
|
||||
|
||||
// V047:小程序 privacy-settings 页契约字段(产品裁定以小程序为准)。
|
||||
// 默认值与小程序 data 初值一致 —— showLastOnline 默认 false(不公开在线状态)。
|
||||
private Boolean allowActivityInvitation;
|
||||
private Boolean showLastOnline;
|
||||
}
|
||||
|
||||
+35
-27
@@ -17,31 +17,38 @@ public class PrivacyService {
|
||||
private final UserPrivacySettingsMapper privacySettingsMapper;
|
||||
|
||||
/**
|
||||
* 获取隐私设置
|
||||
* 获取隐私设置。
|
||||
*
|
||||
* <p><b>键集以小程序为准</b>(2026-09-19 产品裁定):
|
||||
* {@code visibleInRecommendations} / {@code allowActivityInvitation} / {@code showLastOnline}。
|
||||
* 后两列由 V047 补齐(此前后端表与小程序只有 {@code visibleInRecommendations} 一个交集)。
|
||||
*
|
||||
* <p>默认值与小程序 {@code data} 初值<b>严格一致</b>,尤其
|
||||
* {@code showLastOnline} 默认 <b>false</b>(不公开在线状态)—— 写成 true 等于默认公开。
|
||||
*
|
||||
* <p>注意:既有的 6 个列并未废弃 —— {@link com.danyang.match.platform.profile.service.UserProfileService}
|
||||
* 的资料详情 DTO 仍在全量返回它们。此处只是<b>隐私设置读写契约</b>以小程序为准。
|
||||
*/
|
||||
public Map<String, Boolean> getPrivacySettings(String userId) {
|
||||
return privacySettingsMapper.findByUserId(userId)
|
||||
.map(s -> {
|
||||
Map<String, Boolean> map = new HashMap<>();
|
||||
map.put("visibleInRecommendations", s.getVisibleInRecommendations());
|
||||
map.put("visibleInSearch", s.getVisibleInSearch());
|
||||
map.put("showOccupation", s.getShowOccupation());
|
||||
map.put("showLifestylePhotos", s.getShowLifestylePhotos());
|
||||
map.put("receiveActivityRecommendations", s.getReceiveActivityRecommendations());
|
||||
map.put("acceptMatchmakerRecommendations", s.getAcceptMatchmakerRecommendations());
|
||||
return map;
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
// 返回默认值
|
||||
Map<String, Boolean> defaults = new HashMap<>();
|
||||
defaults.put("visibleInRecommendations", true);
|
||||
defaults.put("visibleInSearch", true);
|
||||
defaults.put("showOccupation", true);
|
||||
defaults.put("showLifestylePhotos", true);
|
||||
defaults.put("receiveActivityRecommendations", true);
|
||||
defaults.put("acceptMatchmakerRecommendations", true);
|
||||
return defaults;
|
||||
});
|
||||
.map(this::toMiniappContract)
|
||||
.orElseGet(this::defaults);
|
||||
}
|
||||
|
||||
private Map<String, Boolean> toMiniappContract(UserPrivacySettings s) {
|
||||
Map<String, Boolean> map = new HashMap<>();
|
||||
map.put("visibleInRecommendations", s.getVisibleInRecommendations());
|
||||
map.put("allowActivityInvitation", s.getAllowActivityInvitation());
|
||||
map.put("showLastOnline", s.getShowLastOnline());
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Boolean> defaults() {
|
||||
Map<String, Boolean> map = new HashMap<>();
|
||||
map.put("visibleInRecommendations", true);
|
||||
map.put("allowActivityInvitation", true);
|
||||
map.put("showLastOnline", false);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,20 +77,21 @@ public class PrivacyService {
|
||||
} else {
|
||||
privacy = new UserPrivacySettings();
|
||||
privacy.setUserId(userId);
|
||||
// 既有 6 列:仍被 UserProfileService 资料详情 DTO 读取,保持默认 true
|
||||
privacy.setVisibleInRecommendations(true);
|
||||
privacy.setVisibleInSearch(true);
|
||||
privacy.setShowOccupation(true);
|
||||
privacy.setShowLifestylePhotos(true);
|
||||
privacy.setReceiveActivityRecommendations(true);
|
||||
privacy.setAcceptMatchmakerRecommendations(true);
|
||||
// V047 新增:小程序契约列(showLastOnline 默认 false)
|
||||
privacy.setAllowActivityInvitation(true);
|
||||
privacy.setShowLastOnline(false);
|
||||
}
|
||||
|
||||
if (settings.containsKey("visibleInRecommendations")) privacy.setVisibleInRecommendations(settings.get("visibleInRecommendations"));
|
||||
if (settings.containsKey("visibleInSearch")) privacy.setVisibleInSearch(settings.get("visibleInSearch"));
|
||||
if (settings.containsKey("showOccupation")) privacy.setShowOccupation(settings.get("showOccupation"));
|
||||
if (settings.containsKey("showLifestylePhotos")) privacy.setShowLifestylePhotos(settings.get("showLifestylePhotos"));
|
||||
if (settings.containsKey("receiveActivityRecommendations")) privacy.setReceiveActivityRecommendations(settings.get("receiveActivityRecommendations"));
|
||||
if (settings.containsKey("acceptMatchmakerRecommendations")) privacy.setAcceptMatchmakerRecommendations(settings.get("acceptMatchmakerRecommendations"));
|
||||
if (settings.containsKey("allowActivityInvitation")) privacy.setAllowActivityInvitation(settings.get("allowActivityInvitation"));
|
||||
if (settings.containsKey("showLastOnline")) privacy.setShowLastOnline(settings.get("showLastOnline"));
|
||||
|
||||
if (existing != null) {
|
||||
privacySettingsMapper.update(privacy,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- V047: 隐私设置补齐小程序端契约字段
|
||||
--
|
||||
-- 背景:小程序 pages/privacy-settings 使用的键集与后端 user_privacy_settings 长期不一致。
|
||||
-- 小程序(权威): visibleInRecommendations / allowActivityInvitation / showLastOnline
|
||||
-- 后端既有列 : visible_in_recommendations / visible_in_search / show_occupation /
|
||||
-- show_lifestyle_photos / receive_activity_recommendations /
|
||||
-- accept_matchmaker_recommendations
|
||||
-- 两者**仅有 visible_in_recommendations 一个交集**。
|
||||
--
|
||||
-- 产品裁定(2026-09-19):**以小程序为准**,用迁移补齐缺失列。
|
||||
--
|
||||
-- 既有 6 列**保留不动**:UserProfileService 的资料详情 DTO 仍在全量返回这 6 个键
|
||||
-- (UserProfileService.java:74-79),那是一条独立的、仍在生效的读取出口。
|
||||
-- 本迁移只做加法,不做替换。
|
||||
--
|
||||
-- ⚠️ 默认值不对称,必须保真(小程序 data 初值即如此):
|
||||
-- allow_activity_invitation 默认 TRUE
|
||||
-- show_last_online 默认 FALSE ← 不可写成 TRUE,否则等于默认公开在线状态
|
||||
|
||||
ALTER TABLE user_privacy_settings
|
||||
ADD COLUMN IF NOT EXISTS allow_activity_invitation BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
ALTER TABLE user_privacy_settings
|
||||
ADD COLUMN IF NOT EXISTS show_last_online BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
Reference in New Issue
Block a user