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} 的参数化语句:
*
* - 冲突在 JDBC 层被捕获,**不经过** Hibernate 日志体系;
* - 原始数据库异常与 message/cause **一律丢弃**,对外只返回结果码;
* - {@code users} 与 {@code user_roles} 在**同一事务**内提交 ——
* 角色写入失败时用户创建整体回滚;
* - 不打印 SQL、手机号或绑定参数。
*
*/
@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 仓储)。 */
}