package com.mydreamjob.auth; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.support.TransactionTemplate; import java.util.List; import java.util.Optional; /** * S03 定向整改:管理员账号创建的**独立事务组件**。 * *

为什么不用 JPA/Hibernate:并发初始化时数据库唯一约束会成为正常并发控制手段, * 而 Hibernate 的 {@code SqlExceptionHelper} 会在业务代码捕获异常**之前**以 ERROR 级别 * 记录整条语句与绑定值 —— 管理员手机号因此进入生产可见日志。

* *

本组件改用 {@link JdbcTemplate} 的参数化语句:

* */ @Component public class AdminBootstrapCreator { /** 创建结果:成功创建 / 数据库唯一约束冲突。 */ public enum CreateResult { CREATED, CONFLICT } /** 引导账号的最小状态视图(不含任何敏感字段输出)。 */ public record BootstrapUser(Long id, String role, boolean active, boolean verified, boolean passwordAbsent, List roles) { } private final JdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; /** * 使用**独立 REQUIRES_NEW 事务**:冲突必须只回滚本次创建,不得把调用方的外层事务 * 标记为 rollback-only(否则调用方提交时会抛 {@code UnexpectedRollbackException})。 */ public AdminBootstrapCreator(JdbcTemplate jdbcTemplate, org.springframework.transaction.PlatformTransactionManager transactionManager) { this.jdbcTemplate = jdbcTemplate; this.transactionTemplate = new TransactionTemplate(transactionManager); this.transactionTemplate.setPropagationBehavior( org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW); } /** * 在同一事务内写入 {@code users} 与 {@code user_roles}。 * * @return {@link CreateResult#CREATED} 或 {@link CreateResult#CONFLICT}; * 冲突时不抛出任何携带原始数据库信息的异常 */ public CreateResult createAdmin(String phone, String nickname) { try { transactionTemplate.executeWithoutResult(status -> { jdbcTemplate.update( "INSERT INTO users (phone, nickname, password_hash, role, is_active, is_verified, " + "created_at, updated_at) VALUES (?, ?, NULL, 'ADMIN', 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", phone, nickname); Long userId = jdbcTemplate.queryForObject( "SELECT id FROM users WHERE phone = ?", Long.class, phone); jdbcTemplate.update("INSERT INTO user_roles (user_id, role) VALUES (?, 'ADMIN')", userId); }); return CreateResult.CREATED; } catch (DataAccessException e) { // 唯一约束冲突(并发初始化)、角色写入失败或任何 JDBC 层失败:事务已整体回滚。 // 刻意**丢弃**原始异常:它的 message 可能包含 SQL 与绑定值(例如手机号)。 // 调用方据此重新查询,决定是「幂等成功」还是抛出不带 cause 的固定安全异常。 return CreateResult.CONFLICT; } } /** 按手机号读取引导所需的最小状态;只读、参数化,不经过 Hibernate。 */ public Optional findByPhone(String phone) { List rows = jdbcTemplate.query( "SELECT id, role, is_active, is_verified, password_hash FROM users WHERE phone = ?", (rs, i) -> new BootstrapUser( rs.getLong("id"), rs.getString("role"), rs.getInt("is_active") == 1, rs.getInt("is_verified") == 1, rs.getString("password_hash") == null, List.of()), phone); if (rows.isEmpty()) { return Optional.empty(); } BootstrapUser base = rows.get(0); List roles = jdbcTemplate.queryForList( "SELECT role FROM user_roles WHERE user_id = ?", String.class, base.id()); return Optional.of(new BootstrapUser(base.id(), base.role(), base.active(), base.verified(), base.passwordAbsent(), roles)); } /** 是否存在活动 ADMIN(不涉及手机号,可用 JPA 仓储)。 */ }