Refactor authentication logic to unify user account handling and update database schema for user accounts
This commit is contained in:
111
docs/game.sql
111
docs/game.sql
@@ -12,82 +12,77 @@ SET NAMES utf8mb4;
|
||||
SET sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1) 管理员(平台侧)
|
||||
-- 1) 统一账户表(管理员/代理商共用)
|
||||
-- 用 user_type 区分:ADMIN | AGENT
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS admin_user (
|
||||
CREATE TABLE IF NOT EXISTS user_account (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
password_hash VARBINARY(100) NOT NULL, -- 建议存储 bcrypt/argon2 等
|
||||
role ENUM('SUPER','ADMIN') NOT NULL DEFAULT 'ADMIN',
|
||||
user_type ENUM('ADMIN','AGENT') NOT NULL,
|
||||
username VARCHAR(64) NOT NULL UNIQUE, -- 登录名(两类共用)
|
||||
display_name VARCHAR(100) NULL, -- 展示名(AGENT 可用)
|
||||
password_hash VARCHAR(120) NOT NULL, -- 建议存储 BCrypt(或临时 PLAIN:<pwd> 便于初始化)
|
||||
role ENUM('SUPER','ADMIN') NULL, -- 仅 ADMIN 使用
|
||||
status ENUM('ENABLED','DISABLED') NOT NULL DEFAULT 'ENABLED',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2) 代理商(商家)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS agent (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
login_account VARCHAR(64) NOT NULL UNIQUE,
|
||||
password_hash VARBINARY(100) NOT NULL,
|
||||
status ENUM('ENABLED','DISABLED') NOT NULL DEFAULT 'ENABLED',
|
||||
points_balance BIGINT UNSIGNED NOT NULL DEFAULT 0, -- 当前可用点数
|
||||
points_balance BIGINT UNSIGNED NOT NULL DEFAULT 0, -- 仅 AGENT 使用
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
CONSTRAINT chk_agent_points_nonneg CHECK (points_balance >= 0)
|
||||
CONSTRAINT chk_points_nonneg CHECK (points_balance >= 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 默认管理员账号(密码:admin7uqweh12)。
|
||||
-- 生产环境请尽快替换为 BCrypt 哈希;此处为 PLAIN 方便首次初始化。
|
||||
INSERT INTO user_account(user_type, username, display_name, password_hash, role, status, points_balance)
|
||||
VALUES ('ADMIN', 'admin', 'Super Admin', 'PLAIN:admin7uqweh12', 'SUPER', 'ENABLED', 0)
|
||||
ON DUPLICATE KEY UPDATE username = username;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3) 代理商点数流水
|
||||
-- 记录加点/扣点(生成链接时扣 Times×Quantity×BatchSize)
|
||||
-- 2) 代理商点数流水
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS agent_points_tx (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
agent_id BIGINT UNSIGNED NOT NULL,
|
||||
account_id BIGINT UNSIGNED NOT NULL, -- 指向 user_account(AGENT)
|
||||
type ENUM('ADD','DEDUCT') NOT NULL,
|
||||
before_points BIGINT UNSIGNED NOT NULL,
|
||||
delta_points BIGINT SIGNED NOT NULL, -- 可为负/正;与 type 对应
|
||||
delta_points BIGINT SIGNED NOT NULL, -- 可为正/负;与 type 对应
|
||||
after_points BIGINT UNSIGNED NOT NULL,
|
||||
reason ENUM('create_links','manual','refund_no_rollback','other') NOT NULL DEFAULT 'other',
|
||||
ref_id BIGINT UNSIGNED NULL, -- 可关联到 link_batch.id
|
||||
operator_id BIGINT UNSIGNED NULL, -- 操作者(管理员)
|
||||
operator_id BIGINT UNSIGNED NULL, -- 操作者(管理员,指向 user_account)
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
INDEX idx_agent_points_tx_agent_time (agent_id, created_at),
|
||||
CONSTRAINT fk_apx_agent FOREIGN KEY (agent_id) REFERENCES agent(id),
|
||||
CONSTRAINT fk_apx_operator FOREIGN KEY (operator_id) REFERENCES admin_user(id)
|
||||
INDEX idx_apx_account_time (account_id, created_at),
|
||||
CONSTRAINT fk_apx_account FOREIGN KEY (account_id) REFERENCES user_account(id),
|
||||
CONSTRAINT fk_apx_operator FOREIGN KEY (operator_id) REFERENCES user_account(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4) 链接批次(一次生成 N 个链接,按统一设置扣费)
|
||||
-- 3) 链接批次(一次生成 N 个链接,按统一设置扣费)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS link_batch (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
agent_id BIGINT UNSIGNED NOT NULL,
|
||||
agent_id BIGINT UNSIGNED NOT NULL, -- 指向 user_account(AGENT)
|
||||
quantity INT UNSIGNED NOT NULL, -- 每次奖励数量(如 50)
|
||||
times INT UNSIGNED NOT NULL, -- 重复执行次数(如 20)
|
||||
batch_size INT UNSIGNED NOT NULL, -- 本批生成链接数量(如 10)
|
||||
deduct_points BIGINT UNSIGNED NOT NULL, -- 扣点=quantity*times*batch_size
|
||||
operator_id BIGINT UNSIGNED NULL, -- 操作者(管理员或代理自己为空)
|
||||
operator_id BIGINT UNSIGNED NULL, -- 操作者(管理员)
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
INDEX idx_lb_agent_time (agent_id, created_at),
|
||||
CONSTRAINT chk_lb_quantity_pos CHECK (quantity > 0),
|
||||
CONSTRAINT chk_lb_times_pos CHECK (times > 0),
|
||||
CONSTRAINT chk_lb_batch_pos CHECK (batch_size > 0),
|
||||
CONSTRAINT fk_lb_agent FOREIGN KEY (agent_id) REFERENCES agent(id),
|
||||
CONSTRAINT fk_lb_operator FOREIGN KEY (operator_id) REFERENCES admin_user(id)
|
||||
CONSTRAINT fk_lb_agent FOREIGN KEY (agent_id) REFERENCES user_account(id),
|
||||
CONSTRAINT fk_lb_operator FOREIGN KEY (operator_id) REFERENCES user_account(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5) 单链接任务(用户访问的“加密链接”对应的实体)
|
||||
-- 4) 单链接任务(用户访问的“加密链接”对应的实体)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS link_task (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
batch_id BIGINT UNSIGNED NOT NULL,
|
||||
agent_id BIGINT UNSIGNED NOT NULL,
|
||||
agent_id BIGINT UNSIGNED NOT NULL, -- 指向 user_account(AGENT)
|
||||
code_no VARCHAR(32) NOT NULL, -- 后端生成的全局唯一编号
|
||||
token_hash CHAR(64) NOT NULL, -- 加密token的SHA-256十六进制(用于失效/撤销)
|
||||
token_hash CHAR(64) NOT NULL, -- 加密token的SHA-256(用于失效/撤销)
|
||||
expire_at DATETIME(3) NOT NULL, -- 链接有效期(默认 24h)
|
||||
status ENUM('NEW','USING','LOGGED_IN','REFUNDED','EXPIRED') NOT NULL DEFAULT 'NEW',
|
||||
region ENUM('Q','V') NULL, -- 选区;未选择前为 NULL
|
||||
@@ -103,11 +98,11 @@ CREATE TABLE IF NOT EXISTS link_task (
|
||||
INDEX idx_expire_at (expire_at),
|
||||
INDEX idx_created_at (created_at),
|
||||
CONSTRAINT fk_lt_batch FOREIGN KEY (batch_id) REFERENCES link_batch(id),
|
||||
CONSTRAINT fk_lt_agent FOREIGN KEY (agent_id) REFERENCES agent(id)
|
||||
CONSTRAINT fk_lt_agent FOREIGN KEY (agent_id) REFERENCES user_account(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6) 操作日志(审计/可观测性)
|
||||
-- 5) 操作日志(审计/可观测性)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
@@ -124,51 +119,15 @@ CREATE TABLE IF NOT EXISTS operation_log (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 7) 公告
|
||||
-- 6) 公告
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS announcement (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
content TEXT NOT NULL, -- 简单文本
|
||||
content TEXT NOT NULL, -- 富文本
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
jump_url VARCHAR(255) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
INDEX idx_announce_enabled (enabled)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 8) 平台级配置(商家不可配)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sys_config (
|
||||
cfg_key VARCHAR(64) PRIMARY KEY,
|
||||
cfg_value TEXT NOT NULL,
|
||||
description VARCHAR(255) NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 预置关键配置(可按需改值)
|
||||
INSERT INTO sys_config (cfg_key, cfg_value, description) VALUES
|
||||
('LINK_EXPIRE_HOURS', '24', '链接有效期(小时)'),
|
||||
('QR_EXPIRE_SECONDS', '60', '二维码过期秒数(脚本端未返回TTL时使用)'),
|
||||
('REFRESH_WAIT_SECONDS', '10', '刷新后强制等待秒数(商家不可配置)'),
|
||||
('MACHINE_COOLDOWN_MINUTES','10', '同一机器复用冷却时长(分钟)'),
|
||||
('DEFAULT_BATCH_SIZE', '10', '批量生成默认数量'),
|
||||
('SECOND_SCREEN_URL_TEMPLATE','https://你的域名/{codeNo}','二界面URL模板(包含编号占位)')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
cfg_value = VALUES(cfg_value),
|
||||
description = VALUES(description);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 推荐的视图 / 物化统计可按需追加(此处略)
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- =============================================================================
|
||||
-- 说明:
|
||||
-- 1) 生成链接时,请在业务层完成:
|
||||
-- - 计算扣点 = quantity * times * batch_size
|
||||
-- - 扣减 agent.points_balance,并写入 agent_points_tx
|
||||
-- - 写入 link_batch 及其下的若干 link_task(生成 code_no / token 及 token_hash、expire_at)
|
||||
-- 2) 用户端所有页面均以“加密链接 token”为入口,通过 token_hash 可实现失效/撤销。
|
||||
-- 3) operation_log.detail 建议仅存必要字段并脱敏;日志保留 90 天可通过定时归档或分区处理。
|
||||
-- =============================================================================
|
||||
|
||||
122
docs/开发文档.md
Normal file
122
docs/开发文档.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# 开发文档(Game Platform Server)
|
||||
|
||||
本项目为 Spring Boot 3 + WebFlux + MyBatis + MySQL 的后端服务。文档用于:
|
||||
- 统一开发风格与规范
|
||||
- 明确分层、目录结构与模块边界
|
||||
- 约定接口设计、异常与日志规范
|
||||
- 指导环境搭建、数据库迁移与交付流程
|
||||
- 便于新同学快速接手
|
||||
|
||||
## 技术栈与版本
|
||||
- Java 17、Maven 3.9+
|
||||
- Spring Boot 3.x(WebFlux、Actuator、Security)
|
||||
- MyBatis(JDBC,XML 映射)
|
||||
- MySQL 8+
|
||||
- JWT(JJWT 0.11.x)
|
||||
|
||||
## 目录结构(分层优先 layer-first)
|
||||
- `src/main/java/com/gameplatform/server`
|
||||
- `config`:全局配置(WebFlux、CORS、Jackson 等)
|
||||
- `security`:`SecurityConfig`、`JwtService`、认证授权
|
||||
- `exception`:全局异常处理、错误码
|
||||
- `common`:常量、工具、拦截器、审计
|
||||
- `model`
|
||||
- `entity`:与数据库表对应的实体(如 `entity.account.UserAccount`)
|
||||
- `dto`:请求/响应 DTO(如 `dto.auth.LoginRequest`)
|
||||
- `controller`:按模块子包(`auth`, `link`, `batch`, `points`, `announcement`, ...)
|
||||
- `service`:按模块子包(与 controller 对应)
|
||||
- `mapper`:按模块子包(接口 + XML)
|
||||
- `src/main/resources`
|
||||
- `mapper/**.xml`:MyBatis XML 映射
|
||||
- `application.yml`:多环境配置(建议扩展 `application-dev.yml` 等)
|
||||
- 可选:`db/migration`(建议引入 Flyway 管理 SQL)
|
||||
- `docs/`:需求、数据库结构、开发文档(本文件)
|
||||
|
||||
## 数据库与模型
|
||||
- 统一账户表:`user_account`(ADMIN/AGENT 共用,通过 `user_type` 区分)
|
||||
- 关键字段:`username` 唯一、`password_hash`(BCrypt 或初始化阶段 `PLAIN:<password>`)、`role`(ADMIN 用)、`points_balance`(AGENT 用)
|
||||
- 初始账户:`admin / admin7uqweh12`(以 `PLAIN:` 方式插入,部署后务必改为 BCrypt)
|
||||
- 相关表:`agent_points_tx`、`link_batch`、`link_task`、`operation_log`、`announcement`
|
||||
- SQL 参考:`docs/game.sql`
|
||||
- 建议:后续改为 Flyway 迁移(`V001__init.sql` 起步),避免手工执行 SQL
|
||||
|
||||
## 安全与认证
|
||||
- 登录:`POST /api/auth/login`(`userType=admin|agent` + `username` + `password`)
|
||||
- 自我信息:`GET /api/auth/me`(Authorization: Bearer <JWT>)
|
||||
- JWT:HS256;配置在 `security.jwt.*`(`application.yml`)
|
||||
- 密码:默认 `BCrypt`;兼容 `PLAIN:` 前缀以便初始化迁移
|
||||
|
||||
## API 设计规范
|
||||
- 路径:REST 风格、资源复数,如 `/api/links`、`/api/batches`
|
||||
- 状态码:201 创建、204 删除、400/401/403/404/409/429、500 异常
|
||||
- 统一返回(推荐):`{code, message, data, traceId}` 或直接返回资源对象(二选一并全局统一)
|
||||
- 分页:`page,size,sort`;返回 `{items,total,page,size}`;热点列表优先 keyset 分页
|
||||
- 幂等:写操作支持 `Idempotency-Key`
|
||||
- 速率限制:对轮询/二维码代理端点限流,返回 429
|
||||
|
||||
## 异常与日志
|
||||
- 全局异常:`exception/GlobalExceptionHandler` 映射为统一 JSON
|
||||
- 关联 ID:响应体与日志注入 `traceId`(落地可加 Filter/MDC)
|
||||
- 日志:结构化、脱敏(token/手机号/IP 末段)
|
||||
|
||||
## WebFlux + MyBatis 指南
|
||||
- MyBatis 基于 JDBC 阻塞;在 Service 层用 `Schedulers.boundedElastic()` 包裹
|
||||
- 事务在 Service;XML 使用 `resultMap` + `Base_Column_List`,避免 N+1
|
||||
- 命名:DB 下划线、Java 驼峰;启用 `map-underscore-to-camel-case`
|
||||
|
||||
## 开发流程与规范
|
||||
- 分支:`main`(稳定) / `feat/*`(功能) / `fix/*`(修复) / `chore/*`
|
||||
- 提交规范(建议):`type(scope): subject`,如 `feat(auth): add login api`
|
||||
- 代码风格:
|
||||
- Controller 瘦、Service 厚;禁止直接在 Controller 写 SQL
|
||||
- DTO 与 Entity 分离;禁止直接暴露 Entity 给前端
|
||||
- 单一职责、接口优先;跨模块通过 Service 接口交互
|
||||
- 注释清晰,公共逻辑放 `common`/工具类
|
||||
- 代码评审:PR 必须通过基本 CI 与至少 1 名评审
|
||||
|
||||
## 环境与配置
|
||||
- `application.yml`:数据库连接、`security.jwt.secret`(务必替换默认 secret)
|
||||
- 多环境:添加 `application-dev.yml`/`-prod.yml` 并通过 `SPRING_PROFILES_ACTIVE` 选择
|
||||
- 机密:使用环境变量/密钥管理器,不要提交到仓库
|
||||
|
||||
## 构建与运行
|
||||
- 本地:`mvn spring-boot:run`
|
||||
- Jar:`mvn clean package` → `java -jar target/*.jar`
|
||||
- Docker(建议后续补):多阶段构建 + 健康检查 + 环境变量注入
|
||||
|
||||
## 测试策略
|
||||
- 单元测试:Service 规则、扣点/状态机
|
||||
- 切片测试:`@WebFluxTest`(控制器)、`@MybatisTest`(Mapper)
|
||||
- 集成测试:Testcontainers MySQL 覆盖关键闭环(批次→扣点→链接→二维码→登录)
|
||||
|
||||
## 任务分解与进度追踪(示例)
|
||||
- V1 基础闭环
|
||||
- [ ] 统一返回体与错误码
|
||||
- [x] 登录与 JWT(admin/agent 合表)
|
||||
- [ ] 批次创建、扣点流水、链接生成
|
||||
- [ ] 二维码代理与状态轮询
|
||||
- [ ] 操作日志、公告 CRUD
|
||||
- V2 稳态与风控
|
||||
- [ ] 幂等、限流、黑名单/刷新令牌
|
||||
- [ ] 报表与导出、对账
|
||||
- V3 优化
|
||||
- [ ] 乐观锁/并发扣点优化、观测/告警
|
||||
|
||||
## 接口清单(节选)
|
||||
- `POST /api/auth/login`:登录获取 JWT
|
||||
- `GET /api/auth/me`:当前用户信息
|
||||
- `GET /api/link/{codeNo}`:查询链接元数据(待实现)
|
||||
- `POST /api/link/{codeNo}/select-region`:选择区服(待实现)
|
||||
- `GET /api/link/{token}/qr.png`:二维码代理(待实现)
|
||||
|
||||
## 上手步骤
|
||||
1) 配置数据库连接与 `security.jwt.secret`
|
||||
2) 执行 `docs/game.sql` 初始化库与默认管理员
|
||||
3) `mvn spring-boot:run` 启动服务
|
||||
4) 调用 `POST /api/auth/login` 获取 token,再访问 `GET /api/auth/me`
|
||||
|
||||
## 注意事项
|
||||
- 默认管理员以 `PLAIN:` 存储密码,仅用于初始化。上线前必须改为 BCrypt:
|
||||
- 方案:使用项目内 `BCryptPasswordEncoder` 生成哈希,更新 `user_account.password_hash`
|
||||
- 统一返回体与 RBAC 规则将在后续迭代落地
|
||||
|
||||
@@ -40,9 +40,8 @@ public class AuthController {
|
||||
put("userType", c.get("userType"));
|
||||
put("userId", c.get("userId"));
|
||||
put("username", c.get("username"));
|
||||
put("role", c.get("role"));
|
||||
if ("admin".equals(c.get("userType"))) put("role", c.get("role"));
|
||||
put("exp", c.getExpiration());
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.gameplatform.server.mapper.account;
|
||||
|
||||
import com.gameplatform.server.model.entity.account.UserAccount;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface UserAccountMapper {
|
||||
UserAccount findByUsernameAndType(@Param("username") String username,
|
||||
@Param("userType") String userType);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.gameplatform.server.mapper.admin;
|
||||
|
||||
import com.gameplatform.server.model.entity.admin.AdminUser;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface AdminUserMapper {
|
||||
AdminUser findByUsername(@Param("username") String username);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.gameplatform.server.mapper.agent;
|
||||
|
||||
import com.gameplatform.server.model.entity.agent.Agent;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
public interface AgentMapper {
|
||||
Agent findByLoginAccount(@Param("loginAccount") String loginAccount);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
package com.gameplatform.server.model.entity.admin;
|
||||
package com.gameplatform.server.model.entity.account;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class AdminUser {
|
||||
public class UserAccount {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String passwordHash;
|
||||
private String role; // SUPER / ADMIN
|
||||
private String userType; // ADMIN | AGENT
|
||||
private String username; // 登录名(admin/agent 共用)
|
||||
private String displayName; // 显示名称(agent 可用)
|
||||
private String passwordHash; // BCrypt 或 PLAIN:xxx(初始化用)
|
||||
private String role; // 仅 ADMIN 使用:SUPER / ADMIN
|
||||
private String status; // ENABLED / DISABLED
|
||||
private Long pointsBalance; // 仅 AGENT 使用
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getUserType() { return userType; }
|
||||
public void setUserType(String userType) { this.userType = userType; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getPasswordHash() { return passwordHash; }
|
||||
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public Long getPointsBalance() { return pointsBalance; }
|
||||
public void setPointsBalance(Long pointsBalance) { this.pointsBalance = pointsBalance; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.gameplatform.server.model.entity.agent;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class Agent {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String loginAccount;
|
||||
private String passwordHash;
|
||||
private String status; // ENABLED / DISABLED
|
||||
private Long pointsBalance;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getLoginAccount() { return loginAccount; }
|
||||
public void setLoginAccount(String loginAccount) { this.loginAccount = loginAccount; }
|
||||
public String getPasswordHash() { return passwordHash; }
|
||||
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public Long getPointsBalance() { return pointsBalance; }
|
||||
public void setPointsBalance(Long pointsBalance) { this.pointsBalance = pointsBalance; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.gameplatform.server.service.auth;
|
||||
|
||||
import com.gameplatform.server.mapper.admin.AdminUserMapper;
|
||||
import com.gameplatform.server.mapper.agent.AgentMapper;
|
||||
import com.gameplatform.server.mapper.account.UserAccountMapper;
|
||||
import com.gameplatform.server.model.dto.auth.LoginRequest;
|
||||
import com.gameplatform.server.model.dto.auth.LoginResponse;
|
||||
import com.gameplatform.server.model.entity.admin.AdminUser;
|
||||
import com.gameplatform.server.model.entity.agent.Agent;
|
||||
import com.gameplatform.server.model.entity.account.UserAccount;
|
||||
import com.gameplatform.server.security.JwtService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -16,78 +14,62 @@ import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
private final AdminUserMapper adminUserMapper;
|
||||
private final AgentMapper agentMapper;
|
||||
private final UserAccountMapper userAccountMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public AuthService(AdminUserMapper adminUserMapper,
|
||||
AgentMapper agentMapper,
|
||||
public AuthService(UserAccountMapper userAccountMapper,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JwtService jwtService) {
|
||||
this.adminUserMapper = adminUserMapper;
|
||||
this.agentMapper = agentMapper;
|
||||
this.userAccountMapper = userAccountMapper;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
public Mono<LoginResponse> login(LoginRequest req) {
|
||||
String userType = req.getUserType().toLowerCase();
|
||||
if ("admin".equals(userType)) {
|
||||
return Mono.fromCallable(() -> adminUserMapper.findByUsername(req.getUsername()))
|
||||
String userType = normalizeType(req.getUserType());
|
||||
return Mono.fromCallable(() -> userAccountMapper.findByUsernameAndType(req.getUsername(), userType))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(admin -> validateAdminPassword(admin, req.getPassword()));
|
||||
} else if ("agent".equals(userType)) {
|
||||
return Mono.fromCallable(() -> agentMapper.findByLoginAccount(req.getUsername()))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(agent -> validateAgentPassword(agent, req.getPassword()));
|
||||
.flatMap(acc -> validatePasswordAndBuild(acc, userType, req.getPassword()));
|
||||
}
|
||||
|
||||
private String normalizeType(String t) {
|
||||
if (t == null) return "";
|
||||
t = t.trim().toLowerCase();
|
||||
if ("admin".equals(t)) return "ADMIN";
|
||||
if ("agent".equals(t)) return "AGENT";
|
||||
throw new IllegalArgumentException("unsupported userType: " + t);
|
||||
}
|
||||
|
||||
private Mono<LoginResponse> validatePasswordAndBuild(UserAccount acc, String userType, String rawPwd) {
|
||||
if (acc == null || acc.getPasswordHash() == null) {
|
||||
return Mono.error(new IllegalArgumentException("用户名或密码错误"));
|
||||
}
|
||||
boolean ok;
|
||||
String hash = acc.getPasswordHash();
|
||||
if (hash.startsWith("$2a$") || hash.startsWith("$2b$") || hash.startsWith("$2y$")) {
|
||||
ok = passwordEncoder.matches(rawPwd, hash);
|
||||
} else if (hash.startsWith("PLAIN:")) {
|
||||
ok = rawPwd.equals(hash.substring(6));
|
||||
} else {
|
||||
return Mono.error(new IllegalArgumentException("unsupported userType: " + userType));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<LoginResponse> validateAdminPassword(AdminUser admin, String rawPassword) {
|
||||
if (admin == null || admin.getPasswordHash() == null) {
|
||||
return Mono.error(new IllegalArgumentException("用户名或密码错误"));
|
||||
}
|
||||
boolean ok = passwordEncoder.matches(rawPassword, admin.getPasswordHash());
|
||||
if (!ok) return Mono.error(new IllegalArgumentException("用户名或密码错误"));
|
||||
if (!"ENABLED".equalsIgnoreCase(admin.getStatus())) {
|
||||
if (!"ENABLED".equalsIgnoreCase(acc.getStatus())) {
|
||||
return Mono.error(new IllegalStateException("账户已禁用"));
|
||||
}
|
||||
|
||||
String token = jwtService.generateToken(
|
||||
"admin:" + admin.getId(),
|
||||
"admin", admin.getId(), admin.getUsername(), Map.of("role", admin.getRole())
|
||||
userType.toLowerCase() + ":" + acc.getId(),
|
||||
userType.toLowerCase(), acc.getId(), acc.getUsername(),
|
||||
userType.equals("ADMIN") ? Map.of("role", acc.getRole()) : Map.of("displayName", acc.getDisplayName())
|
||||
);
|
||||
LoginResponse resp = new LoginResponse();
|
||||
resp.setAccessToken(token);
|
||||
resp.setUserType("admin");
|
||||
resp.setUserId(admin.getId());
|
||||
resp.setUsername(admin.getUsername());
|
||||
resp.setExpiresIn(60L * 30); // align with default 30min
|
||||
return Mono.just(resp);
|
||||
}
|
||||
|
||||
private Mono<LoginResponse> validateAgentPassword(Agent agent, String rawPassword) {
|
||||
if (agent == null || agent.getPasswordHash() == null) {
|
||||
return Mono.error(new IllegalArgumentException("用户名或密码错误"));
|
||||
}
|
||||
boolean ok = passwordEncoder.matches(rawPassword, agent.getPasswordHash());
|
||||
if (!ok) return Mono.error(new IllegalArgumentException("用户名或密码错误"));
|
||||
if (!"ENABLED".equalsIgnoreCase(agent.getStatus())) {
|
||||
return Mono.error(new IllegalStateException("账户已禁用"));
|
||||
}
|
||||
String token = jwtService.generateToken(
|
||||
"agent:" + agent.getId(),
|
||||
"agent", agent.getId(), agent.getLoginAccount(), Map.of("name", agent.getName())
|
||||
);
|
||||
LoginResponse resp = new LoginResponse();
|
||||
resp.setAccessToken(token);
|
||||
resp.setUserType("agent");
|
||||
resp.setUserId(agent.getId());
|
||||
resp.setUsername(agent.getLoginAccount());
|
||||
resp.setUserType(userType.toLowerCase());
|
||||
resp.setUserId(acc.getId());
|
||||
resp.setUsername(acc.getUsername());
|
||||
resp.setExpiresIn(60L * 30);
|
||||
return Mono.just(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,4 +37,3 @@
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gameplatform.server.mapper.admin.AdminUserMapper">
|
||||
<resultMap id="AdminUserMap" type="com.gameplatform.server.model.entity.admin.AdminUser">
|
||||
<mapper namespace="com.gameplatform.server.mapper.account.UserAccountMapper">
|
||||
<resultMap id="UserAccountMap" type="com.gameplatform.server.model.entity.account.UserAccount">
|
||||
<id property="id" column="id" />
|
||||
<result property="userType" column="user_type" />
|
||||
<result property="username" column="username" />
|
||||
<result property="displayName" column="display_name" />
|
||||
<result property="passwordHash" column="password_hash" />
|
||||
<result property="role" column="role" />
|
||||
<result property="status" column="status" />
|
||||
<result property="pointsBalance" column="points_balance" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
<result property="updatedAt" column="updated_at" />
|
||||
</resultMap>
|
||||
|
||||
<select id="findByUsername" parameterType="string" resultMap="AdminUserMap">
|
||||
SELECT id, username, password_hash, role, status, created_at, updated_at
|
||||
FROM admin_user
|
||||
<select id="findByUsernameAndType" resultMap="UserAccountMap">
|
||||
SELECT id, user_type, username, display_name, password_hash, role, status, points_balance, created_at, updated_at
|
||||
FROM user_account
|
||||
WHERE username = #{username}
|
||||
AND user_type = #{userType}
|
||||
LIMIT 1
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gameplatform.server.mapper.agent.AgentMapper">
|
||||
<resultMap id="AgentMap" type="com.gameplatform.server.model.entity.agent.Agent">
|
||||
<id property="id" column="id" />
|
||||
<result property="name" column="name" />
|
||||
<result property="loginAccount" column="login_account" />
|
||||
<result property="passwordHash" column="password_hash" />
|
||||
<result property="status" column="status" />
|
||||
<result property="pointsBalance" column="points_balance" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
<result property="updatedAt" column="updated_at" />
|
||||
</resultMap>
|
||||
|
||||
<select id="findByLoginAccount" parameterType="string" resultMap="AgentMap">
|
||||
SELECT id, name, login_account, password_hash, status, points_balance, created_at, updated_at
|
||||
FROM agent
|
||||
WHERE login_account = #{loginAccount}
|
||||
LIMIT 1
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -37,4 +37,3 @@
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gameplatform.server.mapper.admin.AdminUserMapper">
|
||||
<resultMap id="AdminUserMap" type="com.gameplatform.server.model.entity.admin.AdminUser">
|
||||
<id property="id" column="id" />
|
||||
<result property="username" column="username" />
|
||||
<result property="passwordHash" column="password_hash" />
|
||||
<result property="role" column="role" />
|
||||
<result property="status" column="status" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
<result property="updatedAt" column="updated_at" />
|
||||
</resultMap>
|
||||
|
||||
<select id="findByUsername" parameterType="string" resultMap="AdminUserMap">
|
||||
SELECT id, username, password_hash, role, status, created_at, updated_at
|
||||
FROM admin_user
|
||||
WHERE username = #{username}
|
||||
LIMIT 1
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gameplatform.server.mapper.agent.AgentMapper">
|
||||
<resultMap id="AgentMap" type="com.gameplatform.server.model.entity.agent.Agent">
|
||||
<id property="id" column="id" />
|
||||
<result property="name" column="name" />
|
||||
<result property="loginAccount" column="login_account" />
|
||||
<result property="passwordHash" column="password_hash" />
|
||||
<result property="status" column="status" />
|
||||
<result property="pointsBalance" column="points_balance" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
<result property="updatedAt" column="updated_at" />
|
||||
</resultMap>
|
||||
|
||||
<select id="findByLoginAccount" parameterType="string" resultMap="AgentMap">
|
||||
SELECT id, name, login_account, password_hash, status, points_balance, created_at, updated_at
|
||||
FROM agent
|
||||
WHERE login_account = #{loginAccount}
|
||||
LIMIT 1
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user