11package com .involutionhell .backend .usercenter .repository ;
22
3+ import tools .jackson .core .type .TypeReference ;
4+ import tools .jackson .databind .ObjectMapper ;
35import com .involutionhell .backend .usercenter .model .UserAccount ;
46
57import java .sql .PreparedStatement ;
8+ import java .sql .Types ;
69import java .util .Arrays ;
10+ import java .util .HashMap ;
711import java .util .HashSet ;
812import java .util .List ;
13+ import java .util .Map ;
914import java .util .Optional ;
1015import java .util .Set ;
16+ import org .slf4j .Logger ;
17+ import org .slf4j .LoggerFactory ;
1118import org .springframework .jdbc .core .JdbcTemplate ;
1219import org .springframework .jdbc .core .RowMapper ;
1320import org .springframework .jdbc .support .GeneratedKeyHolder ;
2027@ Repository
2128public class JdbcUserAccountRepository implements UserAccountRepository {
2229
30+ private static final Logger log = LoggerFactory .getLogger (JdbcUserAccountRepository .class );
31+
2332 private final JdbcTemplate jdbc ;
33+ private final ObjectMapper objectMapper ;
34+
35+ private static final TypeReference <Map <String , Object >> MAP_TYPE = new TypeReference <>() {};
2436
2537 /**
2638 * 将数据库行映射为 UserAccount 记录。
2739 * roles / permissions 以逗号分隔字符串存储,空字符串对应空集合。
40+ * preferences 存为 JSONB(测试 H2 用 VARCHAR),读出后解析为 Map。
2841 */
29- private static final RowMapper <UserAccount > ROW_MAPPER = (rs , rowNum ) -> new UserAccount (
42+ private final RowMapper <UserAccount > rowMapper = (rs , rowNum ) -> new UserAccount (
3043 rs .getLong ("id" ),
3144 rs .getString ("username" ),
3245 rs .getString ("password_hash" ),
@@ -36,30 +49,32 @@ public class JdbcUserAccountRepository implements UserAccountRepository {
3649 parseSet (rs .getString ("permissions" )),
3750 rs .getString ("avatar_url" ),
3851 rs .getString ("email" ),
39- rs .getObject ("github_id" , Long .class ) // nullable Long
52+ rs .getObject ("github_id" , Long .class ),
53+ parseJson (rs .getString ("preferences" ))
4054 );
4155
42- public JdbcUserAccountRepository (JdbcTemplate jdbc ) {
56+ public JdbcUserAccountRepository (JdbcTemplate jdbc , ObjectMapper objectMapper ) {
4357 this .jdbc = jdbc ;
58+ this .objectMapper = objectMapper ;
4459 }
4560
4661 @ Override
4762 public Optional <UserAccount > findById (Long id ) {
4863 List <UserAccount > results = jdbc .query (
49- "SELECT * FROM user_accounts WHERE id = ?" , ROW_MAPPER , id );
64+ "SELECT * FROM user_accounts WHERE id = ?" , rowMapper , id );
5065 return results .stream ().findFirst ();
5166 }
5267
5368 @ Override
5469 public Optional <UserAccount > findByUsername (String username ) {
5570 List <UserAccount > results = jdbc .query (
56- "SELECT * FROM user_accounts WHERE username = ?" , ROW_MAPPER , username );
71+ "SELECT * FROM user_accounts WHERE username = ?" , rowMapper , username );
5772 return results .stream ().findFirst ();
5873 }
5974
6075 @ Override
6176 public List <UserAccount > findAll () {
62- return jdbc .query ("SELECT * FROM user_accounts ORDER BY id" , ROW_MAPPER );
77+ return jdbc .query ("SELECT * FROM user_accounts ORDER BY id" , rowMapper );
6378 }
6479
6580 @ Override
@@ -74,8 +89,11 @@ public UserAccount updateAuthorization(Long userId, Set<String> roles, Set<Strin
7489 @ Override
7590 public UserAccount insert (UserAccount userAccount ) {
7691 KeyHolder keyHolder = new GeneratedKeyHolder ();
77- String sql = "INSERT INTO user_accounts (username, password_hash, display_name, enabled, roles, permissions, avatar_url, email, github_id) " +
78- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" ;
92+ // 把 preferences 一并写入 INSERT,避免创建用户时携带的初始偏好被丢弃
93+ String sql = "INSERT INTO user_accounts (username, password_hash, display_name, enabled, roles, permissions, avatar_url, email, github_id, preferences) " +
94+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" ;
95+
96+ String prefsJson = toJson (userAccount .preferences ());
7997
8098 jdbc .update (connection -> {
8199 PreparedStatement ps = connection .prepareStatement (sql , new String []{"id" });
@@ -89,6 +107,8 @@ public UserAccount insert(UserAccount userAccount) {
89107 ps .setString (8 , userAccount .email ());
90108 // github_id 可为 null,用 setObject 处理
91109 ps .setObject (9 , userAccount .githubId ());
110+ // jsonb 用 Types.OTHER 让 PostgreSQL 驱动自行识别;H2 会当作字符串处理
111+ ps .setObject (10 , prefsJson , Types .OTHER );
92112 return ps ;
93113 }, keyHolder );
94114
@@ -97,18 +117,9 @@ public UserAccount insert(UserAccount userAccount) {
97117 throw new IllegalStateException ("插入用户失败,无法获取生成的 ID" );
98118 }
99119
100- return new UserAccount (
101- key .longValue (),
102- userAccount .username (),
103- userAccount .passwordHash (),
104- userAccount .displayName (),
105- userAccount .enabled (),
106- userAccount .roles (),
107- userAccount .permissions (),
108- userAccount .avatarUrl (),
109- userAccount .email (),
110- userAccount .githubId ()
111- );
120+ // 插入后回读整行,确保返回值与数据库一致,避免遗漏新列或字段漂移
121+ return findById (key .longValue ())
122+ .orElseThrow (() -> new IllegalStateException ("插入用户后无法读取回数据: id=" + key .longValue ()));
112123 }
113124
114125 @ Override
@@ -121,6 +132,72 @@ public UserAccount updateProfile(Long userId, String displayName, String avatarU
121132 .orElseThrow (() -> new IllegalArgumentException ("用户不存在: " + userId ));
122133 }
123134
135+ @ Override
136+ public Map <String , Object > findPreferences (Long userId ) {
137+ List <String > results = jdbc .query (
138+ "SELECT preferences FROM user_accounts WHERE id = ?" ,
139+ (rs , rn ) -> rs .getString ("preferences" ),
140+ userId );
141+ if (results .isEmpty ()) {
142+ throw new IllegalArgumentException ("用户不存在: " + userId );
143+ }
144+ return parseJson (results .get (0 ));
145+ }
146+
147+ @ Override
148+ public Map <String , Object > patchPreferences (Long userId , Map <String , Object > patch ) {
149+ // 直接在 DB 端做原子 merge,避免 Java 侧 read-merge-write 的并发 lost update;
150+ // 同时用 setObject + Types.OTHER,避开反射 PGobject 在 GraalVM native image 下
151+ // reflection hints 未注册导致的启动失败。
152+ //
153+ // PostgreSQL:UPDATE ... SET preferences = preferences || ?::jsonb
154+ // 使用 jsonb 原生 `||` 操作符做顶层 key 合并,单条语句原子完成
155+ // H2(测试环境):UPDATE ... SET preferences = ? (全量覆盖)
156+ // 测试环境不追求并发正确性,由 service 层先 read-merge-write 保证合并语义
157+ String patchJson = toJson (patch );
158+ boolean isPostgres = isPostgres ();
159+
160+ if (isPostgres ) {
161+ int updated = jdbc .update (connection -> {
162+ var ps = connection .prepareStatement (
163+ "UPDATE user_accounts SET preferences = preferences || ?::jsonb WHERE id = ?" );
164+ ps .setObject (1 , patchJson , Types .OTHER );
165+ ps .setLong (2 , userId );
166+ return ps ;
167+ });
168+ if (updated == 0 ) {
169+ throw new IllegalArgumentException ("用户不存在: " + userId );
170+ }
171+ } else {
172+ // H2 路径:先读后合并再整体写入(测试环境无并发压力)
173+ Map <String , Object > existing = findPreferences (userId );
174+ Map <String , Object > merged = new HashMap <>(existing );
175+ merged .putAll (patch );
176+ String mergedJson = toJson (merged );
177+ int updated = jdbc .update (
178+ "UPDATE user_accounts SET preferences = ? WHERE id = ?" ,
179+ mergedJson , userId );
180+ if (updated == 0 ) {
181+ throw new IllegalArgumentException ("用户不存在: " + userId );
182+ }
183+ }
184+
185+ return findPreferences (userId );
186+ }
187+
188+ /** 判断当前数据源是否为 PostgreSQL(通过驱动名识别)。 */
189+ private boolean isPostgres () {
190+ try {
191+ return Boolean .TRUE .equals (jdbc .execute ((java .sql .Connection c ) -> {
192+ String name = c .getMetaData ().getDriverName ();
193+ return name != null && name .toLowerCase ().contains ("postgresql" );
194+ }));
195+ } catch (Exception e ) {
196+ log .warn ("检测数据源驱动失败,按非 PostgreSQL 兜底: {}" , e .getMessage ());
197+ return false ;
198+ }
199+ }
200+
124201 /**
125202 * 将逗号分隔字符串解析为集合,空串返回空集合。
126203 */
@@ -140,4 +217,34 @@ private static String joinSet(Set<String> values) {
140217 }
141218 return String .join ("," , values );
142219 }
143- }
220+
221+ /**
222+ * 将 JSON 字符串解析为 Map。
223+ * null / 空串 / "{}" 视为"未设置"返回空 Map;解析失败(数据库里脏数据)则抛出异常,
224+ * 避免静默吞错,让调用方感知并由全局异常处理器返回 500。
225+ */
226+ private Map <String , Object > parseJson (String json ) {
227+ if (json == null || json .isBlank () || "{}" .equals (json .trim ())) {
228+ return new HashMap <>();
229+ }
230+ try {
231+ return objectMapper .readValue (json , MAP_TYPE );
232+ } catch (Exception e ) {
233+ log .error ("解析 preferences JSON 失败,数据可能已损坏: {}" , json , e );
234+ throw new IllegalStateException ("解析 preferences 失败" , e );
235+ }
236+ }
237+
238+ /**
239+ * 将 Map 序列化为 JSON 字符串。
240+ * 失败时抛出异常而不是返回 "{}",避免把有问题的偏好当成空偏好静默覆盖掉原有数据。
241+ */
242+ private String toJson (Map <String , Object > map ) {
243+ try {
244+ return objectMapper .writeValueAsString (map );
245+ } catch (Exception e ) {
246+ log .error ("序列化 preferences 失败: {}" , map , e );
247+ throw new IllegalStateException ("序列化 preferences 失败" , e );
248+ }
249+ }
250+ }
0 commit comments