diff --git a/.gitignore b/.gitignore index 2827609..9d0c258 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ out/ **/application-dev.properties **/.env /log/ +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0f413b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 msksbr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..94614a4 --- /dev/null +++ b/README.MD @@ -0,0 +1,264 @@ +# 图书管理系统 + +

+ version + Kotlin + Spring Boot + MySQL + Java 21 + license +

+ +一个基于 **Spring Boot + Kotlin + MyBatis-Plus** 的后端图书管理系统,提供图书管理、用户认证、借阅归还等核心功能。项目注重安全性、可审计性,等保三级审计日志开箱即用。 + +## 功能 + +- **认证与授权** — JWT 无状态登录,Argon2 密码哈希,基于 AOP 的角色权限校验(admin / user),防用户枚举攻击 +- **图书管理** — 搜索(书名 / 作者)、查看详情、新增 / 修改 / 删除 / 调整库存(管理员),书名 + 作者判重 +- **借阅管理** — 借书 / 还书(自动扣减 / 恢复库存)、个人借阅记录、管理员可查看全部借阅及手动操作 +- **审计日志** — 等保三级(GB/T 22239-2019 8.1.4.3),结构化审计日志,含用户标识、来源 IP、操作参数、结果摘要,支持持久化到文件 +- **统一响应** — 所有接口返回 `ApiResult` 统一格式,业务码自动映射 HTTP 状态码 +- **OpenAPI 文档** — SpringDoc 自动生成,支持在线调试 + +## 技术栈 + +| 类别 | 技术 | 版本 | 说明 | +|------|------|------|------| +| 语言 | Kotlin | 2.2.21 | JVM 目标 | +| 框架 | Spring Boot | 4.0.6 | | +| ORM | MyBatis-Plus | 3.5.15 | 零 XML 配置,BaseMapper 通用 CRUD | +| 数据库 | MySQL | 8.0 | InnoDB,utf8mb4 | +| 认证 | jjwt (JSON Web Token) | 0.13.0 | HMAC-SHA256 签名 | +| 密码 | Argon2 (Spring Security Crypto) | 5.3+ | 抗 GPU/ASIC 暴力破解 | +| 协程 | kotlinx-coroutines | 1.10.2 | 并发查询用户 + 图书信息 | +| 文档 | SpringDoc OpenAPI | 3.0.3 | Swagger UI | +| AOP | AspectJ Weaver | — | 日志 / 审计 / 权限三切面 | +| 日志 | Logback + kotlin-logging | 7.0.0 | Janino 条件表达式,按天滚动 | + +## 快速开始 + +### 1. 准备数据库 + +MySQL 8.0 中新建一个数据库(如 `bookmgr`),然后运行建表脚本: + +```bash +mysql -u root -p bookmgr < bookmgr.schema.sql +``` + +脚本只建表,不插数据。默认用户由应用首次启动时自动创建。 + +### 2. 配置环境变量 + +生产环境通过环境变量注入配置: + +```bash +export DB_DRIVER=com.mysql.cj.jdbc.Driver +export DB_TYPE=mysql +export DB_URL=localhost +export DB_PORT=3306 +export DB_NAME=bookmgr +export DB_USER=root +export DB_PASSWORD=your-password +export JWT_SECRET=your-secret-at-least-32-chars +export JWT_EXPIRATION_TIME=86400000 # 可选,默认 24 小时,单位毫秒 +export LOG_PATH=/var/log/bookmgr # 可选,不设置则不写日志文件 +export LOG_EXPORT_AUDIT=true # 可选,审计日志持久化,默认开启 +export LOG_EXPORT_RUNTIME=false # 可选,运行日志持久化,默认关闭 +export LOG_LEVEL=INFO # 可选,包级别日志级别,默认 INFO +``` + +| 变量 | 必填 | 说明 | +|------|------|------| +| `DB_DRIVER` | 是 | 驱动类名 | +| `DB_TYPE` | 是 | 数据库类型(mysql) | +| `DB_URL` | 是 | 数据库地址 | +| `DB_PORT` | 是 | 数据库端口 | +| `DB_NAME` | 是 | 数据库名 | +| `DB_USER` | 是 | 数据库用户 | +| `DB_PASSWORD` | 是 | 数据库密码 | +| `JWT_SECRET` | **强烈建议** | 不设置则每次启动随机生成,已签发 token 全部失效 | +| `JWT_EXPIRATION_TIME` | 否 | token 有效期(毫秒),默认 86400000(24 小时) | + +### 3. 运行 + +```bash +./gradlew bootRun +``` + +### 4. 默认用户 + +应用首次启动时自动创建以下用户(幂等,已存在则跳过): + +| 用户名 | 密码 | 角色 | +|--------|------|------| +| `admin` | `admin` | 管理员 | +| `user01` | `user01` | 普通用户 | +| `user02` | `user02` | 普通用户 | + +> **注意**:预置用户当前硬编码在 `InitUserRunner` 中。未来计划分三步演进:硬编码 → 配置文件 → 前端初始化向导。生产部署后请第一时间修改密码。 + +## 开发指南 + +### IDEA 运行配置 + +1. 复制 `src/main/resources/application.yaml` 为 `application-dev.yaml`,修改数据库连接等本地配置 +2. IDEA → Run Configuration → Active profiles 填写 `dev` + +![](readme.Assets/IdeaRunConfig.png) + +3. 启动即可 + +示例 `application-dev.yaml`: + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/bookmgr + username: root + password: your-password + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl +logging: + level: + com.msksbr.bookmgr: DEBUG + export: + path: ./log + runtime-enabled: true +jwt: + expiration: 86400000 +springdoc: + swagger-ui: + enabled: true + api-docs: + enabled: true +``` + +### Swagger 文档 + +开发环境启动后访问:`http://localhost:8080/swagger-ui/index.html` + +> 生产环境默认关闭,防止 API 文档暴露。 + +### API 概览 + +``` +公开接口: + POST /api/auth/login — 登录 + POST /api/auth/logout — 登出 + GET /api/books/search — 搜索图书 + GET /api/books/getone — 图书详情 + GET /api/books/getall — 所有图书 + +普通用户(需 JWT + user 角色): + GET /api/borrows/getall — 我的借阅 + GET /api/borrows/search — 搜索我的借阅 + GET /api/borrows/getone — 借阅详情 + POST /api/borrows/borrowbook — 借书 + POST /api/borrows/returnbook — 还书 + +管理员(需 JWT + admin 角色): + POST /api/admin/books/add — 新增图书 + POST /api/admin/books/update — 修改图书 + POST /api/admin/books/delete — 删除图书 + POST /api/admin/books/update-stock — 调整库存 + GET /api/admin/borrows/search — 搜索借阅 + GET /api/admin/borrows/getone — 借阅详情 + GET /api/admin/borrows/getall — 全部借阅 + POST /api/admin/borrows/borrowbook — 手动借书 + POST /api/admin/borrows/returnbook — 手动还书 +``` + +请求示例: + +```bash +# 登录获取 token +curl -X POST http://localhost:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin"}' + +# 搜索图书 +curl "http://localhost:8080/api/books/search?query=西游记" + +# 借书 +curl -X POST "http://localhost:8080/api/borrows/borrowbook?bookId=1" \ + -H "Authorization: Bearer " +``` + +### 项目结构 + +``` +src/main/kotlin/com/msksbr/bookmgr/ +├── controller/ # 控制器 — 接口入口 +├── service/ # 服务接口 +│ └── impl/ # 服务实现 — 业务逻辑 +├── mapper/ # MyBatis-Plus 映射器 — 数据访问 +├── entity/ # 实体 — 表映射 +├── dto/ # 请求体 DTO +├── vo/ # 响应 VO +│ └── borrow/ # 借阅相关 VO +├── config/ # 配置 — JWT / 密码 / AOP 切面 / 异常处理 / OpenAPI +├── annotation/ # 自定义注解 — @RequireRole +├── runner/ # 启动任务 — InitUserRunner +├── script/ # 扩展 — log / audit Logger 快捷获取 +└── template/ # ApiResult 统一响应模板 +``` + +## 部署 + +### Java 直接运行 + +```bash +./gradlew bootJar +java -jar build/libs/bookMgr-0.1.jar +``` + +> `bootJar` 会自动排除 `application-dev.yaml`。 + +### Docker + +Dockerfile 待补充。镜像后续发布至私有 Gitea 仓库: + +[git.msksbr.com/msksbr/bookMgr](https://git.msksbr.com/msksbr/bookMgr) + +### Docker Compose + +Compose 文件待补充。注意生产环境应执行 `bookmgr.schema.sql`(仅建表,不含种子数据),用户由应用初始化。 + +## 审计日志 + +满足等保三级要求的结构化审计日志,默认输出到控制台。配置 `LOG_PATH` 后自动持久化到文件: + +``` +$LOG_PATH/ +├── audit/ +│ ├── audit.log # 当天审计日志 +│ └── audit.2026-05-23.0.gz # 历史归档(滚动策略:按天 + 100MB,保留 365 天,上限 20GB) +└── runtime/ + ├── runtime.log # 当天运行日志(需 LOG_EXPORT_RUNTIME=true) + └── runtime.2026-05-23.0.gz +``` + +审计日志记录字段:事件类型(CTRL / SVC)、用户名、用户 ID、角色、来源 IP、User-Agent、HTTP 方法 + 路径、操作参数(敏感字段自动掩码)、返回状态码、操作对象 ID、耗时。 + +## 安全特性 + +- 密码使用 **Argon2** 哈希存储,即使数据库泄露也无法还原 +- 登录失败时对齐响应时间(dummy hash),防止基于延时的用户枚举 +- 敏感参数(password / token / secret / authorization)在审计日志中自动掩码为 `***` +- JWT 密钥支持环境变量注入,不配置则每次启动随机生成 +- 全局异常捕获,不向客户端泄露堆栈信息 +- 生产环境默认关闭 Swagger 文档 + +## 声明 + +本项目基于 **MIT** 协议开源。本软件无任何销售渠道,不收取费用。请遵守开源协议合法使用。 + +## 作者 + +- 个人主页:[msksbr.com](https://msksbr.com/) +- 项目仓库:[git.msksbr.com/msksbr/bookMgr](https://git.msksbr.com/msksbr/bookMgr) diff --git a/bookmgr.schema.sql b/bookmgr.schema.sql new file mode 100644 index 0000000..0420a89 --- /dev/null +++ b/bookmgr.schema.sql @@ -0,0 +1,59 @@ +/* + Navicat Premium Dump SQL + + Source Server : root@localhost + Source Server Type : MySQL + Source Server Version : 80045 (8.0.45) + Source Host : 127.0.0.1:3306 + Source Schema : bookmgr + + Target Server Type : MySQL + Target Server Version : 80045 (8.0.45) + File Encoding : 65001 + + Date: 24/05/2026 01:05:44 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for book +-- ---------------------------- +DROP TABLE IF EXISTS `book`; +CREATE TABLE `book` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `author` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `stock` int DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ---------------------------- +-- Table structure for borrow_record +-- ---------------------------- +DROP TABLE IF EXISTS `borrow_record`; +CREATE TABLE `borrow_record` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint DEFAULT NULL, + `book_id` bigint DEFAULT NULL, + `borrow_time` datetime DEFAULT NULL, + `return_time` datetime DEFAULT NULL, + `status` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `role` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT 'USER', + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/build.gradle.kts b/build.gradle.kts index 61aaf18..0b0b136 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,10 +3,11 @@ plugins { kotlin("plugin.spring") version "2.2.21" id("org.springframework.boot") version "4.0.6" id("io.spring.dependency-management") version "1.1.7" + id("com.bmuschko.docker-spring-boot-application") version "10.0.0" } group = "com.msksbr" -version = "0.0.1-SNAPSHOT" +version = "0.1" description = "bookMgr" java { @@ -58,3 +59,23 @@ tasks.bootJar { exclude("application-dev.yaml") } +val giteaRegistry = System.getenv("GITEA_REGISTRY") ?: "gitea.example.com" +val giteaUser = System.getenv("GITEA_USERNAME") ?: "" +val giteaToken = System.getenv("GITEA_TOKEN") ?: "" + +docker { + springBootApplication { + baseImage.set("bellsoft/liberica-runtime-container:jre-21-slim-musl") + ports.set(listOf(8080)) + images.set(setOf( + "$giteaRegistry/$giteaUser/bookMgr:${project.version}", + "$giteaRegistry/$giteaUser/bookMgr:latest" + )) + jvmArgs.set(listOf("-Xmx512m", "-Dfile.encoding=UTF-8")) + } + registryCredentials { + url.set("https://$giteaRegistry") + username.set(giteaUser) + password.set(giteaToken) + } +} diff --git a/readme.Assets/IdeaRunConfig.png b/readme.Assets/IdeaRunConfig.png new file mode 100644 index 0000000..d300c9a Binary files /dev/null and b/readme.Assets/IdeaRunConfig.png differ