Skip to content

feat(user-center): 用户偏好读写 API + preferences JSONB 列#7

Merged
longsizhuo merged 3 commits into
mainfrom
feature/user-preferences
Apr 14, 2026
Merged

feat(user-center): 用户偏好读写 API + preferences JSONB 列#7
longsizhuo merged 3 commits into
mainfrom
feature/user-preferences

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

Summary

  • user_accounts 表新增 preferences JSONB NOT NULL DEFAULT '{}'(schema.sql ALTER IF NOT EXISTS 幂等)
  • 新增 GET /api/user-center/preferencesPATCH /api/user-center/preferences@SaCheckLogin 保护
  • PATCH 做顶层 merge(后端 Java 侧 read-merge-write 兼容 H2 测试环境)
  • 新增集成测试:未登录 401、多次 PATCH 合并正确

偏好 schema(软约定,后端不强校验,前端可自由扩展)

```
{ theme?: "light"|"dark"|"system", language?: "zh"|"en", aiDefaultProvider?: "intern"|"openai"|"gemini" }
```

Test plan

  • `./mvnw test` 全过(已知 OAuthCallback 失败与本 PR 无关)
  • curl 端到端:登录拿 token → GET(初始 {})→ PATCH {theme:dark,language:zh} → GET 持久化

依赖

前端 PR: involutionhell#feature/user-preferences

Copilot AI review requested due to automatic review settings April 14, 2026 18:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 为用户中心新增“用户偏好”读写能力:在 user_accounts 表引入 preferences(生产用 JSONB,测试用 VARCHAR),并提供受登录保护的 GET/PATCH 接口,PATCH 以“顶层 key merge”方式更新偏好,同时补充集成测试覆盖匿名访问与多次合并更新。

Changes:

  • 数据库:为 user_accounts 增加 preferences 列(生产 schema.sql 用 JSONB + 幂等 ALTER;测试 test-schema.sql 用 VARCHAR)。
  • API:新增 GET /api/user-center/preferencesPATCH /api/user-center/preferences,并在 service/repository 增加偏好读写方法。
  • 测试:新增偏好接口集成测试,并更新既有测试以适配 UserAccount 新增字段。

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/main/resources/schema.sql 通过幂等 ALTER 为生产库新增 preferences JSONB
src/test/resources/test-schema.sql 为 H2 测试库的 user_accounts 增加 preferences
src/main/java/com/involutionhell/backend/usercenter/controller/UserPreferencesController.java 新增偏好 GET/PATCH Controller(Sa-Token 登录保护)
src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java 增加偏好读取与 PATCH merge(read-merge-write)逻辑
src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java 扩展仓库接口:find/update preferences
src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java JDBC 实现:preferences 的查询/更新 + JSON 序列化/反序列化
src/main/java/com/involutionhell/backend/usercenter/model/UserAccount.java UserAccount 增加 preferences 字段并在构造器中初始化
src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java 新建 GitHub 用户时补齐 preferences 构造参数
src/test/java/com/involutionhell/backend/usercenter/controller/UserPreferencesControllerIntegrationTests.java 新增偏好接口集成测试(401 + 合并更新正确)
src/test/java/com/involutionhell/backend/usercenter/service/AuthServiceTests.java 适配 UserAccount 构造参数新增字段
src/test/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepositoryTests.java 适配 UserAccount 构造参数新增字段
src/test/java/com/involutionhell/backend/usercenter/model/UserAccountTests.java 适配 UserAccount 构造参数新增字段

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +195 to +217
/**
* 将 JSON 字符串解析为 Map,null 或解析失败时返回空 Map。
*/
private Map<String, Object> parseJson(String json) {
if (json == null || json.isBlank() || "{}".equals(json.trim())) {
return new HashMap<>();
}
try {
return objectMapper.readValue(json, MAP_TYPE);
} catch (Exception e) {
return new HashMap<>();
}
}

