first commit
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
18
.idea/compiler.xml
generated
Normal file
18
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="server" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="server" options="-parameters -parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
20
.idea/jarRepositories.xml
generated
Normal file
20
.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
14
.idea/misc.xml
generated
Normal file
14
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="graalvm-jdk-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Game Platform Server (Spring Boot WebFlux + MyBatis + MySQL)
|
||||
|
||||
A minimal backend scaffold using Spring Boot 3, WebFlux, MyBatis, and MySQL. Includes a sample `User` CRUD implemented via MyBatis (blocking JDBC) safely wrapped in reactive APIs.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Spring Boot 3 (WebFlux, Actuator)
|
||||
- MyBatis Spring Boot Starter (JDBC)
|
||||
- MySQL Connector/J
|
||||
- Java 17
|
||||
|
||||
## Blocking JDBC with WebFlux
|
||||
|
||||
MyBatis uses JDBC which is blocking. To keep WebFlux event loop non-blocking, all data access is offloaded to `Schedulers.boundedElastic()` (see `UserService`). This is a common and safe pattern when you need WebFlux endpoints but must use JDBC/MyBatis.
|
||||
|
||||
## Project Layout
|
||||
|
||||
- `pom.xml` – Maven build, dependencies
|
||||
- `src/main/resources/application.yml` – datasource + mybatis config
|
||||
- `src/main/resources/mapper/*.xml` – MyBatis mappers (XML)
|
||||
- `src/main/resources/schema.sql` – initial table
|
||||
- `src/main/java/com/gameplatform/server` – application code
|
||||
|
||||
## Configure Database
|
||||
|
||||
1. Create database:
|
||||
```sql
|
||||
CREATE DATABASE game_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
```
|
||||
2. Update `spring.datasource.*` in `application.yml` with your MySQL credentials.
|
||||
3. Run the SQL init:
|
||||
```sql
|
||||
USE game_platform;
|
||||
-- then execute contents of src/main/resources/schema.sql
|
||||
```
|
||||
|
||||
## Build & Run
|
||||
|
||||
Requires JDK 17+ and Maven 3.9+.
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
The app starts on `http://localhost:8080`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /actuator/health` – health check
|
||||
- `GET /api/users` – list users
|
||||
- `GET /api/users/{id}` – get user by id
|
||||
- `POST /api/users` – create user
|
||||
- body:
|
||||
```json
|
||||
{"username": "alice", "email": "alice@example.com"}
|
||||
```
|
||||
- `DELETE /api/users/{id}` – delete user
|
||||
|
||||
## Notes
|
||||
|
||||
- If you need true end-to-end reactive IO, consider R2DBC instead of JDBC/MyBatis. Here we keep MyBatis for mapping convenience and use bounded elastic threads to avoid blocking the event loop.
|
||||
|
||||
174
docs/game.sql
Normal file
174
docs/game.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
-- =============================================================================
|
||||
-- 上号系统 - 数据库结构 (MySQL 8+)
|
||||
-- =============================================================================
|
||||
|
||||
-- 可选:创建并使用独立库
|
||||
CREATE DATABASE IF NOT EXISTS login_task_db
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_0900_ai_ci;
|
||||
USE login_task_db;
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1) 管理员(平台侧)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS admin_user (
|
||||
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',
|
||||
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, -- 当前可用点数
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3) 代理商点数流水
|
||||
-- 记录加点/扣点(生成链接时扣 Times×Quantity×BatchSize)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS agent_points_tx (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
agent_id BIGINT UNSIGNED NOT NULL,
|
||||
type ENUM('ADD','DEDUCT') NOT NULL,
|
||||
before_points BIGINT UNSIGNED NOT NULL,
|
||||
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, -- 操作者(管理员)
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4) 链接批次(一次生成 N 个链接,按统一设置扣费)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS link_batch (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
agent_id BIGINT UNSIGNED NOT NULL,
|
||||
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, -- 操作者(管理员或代理自己为空)
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5) 单链接任务(用户访问的“加密链接”对应的实体)
|
||||
-- -----------------------------------------------------------------------------
|
||||
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,
|
||||
code_no VARCHAR(32) NOT NULL, -- 后端生成的全局唯一编号
|
||||
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
|
||||
machine_id VARCHAR(64) NULL, -- 绑定的脚本端机器编号
|
||||
login_at DATETIME(3) NULL,
|
||||
refund_at DATETIME(3) NULL,
|
||||
revoked_at DATETIME(3) 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),
|
||||
UNIQUE KEY uk_code_no (code_no),
|
||||
UNIQUE KEY uk_token_hash (token_hash),
|
||||
INDEX idx_agent_status (agent_id, status),
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6) 操作日志(审计/可观测性)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS operation_log (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
actor_type ENUM('admin','agent','system','user') NOT NULL,
|
||||
actor_id BIGINT UNSIGNED NULL, -- 不强制外键,避免多态复杂度
|
||||
code_no VARCHAR(32) NULL,
|
||||
op VARCHAR(64) NOT NULL, -- 如:create_links / refund / select_region / create_qr / poll_login / release_machine
|
||||
detail JSON NULL, -- 具体参数/返回(注意脱敏)
|
||||
client_ip VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(255) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
INDEX idx_log_code_time (code_no, created_at),
|
||||
INDEX idx_log_time (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 7) 公告
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS announcement (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
title VARCHAR(100) 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 天可通过定时归档或分区处理。
|
||||
-- =============================================================================
|
||||
21
docs/需求文档.md
Normal file
21
docs/需求文档.md
Normal file
@@ -0,0 +1,21 @@
|
||||
明白✅
|
||||
|
||||
1. 二界面 URL 模板
|
||||
我已把配置改成:**`域名/{codeNo}`**(文档已更新为 `https://你的域名/{codeNo}`,按运行环境替换域名即可)。
|
||||
|
||||
2. 关于 `http://36.138.184.60:12345/编号/二维码.png`
|
||||
这个是“**脚本端**”暴露出来的**静态图片**地址——**正常情况下会直接返回 PNG 图片**。如果你在页面里看不到图,常见原因有:
|
||||
|
||||
* **混合内容被拦截**:你的前端如果是 **HTTPS**,而这张图是 **HTTP**,浏览器会拦截(安全策略)。
|
||||
解决:给 `36.138.184.60:12345` 配 **HTTPS**,或让后端提供一个 **HTTPS 代理端点**(例如:`GET /api/link/{token}/qr.png`)去转发这张图片,前端只请求自己的 HTTPS 域名即可。(我已在文档里加了这条注意事项)
|
||||
* **二维码尚未生成 / 已过期**:脚本端还没把 `二维码.png` 写到对应 `编号/` 目录,或已被轮转清理 → 会 404 或显示旧图。
|
||||
解决:确保先触发“生成二维码”动作,并用返回的 `createdAt/expireAt` 做本地倒计时;必要时加 `?t=时间戳` 防缓存。
|
||||
* **编号不匹配**:前端用的 `编号` 与实际分配的机器编号不同。
|
||||
解决:以后端返回的 `machineId/编号` 为准。
|
||||
* **端口/防火墙**:12345 端口未对外放通或临时不可达。
|
||||
解决:主机/网关放行该端口并做健康检查。
|
||||
* **跨域非问题**:`<img>` 加载图片不受 CORS 限制,但若被混合内容拦截,同样会显示不出。
|
||||
|
||||
如果你愿意最省心的做法:我们在后端新增一个**图片代理接口**(HTTPS),前端统一拿这个接口的 URL,当脚本端换 IP/端口或做 HTTPS,这边都不用改前端。
|
||||
|
||||
需要的话我可以顺手把这个代理端点的 Spring Boot 代码骨架也给你(带缓存/超时/错误降级)。
|
||||
122
pom.xml
Normal file
122
pom.xml
Normal file
@@ -0,0 +1,122 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.3</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.gameplatform</groupId>
|
||||
<artifactId>server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>gameplatform-server</name>
|
||||
<description>Spring Boot WebFlux + MyBatis + MySQL backend</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<mybatis.spring.boot.version>3.0.3</mybatis.spring.boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Reactive web stack -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Security for authentication/JWT protection -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis starter (JDBC, blocking) -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${mybatis.spring.boot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL JDBC driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation for request DTOs -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Actuator (health/info) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT (JJWT) -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok (optional) -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.gameplatform.server;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.gameplatform.server.mapper")
|
||||
public class GamePlatformServerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GamePlatformServerApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gameplatform.server.controller;
|
||||
|
||||
import com.gameplatform.server.model.User;
|
||||
import com.gameplatform.server.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<User> getById(@PathVariable Long id) {
|
||||
return userService.getById(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<User> listAll() {
|
||||
return userService.listAll();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public Mono<User> create(@Valid @RequestBody User user) {
|
||||
return userService.create(user);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public Mono<Void> delete(@PathVariable Long id) {
|
||||
return userService.deleteById(id)
|
||||
.filter(Boolean::booleanValue)
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.gameplatform.server.controller.auth;
|
||||
|
||||
import com.gameplatform.server.model.dto.auth.LoginRequest;
|
||||
import com.gameplatform.server.model.dto.auth.LoginResponse;
|
||||
import com.gameplatform.server.security.JwtService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final com.gameplatform.server.service.auth.AuthService authService;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public AuthController(com.gameplatform.server.service.auth.AuthService authService, JwtService jwtService) {
|
||||
this.authService = authService;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Mono<LoginResponse> login(@Valid @RequestBody LoginRequest req) {
|
||||
return authService.login(req);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public Mono<Object> me(@RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) {
|
||||
String token = authorization != null && authorization.startsWith("Bearer ") ? authorization.substring(7) : authorization;
|
||||
return Mono.fromCallable(() -> jwtService.parse(token))
|
||||
.map(this::claimsToMe);
|
||||
}
|
||||
|
||||
private Object claimsToMe(Claims c) {
|
||||
return new java.util.LinkedHashMap<>() {{
|
||||
put("userType", c.get("userType"));
|
||||
put("userId", c.get("userId"));
|
||||
put("username", c.get("username"));
|
||||
put("role", c.get("role"));
|
||||
put("exp", c.getExpiration());
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.gameplatform.server.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Object handleBadRequest(IllegalArgumentException e) {
|
||||
return body(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public Object handleForbidden(IllegalStateException e) {
|
||||
return body(HttpStatus.FORBIDDEN.value(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Object handleOther(Exception e) {
|
||||
return body(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误");
|
||||
}
|
||||
|
||||
private Object body(int code, String message) {
|
||||
return new LinkedHashMap<>() {{
|
||||
put("code", code);
|
||||
put("message", message);
|
||||
put("timestamp", Instant.now().toString());
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
17
src/main/java/com/gameplatform/server/mapper/UserMapper.java
Normal file
17
src/main/java/com/gameplatform/server/mapper/UserMapper.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.gameplatform.server.mapper;
|
||||
|
||||
import com.gameplatform.server.model.User;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface UserMapper {
|
||||
User findById(@Param("id") Long id);
|
||||
|
||||
List<User> findAll();
|
||||
|
||||
int insert(User user);
|
||||
|
||||
int deleteById(@Param("id") Long id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
}
|
||||
|
||||
51
src/main/java/com/gameplatform/server/model/User.java
Normal file
51
src/main/java/com/gameplatform/server/model/User.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.gameplatform.server.model;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class User {
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
private String username;
|
||||
|
||||
@Email
|
||||
private String email;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.gameplatform.server.model.dto.auth;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class LoginRequest {
|
||||
// userType: admin | agent
|
||||
@NotBlank
|
||||
private String userType;
|
||||
@NotBlank
|
||||
private String username; // admin: username, agent: loginAccount
|
||||
@NotBlank
|
||||
private String password;
|
||||
|
||||
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 getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gameplatform.server.model.dto.auth;
|
||||
|
||||
public class LoginResponse {
|
||||
private String tokenType = "Bearer";
|
||||
private String accessToken;
|
||||
private long expiresIn; // seconds
|
||||
private String userType; // admin | agent
|
||||
private Long userId;
|
||||
private String username;
|
||||
|
||||
public String getTokenType() { return tokenType; }
|
||||
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
|
||||
public String getAccessToken() { return accessToken; }
|
||||
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
|
||||
public long getExpiresIn() { return expiresIn; }
|
||||
public void setExpiresIn(long expiresIn) { this.expiresIn = expiresIn; }
|
||||
public String getUserType() { return userType; }
|
||||
public void setUserType(String userType) { this.userType = userType; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gameplatform.server.model.entity.admin;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class AdminUser {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String passwordHash;
|
||||
private String role; // SUPER / ADMIN
|
||||
private String status; // ENABLED / DISABLED
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
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 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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.gameplatform.server.security;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JwtService {
|
||||
|
||||
private final SecretKey key;
|
||||
private final long accessTokenMinutes;
|
||||
|
||||
public JwtService(@Value("${security.jwt.secret}") String secret,
|
||||
@Value("${security.jwt.access-token-minutes:30}") long accessTokenMinutes) {
|
||||
// accept raw text secret; if base64 provided, still works with Decoders
|
||||
byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes();
|
||||
this.key = Keys.hmacShaKeyFor(bytes);
|
||||
this.accessTokenMinutes = accessTokenMinutes;
|
||||
}
|
||||
|
||||
public String generateToken(String subject, String userType, Long userId, String username, Map<String, Object> extra) {
|
||||
Instant now = Instant.now();
|
||||
var builder = Jwts.builder()
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(Date.from(now))
|
||||
.setExpiration(Date.from(now.plus(accessTokenMinutes, ChronoUnit.MINUTES)))
|
||||
.claim("userType", userType)
|
||||
.claim("userId", userId)
|
||||
.claim("username", username);
|
||||
if (extra != null) {
|
||||
extra.forEach(builder::claim);
|
||||
}
|
||||
return builder.signWith(key, SignatureAlgorithm.HS256).compact();
|
||||
}
|
||||
|
||||
public io.jsonwebtoken.Claims parse(String token) {
|
||||
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gameplatform.server.security;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||
return http
|
||||
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
||||
.cors(ServerHttpSecurity.CorsSpec::disable)
|
||||
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
||||
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
||||
.authorizeExchange(ex -> ex
|
||||
.pathMatchers("/actuator/**").permitAll()
|
||||
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
|
||||
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
|
||||
.anyExchange().permitAll() // 其他接口后续再收紧
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.gameplatform.server.service;
|
||||
|
||||
import com.gameplatform.server.mapper.UserMapper;
|
||||
import com.gameplatform.server.model.User;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public UserService(UserMapper userMapper) {
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
public Mono<User> getById(Long id) {
|
||||
return Mono.fromCallable(() -> userMapper.findById(id))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
public Flux<User> listAll() {
|
||||
return Mono.fromCallable(userMapper::findAll)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMapMany(Flux::fromIterable);
|
||||
}
|
||||
|
||||
public Mono<User> create(User user) {
|
||||
return Mono.fromCallable(() -> {
|
||||
userMapper.insert(user);
|
||||
return user;
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public Mono<Boolean> deleteById(Long id) {
|
||||
return Mono.fromCallable(() -> userMapper.deleteById(id) > 0)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.gameplatform.server.service.auth;
|
||||
|
||||
import com.gameplatform.server.mapper.admin.AdminUserMapper;
|
||||
import com.gameplatform.server.mapper.agent.AgentMapper;
|
||||
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.security.JwtService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
private final AdminUserMapper adminUserMapper;
|
||||
private final AgentMapper agentMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
|
||||
public AuthService(AdminUserMapper adminUserMapper,
|
||||
AgentMapper agentMapper,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JwtService jwtService) {
|
||||
this.adminUserMapper = adminUserMapper;
|
||||
this.agentMapper = agentMapper;
|
||||
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()))
|
||||
.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()));
|
||||
} else {
|
||||
return Mono.error(new IllegalArgumentException("unsupported userType: " + userType));
|
||||
}
|
||||
}
|
||||
|
||||
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())) {
|
||||
return Mono.error(new IllegalStateException("账户已禁用"));
|
||||
}
|
||||
String token = jwtService.generateToken(
|
||||
"admin:" + admin.getId(),
|
||||
"admin", admin.getId(), admin.getUsername(), Map.of("role", admin.getRole())
|
||||
);
|
||||
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.setExpiresIn(60L * 30);
|
||||
return Mono.just(resp);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/main/resources/application.yml
Normal file
39
src/main/resources/application.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
spring:
|
||||
application:
|
||||
name: gameplatform-server
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/login_task_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
|
||||
mybatis:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
type-aliases-package: com.gameplatform.server.model
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
server:
|
||||
port: 18080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
com.gameplatform.server: debug
|
||||
|
||||
security:
|
||||
jwt:
|
||||
secret: "change-this-secret-to-a-long-random-string-please"
|
||||
access-token-minutes: 30
|
||||
refresh-token-days: 7
|
||||
40
src/main/resources/mapper/UserMapper.xml
Normal file
40
src/main/resources/mapper/UserMapper.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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.UserMapper">
|
||||
|
||||
<resultMap id="UserResultMap" type="com.gameplatform.server.model.User">
|
||||
<id property="id" column="id" />
|
||||
<result property="username" column="username" />
|
||||
<result property="email" column="email" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, username, email, created_at
|
||||
</sql>
|
||||
|
||||
<select id="findById" parameterType="long" resultMap="UserResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM users
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="findAll" resultMap="UserResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM users
|
||||
ORDER BY id DESC
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.gameplatform.server.model.User" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO users (username, email, created_at)
|
||||
VALUES (#{username}, #{email}, NOW())
|
||||
</insert>
|
||||
|
||||
<delete id="deleteById" parameterType="long">
|
||||
DELETE FROM users WHERE id = #{id}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
21
src/main/resources/mapper/admin/AdminUserMapper.xml
Normal file
21
src/main/resources/mapper/admin/AdminUserMapper.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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>
|
||||
|
||||
22
src/main/resources/mapper/agent/AgentMapper.xml
Normal file
22
src/main/resources/mapper/agent/AgentMapper.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
|
||||
8
src/main/resources/schema.sql
Normal file
8
src/main/resources/schema.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Initial database schema for game_platform
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(120) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
39
target/classes/application.yml
Normal file
39
target/classes/application.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
spring:
|
||||
application:
|
||||
name: gameplatform-server
|
||||
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/login_task_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
|
||||
mybatis:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
type-aliases-package: com.gameplatform.server.model
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
server:
|
||||
port: 18080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: info
|
||||
com.gameplatform.server: debug
|
||||
|
||||
security:
|
||||
jwt:
|
||||
secret: "change-this-secret-to-a-long-random-string-please"
|
||||
access-token-minutes: 30
|
||||
refresh-token-days: 7
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/gameplatform/server/mapper/UserMapper.class
Normal file
BIN
target/classes/com/gameplatform/server/mapper/UserMapper.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/gameplatform/server/model/User.class
Normal file
BIN
target/classes/com/gameplatform/server/model/User.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/gameplatform/server/security/JwtService.class
Normal file
BIN
target/classes/com/gameplatform/server/security/JwtService.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/gameplatform/server/service/UserService.class
Normal file
BIN
target/classes/com/gameplatform/server/service/UserService.class
Normal file
Binary file not shown.
Binary file not shown.
40
target/classes/mapper/UserMapper.xml
Normal file
40
target/classes/mapper/UserMapper.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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.UserMapper">
|
||||
|
||||
<resultMap id="UserResultMap" type="com.gameplatform.server.model.User">
|
||||
<id property="id" column="id" />
|
||||
<result property="username" column="username" />
|
||||
<result property="email" column="email" />
|
||||
<result property="createdAt" column="created_at" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id, username, email, created_at
|
||||
</sql>
|
||||
|
||||
<select id="findById" parameterType="long" resultMap="UserResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM users
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="findAll" resultMap="UserResultMap">
|
||||
SELECT <include refid="Base_Column_List"/>
|
||||
FROM users
|
||||
ORDER BY id DESC
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.gameplatform.server.model.User" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO users (username, email, created_at)
|
||||
VALUES (#{username}, #{email}, NOW())
|
||||
</insert>
|
||||
|
||||
<delete id="deleteById" parameterType="long">
|
||||
DELETE FROM users WHERE id = #{id}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
21
target/classes/mapper/admin/AdminUserMapper.xml
Normal file
21
target/classes/mapper/admin/AdminUserMapper.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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>
|
||||
|
||||
22
target/classes/mapper/agent/AgentMapper.xml
Normal file
22
target/classes/mapper/agent/AgentMapper.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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>
|
||||
|
||||
8
target/classes/schema.sql
Normal file
8
target/classes/schema.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Initial database schema for game_platform
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(120) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user