/**
* 将 Map 序列化为 JSON 字符串。
*/
private String toJson(Map<String, Object> map) {
try {
return objectMapper.writeValueAsString(map);
} catch (Exception e) {
return "{}";
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseJson/toJson 在解析或序列化失败时直接返回空对象/"{}",会把“数据库里有脏数据”或“请求体含不可序列化值”等问题静默吞掉;随后一次 PATCH 很可能把原偏好整体覆盖成 {},造成数据丢失且难以排查。建议至少记录日志并抛出异常(让调用方返回 500),或显式区分“未设置”和“解析失败”两类情况,避免静默清空。

Copilot uses AI. Check for mistakes.
Comment on lines 24 to +28
*/
public UserAccount {
roles = normalizeSet(roles);
permissions = normalizeSet(permissions);
preferences = preferences != null ? preferences : Map.of();
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserAccountpreferences 仅在为 null 时用 Map.of() 初始化;当传入的是可变 Map(例如从 JDBC/JSON 解析得到的 HashMap)时会直接被记录持有引用,破坏 record 的“快照/不可变”语义,也可能被外部修改导致状态漂移。建议在 compact constructor 里做防御性拷贝并不可变化(如 preferences = preferences == null ? Map.of() : Map.copyOf(preferences),必要时对嵌套结构另行约束)。

Suggested change
*/
public UserAccount {
roles = normalizeSet(roles);
permissions = normalizeSet(permissions);
preferences = preferences != null ? preferences : Map.of();
* 同时对偏好做防御性拷贝避免持有外部可变 Map 引用
*/
public UserAccount {
roles = normalizeSet(roles);
permissions = normalizeSet(permissions);
preferences = preferences == null ? Map.of() : Map.copyOf(preferences);

Copilot uses AI. Check for mistakes.
Comment on lines 84 to 122
userAccount.githubId()
userAccount.githubId(),
Map.of()
);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insert 当前没有写入 preferences 列(依赖数据库默认值),但返回的 UserAccount 也固定构造为 Map.of(),会忽略调用方在 userAccount.preferences() 里传入的任何初始偏好;后续如果有创建用户时携带偏好的场景,这里会静默丢失。建议:要么在 INSERT 显式包含 preferences 并使用 toJson(userAccount.preferences()) 写入;要么在插入后 findById 回读整行,确保返回值与数据库一致并避免遗漏新列。

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +101
public Map<String, Object> patchPreferences(Long userId, Map<String, Object> patch) {
// 先读出现有偏好,再在 Java 侧合并,最后整体写回(兼容 H2 测试环境)
Map<String, Object> existing = userAccountRepository.findPreferences(userId);
Map<String, Object> merged = new HashMap<>(existing);
merged.putAll(patch);
return userAccountRepository.updatePreferences(userId, merged);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patchPreferences 当前采用 read-merge-write,但没有事务/行锁或数据库端原子 merge;并发 PATCH(尤其是不同 key)会出现 lost update,后写入者可能覆盖掉先写入者的更新,导致偏好静默丢失。建议改为在 PostgreSQL 侧用单条 UPDATE 做 jsonb 顶层合并(如 preferences = preferences || :patch / RETURNING preferences),或在事务中对该行 SELECT ... FOR UPDATE 再合并写回。

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +168
jdbc.update(connection -> {
var ps = connection.prepareStatement(
"UPDATE user_accounts SET preferences = ? WHERE id = ?");
// PostgreSQL 连接时用 PGobject 传 jsonb 类型;H2 等直接用 String
String driverName = connection.getMetaData().getDriverName();
if (driverName != null && driverName.toLowerCase().contains("postgresql")) {
try {
var pgObjectClass = Class.forName("org.postgresql.util.PGobject");
var pgObject = pgObjectClass.getDeclaredConstructor().newInstance();
pgObjectClass.getMethod("setType", String.class).invoke(pgObject, "jsonb");
pgObjectClass.getMethod("setValue", String.class).invoke(pgObject, mergedJson);
ps.setObject(1, pgObject);
} catch (Exception e) {
ps.setString(1, mergedJson);
}
} else {
ps.setString(1, mergedJson);
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatePreferences 通过反射创建 org.postgresql.util.PGobject 来写 jsonb;在 native-image/GraalVM 下该反射通常会失败(未注册 runtime hints),随后 fallback 到 ps.setString 可能导致 PostgreSQL 报类型不匹配(varchar → jsonb),从而让偏好更新在生产/原生镜像环境不可用。建议避免反射:在 PostgreSQL 分支用 ps.setObject(1, mergedJson, java.sql.Types.OTHER) 或把 SQL 写成 preferences = ?::jsonb(仅在 Postgres 分支使用),确保无需反射且类型稳定。

Copilot uses AI. Check for mistakes.
Copilot CR #7(多项):
- UserAccount: preferences 用 Map.copyOf 做防御性拷贝,保证 record 的不可变快照语义
- JdbcUserAccountRepository.insert: INSERT 写入 preferences 列,插入后 findById 回读,
  避免初始偏好被丢 + 字段漂移
- JdbcUserAccountRepository.patchPreferences: 改名自 updatePreferences,
  PostgreSQL 路径用 'preferences || ?::jsonb' 单条 UPDATE 原子合并,setObject + Types.OTHER
  替代反射 PGobject,兼容 GraalVM native image;H2 测试路径保留 read-merge-write
- JdbcUserAccountRepository.parseJson/toJson: 失败时抛异常 + log.error,
  不再静默吞掉把偏好覆盖成 '{}'
- UserAccountRepository: 接口改名 updatePreferences → patchPreferences,写清合并语义
- UserCenterService: 删 Java 侧 read-merge-write,直接交给 repository 原子操作
@longsizhuo longsizhuo merged commit fcbf6a2 into main Apr 14, 2026
@longsizhuo longsizhuo deleted the feature/user-preferences branch April 14, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants