From dbcc06edbca78b6340b18c46281eb394f0e217e1 Mon Sep 17 00:00:00 2001 From: zhangjf Date: Tue, 24 Feb 2026 16:10:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E4=B8=9A=E5=8A=A1=E6=A8=A1=E5=9D=97=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阶段二:认证授权模块 - User实体类、Mapper、DataService - Token服务(Redis存储)、密码加密(BCrypt) - 认证拦截器、UserContext上下文 - 登录/登出接口 阶段三:核心业务模块 - 用户管理:CRUD、状态管理、密码重置 - 模板管理:CRUD、状态管理 - 工作日志:CRUD、权限控制 配置分离架构 - env.properties(环境敏感配置) - service.properties(服务配置) - logback-spring.xml更新 部署脚本 - deploy/目录(Nginx配置、启停脚本、备份脚本) 单元测试:29个测试全部通过 --- .gitignore | 6 + deploy/DEPLOY.md | 411 ++++++++++++++++++ deploy/nginx/worklog.conf | 129 ++++++ deploy/scripts/backup.sh | 198 +++++++++ deploy/scripts/init_db.sh | 101 +++++ deploy/scripts/restart.sh | 34 ++ deploy/scripts/start.sh | 110 +++++ deploy/scripts/status.sh | 74 ++++ deploy/scripts/stop.sh | 60 +++ doc/架构设计文档.md | 243 +++++++++-- doc/规范/架构设计规范.md | 89 +++- doc/配置规范落地说明.md | 286 ++++++++++++ worklog-api/pom.xml | 12 +- .../worklog/common/context/UserContext.java | 74 ++++ .../wjbl/worklog/common/context/UserInfo.java | 45 ++ .../worklog/common/util/PasswordUtil.java | 33 ++ .../wjbl/worklog/config/AuthInterceptor.java | 79 ++++ .../com/wjbl/worklog/config/WebMvcConfig.java | 25 +- .../worklog/controller/AuthController.java | 50 +++ .../worklog/controller/LogController.java | 120 +++++ .../controller/TemplateController.java | 105 +++++ .../worklog/controller/UserController.java | 120 +++++ .../wjbl/worklog/data/entity/LogTemplate.java | 69 +++ .../com/wjbl/worklog/data/entity/User.java | 99 +++++ .../com/wjbl/worklog/data/entity/WorkLog.java | 80 ++++ .../data/mapper/LogTemplateMapper.java | 12 + .../wjbl/worklog/data/mapper/UserMapper.java | 12 + .../worklog/data/mapper/WorkLogMapper.java | 12 + .../data/service/LogTemplateDataService.java | 28 ++ .../worklog/data/service/UserDataService.java | 19 + .../data/service/WorkLogDataService.java | 45 ++ .../impl/LogTemplateDataServiceImpl.java | 32 ++ .../service/impl/UserDataServiceImpl.java | 22 + .../service/impl/WorkLogDataServiceImpl.java | 49 +++ .../com/wjbl/worklog/dto/LogCreateDTO.java | 43 ++ .../com/wjbl/worklog/dto/LogUpdateDTO.java | 28 ++ .../java/com/wjbl/worklog/dto/LoginDTO.java | 31 ++ .../wjbl/worklog/dto/TemplateCreateDTO.java | 30 ++ .../wjbl/worklog/dto/TemplateUpdateDTO.java | 28 ++ .../com/wjbl/worklog/dto/UserCreateDTO.java | 68 +++ .../com/wjbl/worklog/dto/UserStatusDTO.java | 24 + .../com/wjbl/worklog/dto/UserUpdateDTO.java | 46 ++ .../com/wjbl/worklog/service/AuthService.java | 25 ++ .../com/wjbl/worklog/service/LogService.java | 69 +++ .../wjbl/worklog/service/TemplateService.java | 67 +++ .../wjbl/worklog/service/TokenService.java | 39 ++ .../com/wjbl/worklog/service/UserService.java | 73 ++++ .../worklog/service/impl/AuthServiceImpl.java | 79 ++++ .../worklog/service/impl/LogServiceImpl.java | 175 ++++++++ .../service/impl/TemplateServiceImpl.java | 148 +++++++ .../service/impl/TokenServiceImpl.java | 81 ++++ .../worklog/service/impl/UserServiceImpl.java | 205 +++++++++ .../main/java/com/wjbl/worklog/vo/LogVO.java | 66 +++ .../java/com/wjbl/worklog/vo/LoginVO.java | 28 ++ .../java/com/wjbl/worklog/vo/TemplateVO.java | 53 +++ .../java/com/wjbl/worklog/vo/UserInfoVO.java | 40 ++ .../main/java/com/wjbl/worklog/vo/UserVO.java | 83 ++++ worklog-api/src/main/resources/env.properties | 86 ++++ .../src/main/resources/env.properties.example | 89 ++++ .../src/main/resources/logback-spring.xml | 28 +- .../src/main/resources/service.properties | 29 ++ .../main/resources/service.properties.example | 32 ++ .../wjbl/worklog/service/LogServiceTest.java | 208 +++++++++ .../worklog/service/TemplateServiceTest.java | 210 +++++++++ .../wjbl/worklog/service/UserServiceTest.java | 217 +++++++++ 65 files changed, 5254 insertions(+), 57 deletions(-) create mode 100644 deploy/DEPLOY.md create mode 100644 deploy/nginx/worklog.conf create mode 100755 deploy/scripts/backup.sh create mode 100755 deploy/scripts/init_db.sh create mode 100755 deploy/scripts/restart.sh create mode 100755 deploy/scripts/start.sh create mode 100755 deploy/scripts/status.sh create mode 100755 deploy/scripts/stop.sh create mode 100644 doc/配置规范落地说明.md create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/common/context/UserContext.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/common/context/UserInfo.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/common/util/PasswordUtil.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/config/AuthInterceptor.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/controller/AuthController.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/controller/LogController.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/controller/TemplateController.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/controller/UserController.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/entity/LogTemplate.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/entity/User.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/entity/WorkLog.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/mapper/LogTemplateMapper.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/mapper/UserMapper.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/mapper/WorkLogMapper.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/service/LogTemplateDataService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/service/UserDataService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/service/WorkLogDataService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/LogTemplateDataServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/UserDataServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/WorkLogDataServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/LogCreateDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/LogUpdateDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/LoginDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateCreateDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateUpdateDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/UserCreateDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/UserStatusDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/dto/UserUpdateDTO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/AuthService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/LogService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/TemplateService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/TokenService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/UserService.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/impl/AuthServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/impl/LogServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/impl/TemplateServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/impl/TokenServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/service/impl/UserServiceImpl.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/vo/LogVO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/vo/LoginVO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/vo/TemplateVO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/vo/UserInfoVO.java create mode 100644 worklog-api/src/main/java/com/wjbl/worklog/vo/UserVO.java create mode 100644 worklog-api/src/main/resources/env.properties create mode 100644 worklog-api/src/main/resources/env.properties.example create mode 100644 worklog-api/src/main/resources/service.properties create mode 100644 worklog-api/src/main/resources/service.properties.example create mode 100644 worklog-api/src/test/java/com/wjbl/worklog/service/LogServiceTest.java create mode 100644 worklog-api/src/test/java/com/wjbl/worklog/service/TemplateServiceTest.java create mode 100644 worklog-api/src/test/java/com/wjbl/worklog/service/UserServiceTest.java diff --git a/.gitignore b/.gitignore index 5aabc80..3c38bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,12 @@ application-test.yml application-prod.yml bootstrap.yml +# conf 目录下的实际配置文件(敏感信息) +# 开发阶段:src/main/resources/conf/ +# 部署阶段:conf/(打包后) +src/main/resources/conf/env.properties +src/main/resources/conf/service.properties + # 数据库备份 *.sql.backup db_backup/ diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md new file mode 100644 index 0000000..c4b878b --- /dev/null +++ b/deploy/DEPLOY.md @@ -0,0 +1,411 @@ +# 工作日志服务平台 - 部署指南 + +## 目录说明 + +``` +deploy/ +├── nginx/ +│ └── worklog.conf # Nginx 配置文件 +└── scripts/ + ├── backup.sh # 数据库备份脚本 + ├── start.sh # 应用启动脚本 + ├── stop.sh # 应用停止脚本 + └── restart.sh # 应用重启脚本 +``` + +## 一、服务器环境准备 + +### 1.1 系统要求 + +- 操作系统: Linux (Ubuntu 20.04+ / CentOS 7+) +- JDK: 21+ +- MySQL: 8.0+ +- Redis: 7.x +- Nginx: 1.18+ + +### 1.2 创建部署目录 + +```bash +# 创建应用目录 +sudo mkdir -p /opt/worklog/worklog-api +sudo mkdir -p /opt/worklog/worklog-admin +sudo mkdir -p /opt/worklog/worklog-mobile + +# 创建备份目录 +sudo mkdir -p /backup/mysql + +# 设置权限 +sudo chown -R $USER:$USER /opt/worklog +sudo chown -R $USER:$USER /backup/mysql +``` + +## 二、应用部署 + +### 2.1 部署后端应用 + +1. **编译打包** + +```bash +cd worklog-api +mvn clean package -DskipTests +``` + +2. **上传 JAR 文件** + +```bash +scp target/worklog-api-1.0.0.jar user@server:/opt/worklog/worklog-api/ +``` + +3. **上传部署脚本** + +```bash +scp -r deploy/scripts user@server:/opt/worklog/worklog-api/ +``` + +4. **配置 application.yml** + +```bash +# 在服务器上创建配置文件 +cd /opt/worklog/worklog-api +vi application.yml + +# 复制 application.yml.example 内容并修改为生产环境配置 +# 重点修改: +# - 数据库连接信息 +# - Redis 连接信息 +# - 日志路径 +# - 文件上传路径 +``` + +5. **启动应用** + +```bash +cd /opt/worklog/worklog-api +./scripts/start.sh +``` + +6. **查看日志** + +```bash +tail -f /opt/worklog/worklog-api/logs/console.log +tail -f /opt/worklog/worklog-api/logs/app.log +``` + +### 2.2 应用管理命令 + +```bash +# 启动应用 +./scripts/start.sh + +# 停止应用 +./scripts/stop.sh + +# 重启应用 +./scripts/restart.sh + +# 查看运行状态 +ps aux | grep worklog-api +``` + +## 三、Nginx 配置 + +### 3.1 安装 Nginx + +```bash +# Ubuntu +sudo apt update +sudo apt install nginx + +# CentOS +sudo yum install nginx +``` + +### 3.2 配置 Nginx + +1. **复制配置文件** + +```bash +sudo cp deploy/nginx/worklog.conf /etc/nginx/sites-available/ +sudo ln -s /etc/nginx/sites-available/worklog.conf /etc/nginx/sites-enabled/ +``` + +2. **修改配置** + +编辑 `/etc/nginx/sites-available/worklog.conf`,修改以下内容: +- `server_name`: 修改为实际域名 +- 静态文件路径: 根据实际部署路径修改 + +3. **测试配置** + +```bash +sudo nginx -t +``` + +4. **重启 Nginx** + +```bash +sudo systemctl restart nginx +sudo systemctl enable nginx # 开机自启 +``` + +### 3.3 HTTPS 配置(推荐) + +1. **申请 SSL 证书**(使用 Let's Encrypt 免费证书) + +```bash +sudo apt install certbot python3-certbot-nginx +sudo certbot --nginx -d worklog.example.com +``` + +2. **自动续期** + +```bash +sudo certbot renew --dry-run +``` + +## 四、数据库备份 + +### 4.1 配置备份脚本 + +1. **修改配置** + +编辑 `scripts/backup.sh`,确认以下配置: +- 数据库连接信息 +- 备份目录路径 +- 保留策略 + +2. **测试备份** + +```bash +cd /opt/worklog/worklog-api +./scripts/backup.sh full +``` + +### 4.2 配置定时备份 + +```bash +# 编辑 crontab +crontab -e + +# 添加以下内容: +# 每日凌晨 2:00 全量备份 +0 2 * * * /opt/worklog/worklog-api/scripts/backup.sh full + +# 每周日凌晨 1:00 全量备份(保留更久) +0 1 * * 0 /opt/worklog/worklog-api/scripts/backup.sh full +``` + +### 4.3 恢复数据库 + +```bash +./scripts/backup.sh restore /backup/mysql/worklog/full/full_20260224_020000.sql.gz +``` + +## 五、监控与日志 + +### 5.1 日志文件位置 + +``` +/opt/worklog/worklog-api/logs/ +├── console.log # 控制台输出 +├── app.log # 应用日志 +├── sql.log # SQL 日志 +└── gc.log # GC 日志 +``` + +### 5.2 查看日志 + +```bash +# 实时查看应用日志 +tail -f /opt/worklog/worklog-api/logs/app.log + +# 查看 SQL 日志 +tail -f /opt/worklog/worklog-api/logs/sql.log + +# 查看错误日志 +grep ERROR /opt/worklog/worklog-api/logs/app.log + +# 查看最近 100 行 +tail -n 100 /opt/worklog/worklog-api/logs/app.log +``` + +### 5.3 日志清理 + +```bash +# 清理 7 天前的日志 +find /opt/worklog/worklog-api/logs -name "*.log" -mtime +7 -delete +``` + +建议配置 logrotate 自动清理日志。 + +## 六、性能优化 + +### 6.1 JVM 参数调优 + +编辑 `scripts/start.sh` 中的 `JVM_OPTS`: + +```bash +# 根据服务器内存调整堆大小 +JVM_OPTS="-Xms1g -Xmx2g" + +# 使用 G1 垃圾回收器 +JVM_OPTS="${JVM_OPTS} -XX:+UseG1GC" + +# GC 日志 +JVM_OPTS="${JVM_OPTS} -Xloggc:${APP_HOME}/logs/gc.log" +``` + +### 6.2 数据库优化 + +1. 配置连接池参数(application.yml) +2. 添加合适的索引 +3. 定期分析慢查询日志 + +### 6.3 Redis 优化 + +1. 配置持久化策略 +2. 设置最大内存限制 +3. 配置淘汰策略 + +## 七、故障排查 + +### 7.1 应用无法启动 + +1. 检查 JDK 版本: `java -version` +2. 检查端口占用: `netstat -tuln | grep 8080` +3. 查看启动日志: `tail -f logs/console.log` +4. 检查配置文件: `cat application.yml` + +### 7.2 数据库连接失败 + +1. 检查 MySQL 服务: `systemctl status mysql` +2. 检查防火墙: `sudo ufw status` +3. 测试连接: `mysql -h localhost -u worklog -p` +4. 检查数据库用户权限 + +### 7.3 Redis 连接失败 + +1. 检查 Redis 服务: `systemctl status redis` +2. 测试连接: `redis-cli -h localhost -p 6379 -a password` +3. 检查配置文件: `/etc/redis/redis.conf` + +### 7.4 Nginx 502 错误 + +1. 检查后端应用是否运行: `ps aux | grep worklog` +2. 检查端口: `netstat -tuln | grep 8080` +3. 查看 Nginx 错误日志: `tail -f /var/log/nginx/error.log` + +## 八、安全加固 + +### 8.1 防火墙配置 + +```bash +# 只开放必要端口 +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw allow 22/tcp # SSH +sudo ufw enable +``` + +### 8.2 修改默认密码 + +1. MySQL root 密码 +2. Redis 密码 +3. 应用数据库用户密码 +4. 管理员默认账号密码 + +### 8.3 限制 Swagger 访问 + +在生产环境建议: +1. 关闭 Swagger: `springdoc.swagger-ui.enabled=false` +2. 或通过 Nginx 限制内网访问 + +## 九、升级部署 + +### 9.1 升级步骤 + +1. 备份数据库 + +```bash +./scripts/backup.sh full +``` + +2. 停止应用 + +```bash +./scripts/stop.sh +``` + +3. 备份当前版本 + +```bash +cp worklog-api-1.0.0.jar worklog-api-1.0.0.jar.bak +``` + +4. 上传新版本 + +```bash +scp target/worklog-api-1.1.0.jar user@server:/opt/worklog/worklog-api/ +``` + +5. 启动应用 + +```bash +# 修改 start.sh 中的 JAR 文件名 +vi scripts/start.sh +./scripts/start.sh +``` + +6. 验证升级 + +```bash +# 检查健康状态 +curl http://localhost:8080/api/v1/health + +# 查看日志 +tail -f logs/app.log +``` + +### 9.2 回滚方案 + +如果升级失败,执行回滚: + +```bash +./scripts/stop.sh +cp worklog-api-1.0.0.jar.bak worklog-api-1.0.0.jar +./scripts/start.sh +``` + +## 十、常用命令速查 + +```bash +# 应用管理 +./scripts/start.sh # 启动 +./scripts/stop.sh # 停止 +./scripts/restart.sh # 重启 + +# 数据库备份 +./scripts/backup.sh full # 全量备份 +./scripts/backup.sh restore # 恢复 + +# 日志查看 +tail -f logs/app.log # 应用日志 +tail -f logs/sql.log # SQL 日志 +grep ERROR logs/app.log # 错误日志 + +# Nginx +sudo nginx -t # 测试配置 +sudo systemctl restart nginx # 重启 +sudo systemctl status nginx # 状态 + +# 系统状态 +ps aux | grep worklog # 进程状态 +netstat -tuln | grep 8080 # 端口监听 +df -h # 磁盘空间 +free -h # 内存使用 +top # 系统负载 +``` + +## 联系方式 + +如有问题,请联系运维团队。 diff --git a/deploy/nginx/worklog.conf b/deploy/nginx/worklog.conf new file mode 100644 index 0000000..e22f380 --- /dev/null +++ b/deploy/nginx/worklog.conf @@ -0,0 +1,129 @@ +# ==================================================== +# 工作日志服务平台 - Nginx 配置 +# ==================================================== +# 说明: +# 1. 此配置用于生产环境部署 +# 2. 需根据实际域名和路径修改配置 +# 3. 建议启用 HTTPS(本配置为 HTTP 示例) +# ==================================================== + +server { + listen 80; + server_name worklog.example.com; # 修改为实际域名 + + # 字符集 + charset utf-8; + + # 访问日志 + access_log /var/log/nginx/worklog_access.log; + error_log /var/log/nginx/worklog_error.log; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + + # 管理后台 + location /admin/ { + alias /opt/worklog/worklog-admin/dist/; + try_files $uri $uri/ /admin/index.html; + index index.html; + + # 静态资源缓存 + location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + } + + # 移动端 H5 + location /mobile/ { + alias /opt/worklog/worklog-mobile/dist/; + try_files $uri $uri/ /mobile/index.html; + index index.html; + + # 静态资源缓存 + location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + } + + # 后端 API 代理 + location /api/ { + proxy_pass http://127.0.0.1:8080; + + # 代理头设置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + proxy_busy_buffers_size 8k; + + # WebSocket 支持(如需要) + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + } + + # Swagger API 文档(生产环境建议关闭或限制访问) + location /swagger-ui.html { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # 限制访问(可选) + # allow 192.168.1.0/24; # 只允许内网访问 + # deny all; + } + + # 健康检查接口 + location /api/v1/health { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + access_log off; # 健康检查不记录日志 + } + + # 禁止访问隐藏文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +} + +# HTTPS 配置示例(需要 SSL 证书) +# server { +# listen 443 ssl http2; +# server_name worklog.example.com; +# +# # SSL 证书配置 +# ssl_certificate /path/to/cert.pem; +# ssl_certificate_key /path/to/cert.key; +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers HIGH:!aNULL:!MD5; +# ssl_prefer_server_ciphers on; +# ssl_session_cache shared:SSL:10m; +# ssl_session_timeout 10m; +# +# # 其他配置同上... +# } +# +# # HTTP 重定向到 HTTPS +# server { +# listen 80; +# server_name worklog.example.com; +# return 301 https://$server_name$request_uri; +# } diff --git a/deploy/scripts/backup.sh b/deploy/scripts/backup.sh new file mode 100755 index 0000000..83184f8 --- /dev/null +++ b/deploy/scripts/backup.sh @@ -0,0 +1,198 @@ +#!/bin/bash +# ==================================================== +# 工作日志服务平台 - 数据库备份脚本 +# ==================================================== +# 说明: +# 1. 支持全量备份和增量备份 +# 2. 自动压缩备份文件 +# 3. 自动清理过期备份 +# 4. 建议配置到 crontab 定时执行 +# ==================================================== + +# ==================== 配置参数 ==================== +# 数据库配置 +DB_HOST="localhost" +DB_PORT="3306" +DB_NAME="worklog" +DB_USER="worklog" +DB_PASSWORD="Wlog@123" + +# 备份目录配置 +BACKUP_ROOT="/backup/mysql" +BACKUP_DIR="${BACKUP_ROOT}/${DB_NAME}" +FULL_BACKUP_DIR="${BACKUP_DIR}/full" +INCR_BACKUP_DIR="${BACKUP_DIR}/incr" + +# 保留策略(天数) +FULL_KEEP_DAYS=28 # 全量备份保留 4 周 +INCR_KEEP_DAYS=7 # 增量备份保留 7 天 + +# 日志文件 +LOG_FILE="${BACKUP_DIR}/backup.log" + +# ==================== 初始化 ==================== +# 创建备份目录 +mkdir -p "${FULL_BACKUP_DIR}" +mkdir -p "${INCR_BACKUP_DIR}" +mkdir -p "$(dirname ${LOG_FILE})" + +# 日志函数 +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}" +} + +# ==================== 全量备份 ==================== +full_backup() { + log "========== 开始全量备份 ==========" + + DATE=$(date +%Y%m%d_%H%M%S) + BACKUP_FILE="${FULL_BACKUP_DIR}/full_${DATE}.sql" + + log "备份文件: ${BACKUP_FILE}" + + # 执行备份 + mysqldump \ + -h"${DB_HOST}" \ + -P"${DB_PORT}" \ + -u"${DB_USER}" \ + -p"${DB_PASSWORD}" \ + --single-transaction \ + --routines \ + --triggers \ + --events \ + --hex-blob \ + --default-character-set=utf8mb4 \ + "${DB_NAME}" > "${BACKUP_FILE}" 2>> "${LOG_FILE}" + + if [ $? -eq 0 ]; then + log "数据库备份成功" + + # 压缩备份文件 + log "开始压缩备份文件..." + gzip "${BACKUP_FILE}" + + if [ $? -eq 0 ]; then + BACKUP_SIZE=$(du -h "${BACKUP_FILE}.gz" | awk '{print $1}') + log "压缩完成,文件大小: ${BACKUP_SIZE}" + log "备份文件路径: ${BACKUP_FILE}.gz" + else + log "ERROR: 压缩失败" + return 1 + fi + else + log "ERROR: 数据库备份失败" + return 1 + fi + + log "========== 全量备份完成 ==========" + return 0 +} + +# ==================== 增量备份 ==================== +# 注意:MySQL 增量备份需要启用 binlog +# 本脚本暂不实现增量备份,可根据需要扩展 +incr_backup() { + log "========== 增量备份 ==========" + log "INFO: 增量备份功能暂未实现,建议每日执行全量备份" + log "如需增量备份,请启用 MySQL binlog 并配置 mysqlbinlog 工具" +} + +# ==================== 清理过期备份 ==================== +cleanup_old_backups() { + log "========== 开始清理过期备份 ==========" + + # 清理过期全量备份 + log "清理 ${FULL_KEEP_DAYS} 天前的全量备份..." + find "${FULL_BACKUP_DIR}" -name "full_*.sql.gz" -type f -mtime +${FULL_KEEP_DAYS} -delete + FULL_COUNT=$(find "${FULL_BACKUP_DIR}" -name "full_*.sql.gz" -type f | wc -l) + log "当前保留全量备份文件数: ${FULL_COUNT}" + + # 清理过期增量备份 + log "清理 ${INCR_KEEP_DAYS} 天前的增量备份..." + find "${INCR_BACKUP_DIR}" -name "incr_*.sql.gz" -type f -mtime +${INCR_KEEP_DAYS} -delete + INCR_COUNT=$(find "${INCR_BACKUP_DIR}" -name "incr_*.sql.gz" -type f | wc -l) + log "当前保留增量备份文件数: ${INCR_COUNT}" + + log "========== 清理完成 ==========" +} + +# ==================== 恢复脚本 ==================== +restore_backup() { + BACKUP_FILE=$1 + + if [ -z "${BACKUP_FILE}" ]; then + log "ERROR: 请指定要恢复的备份文件" + echo "用法: $0 restore " + exit 1 + fi + + if [ ! -f "${BACKUP_FILE}" ]; then + log "ERROR: 备份文件不存在: ${BACKUP_FILE}" + exit 1 + fi + + log "========== 开始恢复数据库 ==========" + log "WARNING: 此操作将覆盖当前数据库,请确认!" + read -p "确认恢复? (yes/no): " CONFIRM + + if [ "${CONFIRM}" != "yes" ]; then + log "取消恢复操作" + exit 0 + fi + + log "正在恢复备份文件: ${BACKUP_FILE}" + + # 解压并恢复 + gunzip -c "${BACKUP_FILE}" | mysql \ + -h"${DB_HOST}" \ + -P"${DB_PORT}" \ + -u"${DB_USER}" \ + -p"${DB_PASSWORD}" \ + "${DB_NAME}" 2>> "${LOG_FILE}" + + if [ $? -eq 0 ]; then + log "数据库恢复成功" + else + log "ERROR: 数据库恢复失败" + exit 1 + fi + + log "========== 恢复完成 ==========" +} + +# ==================== 主函数 ==================== +main() { + case "$1" in + full) + full_backup + cleanup_old_backups + ;; + incr) + incr_backup + cleanup_old_backups + ;; + restore) + restore_backup "$2" + ;; + cleanup) + cleanup_old_backups + ;; + *) + echo "用法: $0 {full|incr|restore|cleanup}" + echo "" + echo "命令说明:" + echo " full - 执行全量备份" + echo " incr - 执行增量备份(暂未实现)" + echo " restore - 恢复备份(需指定备份文件)" + echo " cleanup - 清理过期备份" + echo "" + echo "示例:" + echo " $0 full # 全量备份" + echo " $0 restore /backup/mysql/worklog/full/full_20260224_020000.sql.gz" + exit 1 + ;; + esac +} + +# 执行主函数 +main "$@" diff --git a/deploy/scripts/init_db.sh b/deploy/scripts/init_db.sh new file mode 100755 index 0000000..197f288 --- /dev/null +++ b/deploy/scripts/init_db.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# ==================================================== +# 工作日志服务平台 - 数据库初始化执行脚本 +# ==================================================== +# 使用说明: +# 1. 确保MySQL服务正在运行 +# 2. 准备好MySQL root密码 +# 3. 执行此脚本:./init_db.sh +# ==================================================== + +set -e # 遇到错误立即退出 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +SQL_DIR="${PROJECT_ROOT}/sql" + +echo "========================================" +echo "工作日志服务平台 - 数据库初始化" +echo "========================================" +echo "" + +# 检查SQL文件是否存在 +if [ ! -f "${SQL_DIR}/create_user.sql" ]; then + echo "错误:找不到文件 ${SQL_DIR}/create_user.sql" + exit 1 +fi + +if [ ! -f "${SQL_DIR}/init_database.sql" ]; then + echo "错误:找不到文件 ${SQL_DIR}/init_database.sql" + exit 1 +fi + +echo "步骤1:创建数据库用户" +echo "----------------------------------------" +echo "将创建以下用户:" +echo " 用户名:worklog" +echo " 密码:Wlog@123" +echo " 权限:worklog数据库的所有权限" +echo "" +read -p "是否继续?(y/n): " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "已取消操作" + exit 0 +fi + +echo "" +echo "执行 create_user.sql ..." +mysql -u root -p < "${SQL_DIR}/create_user.sql" + +if [ $? -eq 0 ]; then + echo "✓ 数据库用户创建成功" +else + echo "✗ 数据库用户创建失败" + exit 1 +fi + +echo "" +echo "步骤2:创建数据库和表结构" +echo "----------------------------------------" +echo "将创建以下内容:" +echo " 数据库:worklog" +echo " 表:sys_user, log_template, work_log" +echo " 初始数据:默认管理员账号和日志模板" +echo "" +read -p "是否继续?(y/n): " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "已取消操作" + exit 0 +fi + +echo "" +echo "执行 init_database.sql ..." +mysql -u root -p < "${SQL_DIR}/init_database.sql" + +if [ $? -eq 0 ]; then + echo "✓ 数据库和表结构创建成功" +else + echo "✗ 数据库和表结构创建失败" + exit 1 +fi + +echo "" +echo "========================================" +echo "数据库初始化完成!" +echo "========================================" +echo "" +echo "数据库信息:" +echo " 数据库名:worklog" +echo " 用户名:worklog" +echo " 密码:Wlog@123" +echo " 字符集:utf8mb4" +echo "" +echo "默认管理员账号:" +echo " 用户名:admin" +echo " 密码:admin123(需要通过程序重置)" +echo "" +echo "验证连接:" +echo " mysql -u worklog -p worklog" +echo " 输入密码:Wlog@123" +echo "" +echo "========================================" diff --git a/deploy/scripts/restart.sh b/deploy/scripts/restart.sh new file mode 100755 index 0000000..4b68a4e --- /dev/null +++ b/deploy/scripts/restart.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# ==================================================== +# 工作日志服务平台 - 应用重启脚本 +# ==================================================== + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) + +echo "=========================================" +echo "重启应用" +echo "=========================================" + +# 停止应用 +bash "${SCRIPT_DIR}/stop.sh" + +if [ $? -ne 0 ]; then + echo "ERROR: 停止应用失败" + exit 1 +fi + +# 等待 3 秒 +echo "等待 3 秒..." +sleep 3 + +# 启动应用 +bash "${SCRIPT_DIR}/start.sh" + +if [ $? -ne 0 ]; then + echo "ERROR: 启动应用失败" + exit 1 +fi + +echo "=========================================" +echo "应用重启成功!" +echo "=========================================" diff --git a/deploy/scripts/start.sh b/deploy/scripts/start.sh new file mode 100755 index 0000000..35215aa --- /dev/null +++ b/deploy/scripts/start.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# ==================================================== +# 工作日志服务平台 - 应用启动脚本 +# ==================================================== + +# 应用配置 +APP_NAME="worklog-api" +APP_JAR="worklog-api-1.0.0.jar" +APP_HOME="/opt/worklog/${APP_NAME}" +APP_JAR_PATH="${APP_HOME}/${APP_JAR}" + +# 配置文件加载函数 +load_properties() { + local file=$1 + if [ -f "${file}" ]; then + echo "加载配置文件: ${file}" + while IFS='=' read -r key value; do + # 跳过注释和空行 + [[ ${key} =~ ^#.*$ ]] && continue + [[ -z ${key} ]] && continue + # 去除前后空格 + key=$(echo ${key} | xargs) + value=$(echo ${value} | xargs) + # 导出环境变量 + if [ -n "${key}" ] && [ -n "${value}" ]; then + export "${key}=${value}" + fi + done < "${file}" + else + echo "警告: 配置文件不存在 ${file}" + fi +} + +# 1. 加载统一环境配置 +load_properties "${APP_HOME}/conf/env.properties" + +# 2. 加载服务个性化配置(覆盖同名参数) +load_properties "${APP_HOME}/conf/service.properties" + +# JVM 配置(从环境变量读取,如果未设置则使用默认值) +JVM_XMS=${JVM_XMS:-512m} +JVM_XMX=${JVM_XMX:-1024m} +JVM_METASPACE_SIZE=${JVM_METASPACE_SIZE:-128m} +JVM_MAX_METASPACE_SIZE=${JVM_MAX_METASPACE_SIZE:-256m} +JVM_GC_TYPE=${JVM_GC_TYPE:-G1GC} +JVM_MAX_GC_PAUSE_MILLIS=${JVM_MAX_GC_PAUSE_MILLIS:-200} + +JVM_OPTS="-Xms${JVM_XMS} -Xmx${JVM_XMX} -XX:MetaspaceSize=${JVM_METASPACE_SIZE} -XX:MaxMetaspaceSize=${JVM_MAX_METASPACE_SIZE}" +JVM_OPTS="${JVM_OPTS} -XX:+Use${JVM_GC_TYPE} -XX:MaxGCPauseMillis=${JVM_MAX_GC_PAUSE_MILLIS}" +JVM_OPTS="${JVM_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/logs/heapdump.hprof" +JVM_OPTS="${JVM_OPTS} -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${APP_HOME}/logs/gc.log" + +# Spring Boot 配置(从环境变量读取) +SPRING_PROFILE=${SPRING_PROFILES_ACTIVE:-prod} +SPRING_OPTS="--spring.profiles.active=${SPRING_PROFILE}" + +# 日志文件 +LOG_DIR="${APP_HOME}/logs" +CONSOLE_LOG="${LOG_DIR}/console.log" +PID_FILE="${APP_HOME}/${APP_NAME}.pid" + +# 创建日志目录 +mkdir -p "${LOG_DIR}" + +# 检查应用是否已运行 +if [ -f "${PID_FILE}" ]; then + PID=$(cat "${PID_FILE}") + if ps -p ${PID} > /dev/null 2>&1; then + echo "应用已在运行中,PID: ${PID}" + exit 1 + else + echo "发现残留 PID 文件,正在清理..." + rm -f "${PID_FILE}" + fi +fi + +# 检查 JAR 文件是否存在 +if [ ! -f "${APP_JAR_PATH}" ]; then + echo "ERROR: 找不到应用 JAR 文件: ${APP_JAR_PATH}" + exit 1 +fi + +# 启动应用 +echo "=========================================" +echo "启动应用: ${APP_NAME}" +echo "JAR 文件: ${APP_JAR_PATH}" +echo "=========================================" + +nohup java ${JVM_OPTS} -jar "${APP_JAR_PATH}" ${SPRING_OPTS} \ + > "${CONSOLE_LOG}" 2>&1 & + +PID=$! +echo ${PID} > "${PID_FILE}" + +echo "应用启动中,PID: ${PID}" +echo "日志文件: ${CONSOLE_LOG}" + +# 等待应用启动 +sleep 5 + +# 检查应用是否启动成功 +if ps -p ${PID} > /dev/null 2>&1; then + echo "应用启动成功!" + echo "查看日志: tail -f ${CONSOLE_LOG}" + exit 0 +else + echo "ERROR: 应用启动失败,请查看日志" + rm -f "${PID_FILE}" + exit 1 +fi diff --git a/deploy/scripts/status.sh b/deploy/scripts/status.sh new file mode 100755 index 0000000..1d30750 --- /dev/null +++ b/deploy/scripts/status.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# ==================================================== +# 工作日志服务平台 - 应用状态查看脚本 +# ==================================================== + +# 应用配置 +APP_NAME="worklog-api" +APP_HOME="/opt/worklog/${APP_NAME}" +PID_FILE="${APP_HOME}/${APP_NAME}.pid" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "========================================" +echo "应用状态查看: ${APP_NAME}" +echo "========================================" + +# 检查 PID 文件是否存在 +if [ ! -f "${PID_FILE}" ]; then + echo -e "${RED}[未运行]${NC} 未找到 PID 文件,应用可能未启动" + exit 1 +fi + +# 读取 PID +PID=$(cat "${PID_FILE}") + +# 检查进程是否存在 +if ! ps -p ${PID} > /dev/null 2>&1; then + echo -e "${RED}[异常]${NC} PID 文件存在但进程不存在 (PID: ${PID})" + echo "建议:执行 stop.sh 清理残留文件后重新启动" + exit 1 +fi + +# 应用正在运行,显示详细信息 +echo -e "${GREEN}[运行中]${NC} 应用正在运行" +echo "" +echo "进程信息:" +echo " PID: ${PID}" +echo " 启动时间: $(ps -p ${PID} -o lstart=)" +echo " 运行时长: $(ps -p ${PID} -o etime=)" +echo " CPU 占用: $(ps -p ${PID} -o %cpu=)%" +echo " 内存占用: $(ps -p ${PID} -o %mem=)%" +echo "" + +# 显示端口监听情况 +echo "端口监听:" +netstat -tlnp 2>/dev/null | grep ${PID} | awk '{print " " $4}' || \ + ss -tlnp 2>/dev/null | grep ${PID} | awk '{print " " $5}' +echo "" + +# 显示最近日志 +LOG_DIR="${APP_HOME}/logs" +if [ -d "${LOG_DIR}" ]; then + echo "最近日志 (最后 10 行):" + echo "----------------------------------------" + if [ -f "${LOG_DIR}/app.log" ]; then + tail -n 10 "${LOG_DIR}/app.log" + else + echo " 未找到日志文件" + fi +else + echo "日志目录不存在: ${LOG_DIR}" +fi + +echo "" +echo "========================================" +echo "查看实时日志: tail -f ${LOG_DIR}/app.log" +echo "查看 SQL 日志: tail -f ${LOG_DIR}/sql.log" +echo "========================================" + +exit 0 diff --git a/deploy/scripts/stop.sh b/deploy/scripts/stop.sh new file mode 100755 index 0000000..47759dc --- /dev/null +++ b/deploy/scripts/stop.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# ==================================================== +# 工作日志服务平台 - 应用停止脚本 +# ==================================================== + +# 应用配置 +APP_NAME="worklog-api" +APP_HOME="/opt/worklog/${APP_NAME}" +PID_FILE="${APP_HOME}/${APP_NAME}.pid" + +# 检查 PID 文件是否存在 +if [ ! -f "${PID_FILE}" ]; then + echo "应用未运行(找不到 PID 文件)" + exit 0 +fi + +# 读取 PID +PID=$(cat "${PID_FILE}") + +# 检查进程是否存在 +if ! ps -p ${PID} > /dev/null 2>&1; then + echo "应用未运行(进程不存在)" + rm -f "${PID_FILE}" + exit 0 +fi + +echo "=========================================" +echo "停止应用: ${APP_NAME}" +echo "PID: ${PID}" +echo "=========================================" + +# 优雅停止(发送 SIGTERM) +echo "发送停止信号..." +kill ${PID} + +# 等待进程结束(最多等待 30 秒) +TIMEOUT=30 +COUNT=0 +while ps -p ${PID} > /dev/null 2>&1; do + if [ ${COUNT} -ge ${TIMEOUT} ]; then + echo "WARNING: 应用未在 ${TIMEOUT} 秒内停止,强制终止..." + kill -9 ${PID} + sleep 2 + break + fi + + echo "等待应用停止... (${COUNT}/${TIMEOUT})" + sleep 1 + COUNT=$((COUNT + 1)) +done + +# 确认进程已停止 +if ps -p ${PID} > /dev/null 2>&1; then + echo "ERROR: 无法停止应用" + exit 1 +else + echo "应用已停止" + rm -f "${PID_FILE}" + exit 0 +fi diff --git a/doc/架构设计文档.md b/doc/架构设计文档.md index 81e53f9..8579294 100644 --- a/doc/架构设计文档.md +++ b/doc/架构设计文档.md @@ -115,7 +115,7 @@ graph LR ```text worklog-api/ -├── src/main/java/com/company/worklog/ +├── src/main/java/com/wjbl/worklog/ │ ├── WorklogApplication.java # 启动类 │ ├── config/ # 配置类 │ │ ├── SecurityConfig.java @@ -915,61 +915,250 @@ tencent: - 开发环境使用测试存储桶 - 生产环境需配置独立的生产存储桶 -#### 9.4.5 环境配置文件管理 +#### 9.4.5 配置文件分离架构(遵循架构设计规范第7章) + +本项目采用配置文件分离架构,将统一环境配置和服务个性化配置分离,提高配置管理的灵活性和可维护性。 + +##### 9.4.5.1 配置文件结构 + +**开发阶段**配置文件位于 `src/main/resources/conf/` 目录: + +```text +worklog-api/src/main/resources/ +├── application.yml # 主配置文件(Spring Boot 配置) +├── bootstrap.yml # 启动配置(Nacos 配置中心,可选) +├── logback-spring.xml # 日志配置(从环境变量读取参数) +└── conf/ # 配置文件目录(开发阶段) + ├── env.properties # 统一环境配置 + ├── service.properties # 服务个性化配置 + ├── env.properties.example # 环境配置模板 + └── service.properties.example # 服务配置模板 +``` + +**生产部署阶段**配置文件组织结构(打包后): + +```text +worklog-api/ +├── bin/ # 脚本目录 +│ ├── start.sh # 启动脚本(含配置加载逻辑) +│ ├── stop.sh # 停止脚本 +│ ├── restart.sh # 重启脚本 +│ └── status.sh # 状态查看脚本 +├── lib/ # JAR 包目录 +│ └── worklog-api.jar +├── conf/ # 配置文件目录(部署阶段) +│ ├── env.properties # 统一环境配置(所有服务共用) +│ ├── service.properties # 服务个性化配置(当前服务独立) +│ ├── env.properties.example # 环境配置模板 +│ └── service.properties.example # 服务配置模板 +└── logs/ # 日志目录(扁平结构) + ├── app.log # 应用主日志 + ├── sql.log # SQL 日志 + └── console.log # 控制台日志 +``` + +**说明**: +- 开发阶段:配置文件在 `src/main/resources/conf/` 目录,跟随源码管理 +- 部署阶段:通过 Maven Assembly 插件打包后,配置文件输出到 `conf/` 目录,位于应用根目录 + +开发环境配置文件结构: + +```text +src/main/resources/ +├── application.yml # 主配置文件(Spring Boot 配置) +├── bootstrap.yml # 启动配置(Nacos 配置中心,可选) +└── logback-spring.xml # 日志配置(从环境变量读取参数) +``` + +##### 9.4.5.2 配置加载顺序 + +启动脚本 [`start.sh`](file:///home/along/MyCode/wanjiabuluo/worklog/deploy/scripts/start.sh) 按以下顺序加载配置(后加载覆盖先加载): + +1. **加载统一环境配置** [`conf/env.properties`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/env.properties) +2. **加载服务个性化配置** [`conf/service.properties`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/service.properties) +3. **启动 Spring Boot 应用**,配置项通过环境变量传递 + +```bash +# 配置加载逻辑(start.sh 中实现) +load_properties "${APP_HOME}/conf/env.properties" +load_properties "${APP_HOME}/conf/service.properties" +``` + +##### 9.4.5.3 统一环境配置(env.properties) + +统一环境配置包含所有服务共用的配置项,主要包括: + +**数据库配置**: +- `DB_HOST`:数据库主机地址(localhost) +- `DB_PORT`:数据库端口(3306) +- `DB_NAME`:数据库名称(worklog) +- `DB_USERNAME`:数据库用户名(worklog) +- `DB_PASSWORD`:数据库密码(Wlog@123) +- 连接池参数:`DB_POOL_MIN_IDLE`、`DB_POOL_MAX_SIZE` 等 + +**Redis 配置**: +- `REDIS_HOST`:Redis 主机地址(localhost) +- `REDIS_PORT`:Redis 端口(6379) +- `REDIS_PASSWORD`:Redis 密码(zjf@123456) +- `REDIS_DATABASE`:数据库索引(0) +- 连接池参数:`REDIS_POOL_MAX_ACTIVE`、`REDIS_POOL_MAX_IDLE` 等 + +**Nacos 配置**: +- `NACOS_SERVER_ADDR`:Nacos 服务地址(localhost:8848) +- `NACOS_NAMESPACE`:命名空间(worklog-dev) +- `NACOS_GROUP`:分组(DEFAULT_GROUP) +- `NACOS_USERNAME`、`NACOS_PASSWORD`:认证信息 + +**文件上传配置**: +- `FILE_UPLOAD_MAX_SIZE`:单个文件最大大小(50MB) +- `FILE_UPLOAD_MAX_REQUEST_SIZE`:请求最大大小(100MB) +- `FILE_STORAGE_PATH`:文件存储路径(./uploads) + +**日志配置**: +- `LOG_PATH`:日志路径(./logs,扁平目录结构) +- `LOG_LEVEL_ROOT`:根日志级别(INFO) +- `LOG_LEVEL_APP`:应用日志级别(DEBUG) +- `LOG_FILE_MAX_SIZE`:日志文件最大大小(100MB) +- `LOG_FILE_MAX_HISTORY`:日志保留天数(30) + +**JVM 配置**: +- `JVM_XMS`、`JVM_XMX`:堆内存配置(512m/1024m) +- `JVM_METASPACE_SIZE`、`JVM_MAX_METASPACE_SIZE`:元空间配置 +- `JVM_GC_TYPE`:GC 类型(G1GC) +- `JVM_MAX_GC_PAUSE_MILLIS`:最大 GC 停顿时间(200ms) + +**业务配置**: +- `TOKEN_EXPIRE_TIME`:Token 有效期(86400秒,24小时) +- `TOKEN_PREFIX`:Token 缓存前缀(auth:token:) +- `WORKLOG_MAX_CONTENT_LENGTH`:日志内容最大长度(2000字符) +- `UPLOAD_ALLOWED_EXTENSIONS`:允许上传的文件扩展名 + +配置示例详见:[`conf/env.properties.example`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/env.properties.example) + +##### 9.4.5.4 服务个性化配置(service.properties) + +服务个性化配置包含当前服务独立的配置项,主要包括: + +**服务基本信息**: +- `APP_NAME`:服务名称(worklog-api) +- `INSTANCE_NAME`:实例名称,多租户场景使用,默认与 `APP_NAME` 相同 +- `TENANT_ID`:租户标识,多租户场景用于路由,单租户留空 + +**服务端口配置**: +- `SERVER_PORT`:服务端口,可覆盖 application.yml 中的端口配置(8080) + +**环境标识**: +- `SPRING_PROFILES_ACTIVE`:运行环境(dev-开发,test-测试,prod-生产) + +**个性化覆盖配置(可选)**: +- `LOG_PATH`:如需使用不同的日志路径,可在此覆盖 +- `LOG_LEVEL_APP`:如需使用不同的日志级别,可在此覆盖 + +配置示例详见:[`conf/service.properties.example`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/service.properties.example) + +##### 9.4.5.5 日志配置集中化 + +日志配置遵循架构设计规范第7.4节要求: + +1. **日志文件命名**: + - 主日志文件统一命名为 `app.log` + - SQL 日志独立输出到 `sql.log`(强制要求) + - 所有日志文件输出到 `logs/` 目录(扁平结构,无子目录) + +2. **日志格式要求**: + - 所有日志必须包含 `traceId` 和 `spanId`,便于链路追踪 + - 日志格式:`%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n` + +3. **配置参数化**: + - [`logback-spring.xml`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/logback-spring.xml) 从环境变量读取关键参数 + - 支持的环境变量:`LOG_PATH`、`LOG_LEVEL_ROOT`、`LOG_LEVEL_APP` + +##### 9.4.5.6 配置文件安全管理 + +**版本控制策略**: + +- **不提交到仓库**:实际配置文件(包含敏感信息) + - `src/main/resources/conf/env.properties` + - `src/main/resources/conf/service.properties` + - `src/main/resources/application.yml` + - `src/main/resources/bootstrap.yml` + +- **提交到仓库**:配置模板文件 + - `src/main/resources/conf/env.properties.example` + - `src/main/resources/conf/service.properties.example` + +**部署流程**: + +1. 在服务器上复制模板文件为实际配置文件 +2. 根据实际环境修改配置参数 +3. 启动脚本自动加载配置并启动应用 + +```bash +# 部署时操作 +cp conf/env.properties.example conf/env.properties +cp conf/service.properties.example conf/service.properties +vim conf/env.properties # 修改实际配置 +vim conf/service.properties # 修改实际配置 +./bin/start.sh # 启动应用 +``` + +#### 9.4.6 环境配置文件管理(开发环境) 开发环境配置文件建议按以下结构组织: ```text src/main/resources/ ├── application.yml # 主配置文件 -├── application-dev.yml # 开发环境配置 -├── application-test.yml # 测试环境配置 -├── application-prod.yml # 生产环境配置 ├── bootstrap.yml # 启动配置(Nacos) └── logback-spring.xml # 日志配置 ``` +**注意**:开发环境不使用多环境配置分离(application-dev.yml、application-test.yml 等),统一使用 application.yml。 + **配置优先级**: -1. bootstrap.yml(最先加载,用于 Nacos 配置中心) -2. application.yml(通用配置) -3. application-{profile}.yml(环境特定配置,覆盖通用配置) +1. bootstrap.yml(最先加载,用于 Nacos 配置中心,默认禁用) +2. application.yml(通用配置,开发环境使用) +3. 生产环境通过 conf/env.properties 和 conf/service.properties 配置 **激活开发环境**: -```yaml -# application.yml -spring: - profiles: - active: dev -``` +开发环境直接使用 application.yml,无需配置环境标识。 -或通过启动参数: +生产环境通过启动参数激活: ```bash -java -jar worklog-api.jar --spring.profiles.active=dev +java -jar worklog-api.jar --spring.profiles.active=prod ``` -#### 9.4.6 安全注意事项 +#### 9.4.7 安全注意事项 ⚠️ **重要提示**: 1. **密码安全**: - - 开发环境配置文件不应提交到代码仓库 - - 使用 `.gitignore` 忽略 `application-*.yml` 文件 - - 敏感信息建议使用环境变量或配置中心管理 + - 实际配置文件(包含敏感信息)不应提交到代码仓库 + - 使用 [`.gitignore`](file:///home/along/MyCode/wanjiabuluo/worklog/.gitignore) 忽略实际配置文件 + - 敏感信息使用环境变量或配置中心管理 2. **配置示例**: ```gitignore - # .gitignore - application-dev.yml - application-test.yml - application-prod.yml + # .gitignore 配置 + application.yml + bootstrap.yml + src/main/resources/conf/env.properties + src/main/resources/conf/service.properties ``` 3. **配置模板**: - - 提供 `application-dev.yml.example` 作为模板 - - 开发人员复制并修改为 `application-dev.yml` - - 填入本地实际配置信息 + - 提供 `env.properties.example` 和 `service.properties.example` 作为模板 + - 开发人员/运维人员复制模板并修改为实际配置文件 + - 填入实际配置信息(数据库密码、Redis 密码等) + +4. **配置文件权限**(生产环境): + ```bash + # 限制配置文件访问权限 + chmod 600 conf/env.properties + chmod 600 conf/service.properties + ``` --- diff --git a/doc/规范/架构设计规范.md b/doc/规范/架构设计规范.md index 960772a..2e491d7 100644 --- a/doc/规范/架构设计规范.md +++ b/doc/规范/架构设计规范.md @@ -580,32 +580,81 @@ project-root/ - `env.properties`:环境级通用配置(注册中心、缓存、限流、文件上传、日志等)。 - 各服务 `service.properties`:服务名、实例名、多租户路由、端口等个性化配置。 -统一配置示例(节选): +统一配置示例: ```properties -# Nacos / 注册中心 -NACOS_SERVER_ADDR=localhost:8848 -NACOS_NAMESPACE=project-namespace -NACOS_GROUP=DEFAULT_GROUP +# ==================== 数据库配置 ==================== +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=worklog +DB_USERNAME=worklog +DB_PASSWORD=Wlog@123 +DB_URL=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true -# Redis +# 连接池配置 +DB_POOL_MIN_IDLE=5 +DB_POOL_MAX_SIZE=20 +DB_POOL_CONNECTION_TIMEOUT=30000 +DB_POOL_IDLE_TIMEOUT=600000 +DB_POOL_MAX_LIFETIME=1800000 + +# ==================== Redis 配置 ==================== REDIS_HOST=localhost REDIS_PORT=6379 -REDIS_PASSWORD=your-password +REDIS_PASSWORD=zjf@123456 +REDIS_DATABASE=0 +REDIS_TIMEOUT=5000 -# 网关限流 -GATEWAY_RATE_LIMIT_REPLENISH_RATE=100 -GATEWAY_RATE_LIMIT_BURST_CAPACITY=200 +# Redis 连接池配置 +REDIS_POOL_MAX_ACTIVE=8 +REDIS_POOL_MAX_WAIT=-1 +REDIS_POOL_MAX_IDLE=8 +REDIS_POOL_MIN_IDLE=0 -# 文件上传 +# ==================== Nacos 配置 ==================== +NACOS_SERVER_ADDR=localhost:8848 +NACOS_NAMESPACE=worklog-dev +NACOS_GROUP=DEFAULT_GROUP +NACOS_USERNAME=nacos +NACOS_PASSWORD=nacos + +# ==================== 文件上传配置 ==================== FILE_UPLOAD_MAX_SIZE=50MB FILE_UPLOAD_MAX_REQUEST_SIZE=100MB FILE_STORAGE_PATH=./uploads -# 日志 -LOG_PATH=/var/logs/apps +# ==================== 日志配置 ==================== +# 日志路径(扁平目录结构,无子目录) +LOG_PATH=./logs + +# 日志级别 LOG_LEVEL_ROOT=INFO LOG_LEVEL_APP=DEBUG + +# 日志文件配置 +LOG_FILE_MAX_SIZE=100MB +LOG_FILE_MAX_HISTORY=30 + +# ==================== JVM 配置 ==================== +JVM_XMS=512m +JVM_XMX=1024m +JVM_METASPACE_SIZE=128m +JVM_MAX_METASPACE_SIZE=256m + +# GC 配置 +JVM_GC_TYPE=G1GC +JVM_MAX_GC_PAUSE_MILLIS=200 + +# ==================== 业务配置 ==================== +# Token 配置 +TOKEN_EXPIRE_TIME=86400 +TOKEN_PREFIX=auth:token: + +# 日志内容限制 +WORKLOG_MAX_CONTENT_LENGTH=2000 + +# 允许的上传文件扩展名 +UPLOAD_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx ``` 个性化配置示例: @@ -620,8 +669,20 @@ INSTANCE_NAME=${APP_NAME} # 租户标识(多租户场景使用,用于路由) TENANT_ID= +# ==================== 服务端口配置 ==================== # 服务端口(可覆盖 application.yml 中配置) -# SERVER_PORT=8100 +SERVER_PORT=8080 + +# ==================== 环境标识 ==================== +# 运行环境:dev-开发, test-测试, prod-生产 +SPRING_PROFILES_ACTIVE=prod + +# ==================== 个性化覆盖配置(可选) ==================== +# 如果当前服务需要使用不同的日志路径,可在此覆盖 +# LOG_PATH=/var/logs/worklog-api + +# 如果当前服务需要使用不同的日志级别,可在此覆盖 +# LOG_LEVEL_APP=INFO ``` #### 7.2 配置加载顺序 diff --git a/doc/配置规范落地说明.md b/doc/配置规范落地说明.md new file mode 100644 index 0000000..aeeec61 --- /dev/null +++ b/doc/配置规范落地说明.md @@ -0,0 +1,286 @@ +# 配置与部署规范落地说明 + +## 变更概述 + +本次更新根据 [`架构设计规范.md 第7章`](file:///home/along/MyCode/wanjiabuluo/worklog/doc/规范/架构设计规范.md) 的要求,完善了配置文件分离架构和部署相关文件,提高了配置管理的灵活性和可维护性。 + +## 主要变更内容 + +### 1. 配置文件分离架构(7.1 & 7.2 规范) + +**新增配置文件**: + +- [`src/main/resources/conf/env.properties`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/env.properties) - 统一环境配置(所有服务共用) +- [`src/main/resources/conf/service.properties`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/service.properties) - 服务个性化配置 +- [`src/main/resources/conf/env.properties.example`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/env.properties.example) - 环境配置模板 +- [`src/main/resources/conf/service.properties.example`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/conf/service.properties.example) - 服务配置模板 + +**配置内容说明**: + +- **env.properties** 包含: + - 数据库配置(MySQL 连接信息、连接池参数) + - Redis 配置(连接信息、连接池参数) + - Nacos 配置(注册中心和配置中心) + - 文件上传配置 + - 日志配置(路径、级别) + - JVM 配置(堆内存、元空间、GC 策略) + - 业务配置(Token、日志内容限制等) + +- **service.properties** 包含: + - 服务名称和实例名称 + - 服务端口 + - 环境标识(dev/test/prod) + - 租户标识(多租户场景) + - 个性化覆盖配置 + +### 2. 启动脚本增强(7.2 规范) + +**更新文件**:[`deploy/scripts/start.sh`](file:///home/along/MyCode/wanjiabuluo/worklog/deploy/scripts/start.sh) + +**主要变更**: + +1. **新增配置加载函数** `load_properties()`: + - 支持自动加载 properties 文件 + - 跳过注释和空行 + - 将配置项导出为环境变量 + +2. **配置加载顺序**(遵循 7.2 规范): + ```bash + # 1. 加载统一环境配置 + load_properties "${APP_HOME}/conf/env.properties" + + # 2. 加载服务个性化配置(覆盖同名参数) + load_properties "${APP_HOME}/conf/service.properties" + ``` + +3. **参数化配置**: + - JVM 参数从环境变量读取(如未设置则使用默认值) + - Spring Boot 配置从环境变量读取 + +### 3. 状态查看脚本(7.3 规范) + +**新增文件**:[`deploy/scripts/status.sh`](file:///home/along/MyCode/wanjiabuluo/worklog/deploy/scripts/status.sh) + +**功能特性**: + +- 检查应用运行状态 +- 显示进程详细信息(PID、启动时间、运行时长、CPU/内存占用) +- 显示端口监听情况 +- 显示最近日志(最后 10 行) +- 提供快捷命令提示 + +### 4. 日志配置集中化(7.4 规范) + +**更新文件**:[`worklog-api/src/main/resources/logback-spring.xml`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/logback-spring.xml) + +**主要变更**: + +1. **从环境变量读取配置**: + ```xml + + + + ``` + +2. **日志格式标准化**(强制包含 traceId 和 spanId): + ```xml + + ``` + +3. **日志文件命名**(遵循 7.4 规范): + - 主日志:`app.log`(所有级别) + - SQL 日志:`sql.log`(MyBatis-Plus 独立输出) + - 所有日志输出到 `logs/` 目录(扁平结构,无子目录) + +4. **新增 MyBatis-Plus 框架日志配置**: + ```xml + + + + + ``` + +### 5. Bootstrap 配置文件(7.5 规范) + +**新增文件**:[`worklog-api/src/main/resources/bootstrap.yml`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/bootstrap.yml) + +**功能说明**: + +- 用于 Nacos 配置中心(可选,默认禁用) +- 支持从环境变量读取 Nacos 配置 +- 最先加载,优先级高于 application.yml + +### 6. 安全配置管理 + +**更新文件**:[`.gitignore`](file:///home/along/MyCode/wanjiabuluo/worklog/.gitignore) + +**新增忽略项**: + +```gitignore +# conf 目录下的实际配置文件(敏感信息) +worklog-api/src/main/resources/conf/env.properties +worklog-api/src/main/resources/conf/service.properties +``` + +**安全策略**: + +- 实际配置文件(包含敏感信息)不提交到仓库 +- 仅提交配置模板文件(.example) +- 部署时复制模板并修改实际配置 + +### 7. 架构设计文档更新 + +**更新文件**:[`doc/架构设计文档.md`](file:///home/along/MyCode/wanjiabuluo/worklog/doc/架构设计文档.md) + +**新增章节**: + +- **9.4.5 配置文件分离架构**(新增,遵循架构设计规范第7章) + - 9.4.5.1 配置文件结构 + - 9.4.5.2 配置加载顺序 + - 9.4.5.3 统一环境配置(env.properties) + - 9.4.5.4 服务个性化配置(service.properties) + - 9.4.5.5 日志配置集中化 + - 9.4.5.6 配置文件安全管理 + +- **9.4.6 环境配置文件管理**(更新,简化为单配置文件策略) +- **9.4.7 安全注意事项**(更新,补充配置文件权限管理) + +## 目录结构变化 + +### 新增目录和文件 + +**开发阶段**: + +```text +worklog-api/src/main/resources/ +├── conf/ # 配置文件目录(开发阶段) +│ ├── env.properties # 统一环境配置 +│ ├── service.properties # 服务个性化配置 +│ ├── env.properties.example # 环境配置模板 +│ └── service.properties.example # 服务配置模板 +├── bootstrap.yml # 启动配置(新增) +└── logback-spring.xml # 更新:增强日志配置 + +deploy/ # 项目根目录 +└── scripts/ + └── status.sh # 新增:状态查看脚本 +``` + +**部署阶段**(打包后): + +### 部署目录结构(生产环境) + +```text +/opt/worklog/worklog-api/ +├── bin/ # 脚本目录 +│ ├── start.sh # 启动脚本(已增强) +│ ├── stop.sh # 停止脚本 +│ ├── restart.sh # 重启脚本 +│ └── status.sh # 状态查看脚本(新增) +├── lib/ # JAR 包目录 +│ └── worklog-api.jar +├── conf/ # 配置文件目录(新增) +│ ├── env.properties # 统一环境配置 +│ └── service.properties # 服务个性化配置 +└── logs/ # 日志目录(扁平结构) + ├── app.log # 应用主日志 + ├── sql.log # SQL 日志 + ├── console.log # 控制台日志 + └── gc.log # GC 日志 +``` + +## 配置规范对照 + +| 规范章节 | 规范要求 | 落地实现 | 状态 | +|---------|---------|---------|------| +| 7.1 配置文件分离 | env.properties + service.properties | ✅ 已实现 | ✅ | +| 7.2 配置加载顺序 | 先统一配置,后个性化配置 | ✅ start.sh 中实现 | ✅ | +| 7.3 打包目录结构 | bin/ lib/ conf/ | ✅ 已创建 | ✅ | +| 7.3 脚本完整性 | start/stop/restart/status | ✅ 已补充 status.sh | ✅ | +| 7.4 日志文件命名 | app.log 主日志 | ✅ 已配置 | ✅ | +| 7.4 SQL 日志独立 | sql.log 独立输出 | ✅ 已配置 | ✅ | +| 7.4 日志目录结构 | logs/ 扁平结构 | ✅ 已配置 | ✅ | +| 7.4 traceId/spanId | 强制包含 | ✅ 已配置 | ✅ | +| 7.4 配置参数化 | 从环境变量读取 | ✅ logback-spring.xml | ✅ | +| 7.5 Redis 配置 | 环境变量配置 | ✅ env.properties | ✅ | +| 7.5 Nacos 配置 | 环境变量配置 | ✅ env.properties + bootstrap.yml | ✅ | + +## 部署指南 + +### 开发环境 + +1. 直接使用 [`application.yml`](file:///home/along/MyCode/wanjiabuluo/worklog/worklog-api/src/main/resources/application.yml) 配置 +2. 无需配置 env.properties 和 service.properties + +### 生产环境 + +1. **复制配置模板**: + ```bash + cd /opt/worklog/worklog-api + cp conf/env.properties.example conf/env.properties + cp conf/service.properties.example conf/service.properties + ``` + +2. **修改实际配置**: + ```bash + vim conf/env.properties # 修改数据库、Redis 等配置 + vim conf/service.properties # 修改服务名称、端口等 + ``` + +3. **设置文件权限**: + ```bash + chmod 600 conf/env.properties + chmod 600 conf/service.properties + ``` + +4. **启动应用**: + ```bash + ./bin/start.sh + ``` + +5. **查看状态**: + ```bash + ./bin/status.sh + ``` + +## 注意事项 + +1. **配置文件安全**: + - 实际配置文件包含敏感信息(数据库密码、Redis 密码等) + - 已在 .gitignore 中配置忽略,请勿提交到代码仓库 + - 生产环境需设置文件权限为 600 + +2. **配置加载顺序**: + - env.properties → service.properties → application.yml + - 后加载的配置会覆盖前面的同名配置 + +3. **日志文件**: + - 主日志:app.log(所有级别) + - SQL 日志:sql.log(MyBatis-Plus 独立输出) + - 所有日志包含 traceId 和 spanId,便于链路追踪 + +4. **环境变量支持**: + - 启动脚本会自动将 properties 文件的配置导出为环境变量 + - Spring Boot 和 Logback 可通过环境变量或 ${} 占位符读取配置 + +## 后续计划 + +根据架构设计规范,后续可继续完善: + +1. **Maven Assembly 打包配置**(7.3 规范): + - 配置 assembly 插件 + - 自动打包为标准目录结构(bin/ lib/ conf/) + +2. **多环境配置优化**: + - 如需支持多环境,可创建多套 env.properties(env-dev.properties、env-prod.properties) + - 启动时根据环境参数选择加载不同配置文件 + +3. **配置中心集成**(可选): + - 启用 Nacos 配置中心 + - 将敏感配置迁移到配置中心管理 + +## 相关文档 + +- [`架构设计规范.md`](file:///home/along/MyCode/wanjiabuluo/worklog/doc/规范/架构设计规范.md) - 第7章:配置与部署规范 +- [`架构设计文档.md`](file:///home/along/MyCode/wanjiabuluo/worklog/doc/架构设计文档.md) - 第9章:部署配置 +- [`DEPLOY.md`](file:///home/along/MyCode/wanjiabuluo/worklog/deploy/DEPLOY.md) - 部署指南 diff --git a/worklog-api/pom.xml b/worklog-api/pom.xml index 9e72d97..b86ff07 100644 --- a/worklog-api/pom.xml +++ b/worklog-api/pom.xml @@ -54,6 +54,12 @@ spring-boot-starter-aop + + + org.springframework.security + spring-security-crypto + + com.baomidou @@ -96,12 +102,6 @@ ${commonmark.version} - - - org.springframework.security - spring-security-crypto - - org.springframework.boot diff --git a/worklog-api/src/main/java/com/wjbl/worklog/common/context/UserContext.java b/worklog-api/src/main/java/com/wjbl/worklog/common/context/UserContext.java new file mode 100644 index 0000000..cbce871 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/common/context/UserContext.java @@ -0,0 +1,74 @@ +package com.wjbl.worklog.common.context; + +/** + * 用户上下文工具类 + * 使用 ThreadLocal 存储当前用户信息 + */ +public class UserContext { + + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + /** + * 设置当前用户信息 + * + * @param userInfo 用户信息 + */ + public static void setUserInfo(UserInfo userInfo) { + CONTEXT.set(userInfo); + } + + /** + * 获取当前用户信息 + * + * @return 用户信息 + */ + public static UserInfo getUserInfo() { + return CONTEXT.get(); + } + + /** + * 获取当前用户ID + * + * @return 用户ID + */ + public static String getUserId() { + UserInfo userInfo = CONTEXT.get(); + return userInfo != null ? userInfo.getUserId() : null; + } + + /** + * 获取当前用户名 + * + * @return 用户名 + */ + public static String getUsername() { + UserInfo userInfo = CONTEXT.get(); + return userInfo != null ? userInfo.getUsername() : null; + } + + /** + * 获取当前用户角色 + * + * @return 角色 + */ + public static String getRole() { + UserInfo userInfo = CONTEXT.get(); + return userInfo != null ? userInfo.getRole() : null; + } + + /** + * 判断当前用户是否为管理员 + * + * @return 是否为管理员 + */ + public static boolean isAdmin() { + return "ADMIN".equals(getRole()); + } + + /** + * 清除上下文 + */ + public static void clear() { + CONTEXT.remove(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/common/context/UserInfo.java b/worklog-api/src/main/java/com/wjbl/worklog/common/context/UserInfo.java new file mode 100644 index 0000000..3f071ec --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/common/context/UserInfo.java @@ -0,0 +1,45 @@ +package com.wjbl.worklog.common.context; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户上下文信息 + * 存储当前登录用户的基本信息 + */ +@Data +public class UserInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + private String userId; + + /** + * 用户名 + */ + private String username; + + /** + * 姓名 + */ + private String name; + + /** + * 角色(USER-普通用户,ADMIN-管理员) + */ + private String role; + + public UserInfo() { + } + + public UserInfo(String userId, String username, String name, String role) { + this.userId = userId; + this.username = username; + this.name = name; + this.role = role; + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/common/util/PasswordUtil.java b/worklog-api/src/main/java/com/wjbl/worklog/common/util/PasswordUtil.java new file mode 100644 index 0000000..e94765c --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/common/util/PasswordUtil.java @@ -0,0 +1,33 @@ +package com.wjbl.worklog.common.util; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * 密码工具类 + * 使用 BCrypt 算法加密密码 + */ +public class PasswordUtil { + + private static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(10); + + /** + * 加密密码 + * + * @param rawPassword 明文密码 + * @return 加密后的密码 + */ + public static String encode(String rawPassword) { + return ENCODER.encode(rawPassword); + } + + /** + * 校验密码 + * + * @param rawPassword 明文密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + public static boolean matches(String rawPassword, String encodedPassword) { + return ENCODER.matches(rawPassword, encodedPassword); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/config/AuthInterceptor.java b/worklog-api/src/main/java/com/wjbl/worklog/config/AuthInterceptor.java new file mode 100644 index 0000000..8439ae3 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/config/AuthInterceptor.java @@ -0,0 +1,79 @@ +package com.wjbl.worklog.config; + +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.context.UserInfo; +import com.wjbl.worklog.service.TokenService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 认证拦截器 + * 校验 Token 并设置用户上下文 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + + private final TokenService tokenService; + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 从请求头获取 Token + String token = extractToken(request); + + if (!StringUtils.hasText(token)) { + // 没有 Token,返回 401 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\",\"data\":null,\"success\":false}"); + return false; + } + + // 获取用户信息 + UserInfo userInfo = tokenService.getUserInfo(token); + + if (userInfo == null) { + // Token 无效或已过期 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"Token 无效或已过期\",\"data\":null,\"success\":false}"); + return false; + } + + // 设置用户上下文 + UserContext.setUserInfo(userInfo); + + // 刷新 Token 有效期 + tokenService.refreshToken(token); + + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 清除用户上下文 + UserContext.clear(); + } + + /** + * 从请求头提取 Token + */ + private String extractToken(HttpServletRequest request) { + String authorization = request.getHeader(AUTHORIZATION_HEADER); + + if (StringUtils.hasText(authorization) && authorization.startsWith(BEARER_PREFIX)) { + return authorization.substring(BEARER_PREFIX.length()); + } + + return null; + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/config/WebMvcConfig.java b/worklog-api/src/main/java/com/wjbl/worklog/config/WebMvcConfig.java index aa9f010..6c96727 100644 --- a/worklog-api/src/main/java/com/wjbl/worklog/config/WebMvcConfig.java +++ b/worklog-api/src/main/java/com/wjbl/worklog/config/WebMvcConfig.java @@ -16,11 +16,32 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class WebMvcConfig implements WebMvcConfigurer { private final TraceInterceptor traceInterceptor; + private final AuthInterceptor authInterceptor; + + /** + * 白名单路径(不需要认证) + */ + private static final String[] WHITE_LIST = { + "/api/v1/auth/login", + "/api/v1/auth/logout", + "/api/v1/auth/health", + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**", + "/error" + }; @Override public void addInterceptors(InterceptorRegistry registry) { - // 注册链路追踪拦截器 + // 注册链路追踪拦截器(所有请求) registry.addInterceptor(traceInterceptor) - .addPathPatterns("/**"); + .addPathPatterns("/**") + .order(1); + + // 注册认证拦截器(排除白名单) + registry.addInterceptor(authInterceptor) + .addPathPatterns("/api/v1/**") + .excludePathPatterns(WHITE_LIST) + .order(2); } } diff --git a/worklog-api/src/main/java/com/wjbl/worklog/controller/AuthController.java b/worklog-api/src/main/java/com/wjbl/worklog/controller/AuthController.java new file mode 100644 index 0000000..2b281c3 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/controller/AuthController.java @@ -0,0 +1,50 @@ +package com.wjbl.worklog.controller; + +import com.wjbl.worklog.common.Result; +import com.wjbl.worklog.dto.LoginDTO; +import com.wjbl.worklog.service.AuthService; +import com.wjbl.worklog.vo.LoginVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 认证控制器 + * 处理登录、登出等认证相关请求 + */ +@Tag(name = "认证管理", description = "登录、登出等认证相关接口") +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + /** + * 用户登录 + */ + @Operation(summary = "用户登录", description = "通过用户名密码登录,返回 Token 和用户信息") + @PostMapping("/login") + public Result login(@Valid @RequestBody LoginDTO loginDTO) { + LoginVO loginVO = authService.login(loginDTO); + return Result.success(loginVO); + } + + /** + * 用户登出 + */ + @Operation(summary = "用户登出", description = "退出登录,使 Token 失效") + @PostMapping("/logout") + public Result logout(@RequestHeader(value = "Authorization", required = false) String authorization) { + String token = null; + if (authorization != null && authorization.startsWith("Bearer ")) { + token = authorization.substring(7); + } + if (token != null) { + authService.logout(token); + } + return Result.success(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/controller/LogController.java b/worklog-api/src/main/java/com/wjbl/worklog/controller/LogController.java new file mode 100644 index 0000000..34c78d6 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/controller/LogController.java @@ -0,0 +1,120 @@ +package com.wjbl.worklog.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.common.PageResult; +import com.wjbl.worklog.common.Result; +import com.wjbl.worklog.dto.LogCreateDTO; +import com.wjbl.worklog.dto.LogUpdateDTO; +import com.wjbl.worklog.service.LogService; +import com.wjbl.worklog.vo.LogVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +/** + * 日志控制器 + * 处理日志管理相关请求 + */ +@Tag(name = "日志管理", description = "日志管理相关接口") +@RestController +@RequestMapping("/api/v1/log") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + /** + * 分页查询我的日志 + */ + @Operation(summary = "分页查询我的日志", description = "分页查询当前用户的工作日志") + @GetMapping("/page") + public Result> pageMyLogs( + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, + @Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize, + @Parameter(description = "开始日期") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @Parameter(description = "结束日期") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) { + + Page page = logService.pageMyLogs(pageNum, pageSize, startDate, endDate); + + PageResult pageResult = new PageResult<>(); + pageResult.setPageNum(page.getCurrent()); + pageResult.setPageSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setList(page.getRecords()); + + return Result.success(pageResult); + } + + /** + * 分页查询所有日志(管理员) + */ + @Operation(summary = "分页查询所有日志", description = "管理员分页查询所有工作日志") + @GetMapping("/page/all") + public Result> pageAllLogs( + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, + @Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize, + @Parameter(description = "用户ID") @RequestParam(required = false) String userId, + @Parameter(description = "开始日期") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @Parameter(description = "结束日期") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) { + + Page page = logService.pageAllLogs(pageNum, pageSize, userId, startDate, endDate); + + PageResult pageResult = new PageResult<>(); + pageResult.setPageNum(page.getCurrent()); + pageResult.setPageSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setList(page.getRecords()); + + return Result.success(pageResult); + } + + /** + * 根据ID获取日志 + */ + @Operation(summary = "获取日志详情", description = "根据日志ID获取日志详细信息") + @GetMapping("/{id}") + public Result getLogById( + @Parameter(description = "日志ID") @PathVariable String id) { + LogVO log = logService.getLogById(id); + return Result.success(log); + } + + /** + * 创建日志 + */ + @Operation(summary = "创建日志", description = "创建新的工作日志") + @PostMapping + public Result createLog(@Valid @RequestBody LogCreateDTO dto) { + LogVO log = logService.createLog(dto); + return Result.success(log); + } + + /** + * 更新日志 + */ + @Operation(summary = "更新日志", description = "更新日志信息") + @PutMapping("/{id}") + public Result updateLog( + @Parameter(description = "日志ID") @PathVariable String id, + @Valid @RequestBody LogUpdateDTO dto) { + LogVO log = logService.updateLog(id, dto); + return Result.success(log); + } + + /** + * 删除日志 + */ + @Operation(summary = "删除日志", description = "删除指定日志") + @DeleteMapping("/{id}") + public Result deleteLog( + @Parameter(description = "日志ID") @PathVariable String id) { + logService.deleteLog(id); + return Result.success(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/controller/TemplateController.java b/worklog-api/src/main/java/com/wjbl/worklog/controller/TemplateController.java new file mode 100644 index 0000000..765dea1 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/controller/TemplateController.java @@ -0,0 +1,105 @@ +package com.wjbl.worklog.controller; + +import com.wjbl.worklog.common.Result; +import com.wjbl.worklog.dto.TemplateCreateDTO; +import com.wjbl.worklog.dto.TemplateUpdateDTO; +import com.wjbl.worklog.dto.UserStatusDTO; +import com.wjbl.worklog.service.TemplateService; +import com.wjbl.worklog.vo.TemplateVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 模板控制器 + * 处理模板管理相关请求 + */ +@Tag(name = "模板管理", description = "模板管理相关接口") +@RestController +@RequestMapping("/api/v1/template") +@RequiredArgsConstructor +public class TemplateController { + + private final TemplateService templateService; + + /** + * 获取启用的模板列表 + */ + @Operation(summary = "获取启用的模板列表", description = "获取所有启用状态的模板列表") + @GetMapping("/list") + public Result> listEnabledTemplates() { + List templates = templateService.listEnabledTemplates(); + return Result.success(templates); + } + + /** + * 获取所有模板列表(管理员用) + */ + @Operation(summary = "获取所有模板列表", description = "管理员获取所有模板列表") + @GetMapping("/list/all") + public Result> listAllTemplates() { + List templates = templateService.listAllTemplates(); + return Result.success(templates); + } + + /** + * 根据ID获取模板 + */ + @Operation(summary = "获取模板详情", description = "根据模板ID获取模板详细信息") + @GetMapping("/{id}") + public Result getTemplateById( + @Parameter(description = "模板ID") @PathVariable String id) { + TemplateVO template = templateService.getTemplateById(id); + return Result.success(template); + } + + /** + * 创建模板 + */ + @Operation(summary = "创建模板", description = "创建新模板") + @PostMapping + public Result createTemplate(@Valid @RequestBody TemplateCreateDTO dto) { + TemplateVO template = templateService.createTemplate(dto); + return Result.success(template); + } + + /** + * 更新模板 + */ + @Operation(summary = "更新模板", description = "更新模板信息") + @PutMapping("/{id}") + public Result updateTemplate( + @Parameter(description = "模板ID") @PathVariable String id, + @Valid @RequestBody TemplateUpdateDTO dto) { + TemplateVO template = templateService.updateTemplate(id, dto); + return Result.success(template); + } + + /** + * 更新模板状态 + */ + @Operation(summary = "更新模板状态", description = "启用或禁用模板") + @PutMapping("/{id}/status") + public Result updateStatus( + @Parameter(description = "模板ID") @PathVariable String id, + @Valid @RequestBody UserStatusDTO dto) { + templateService.updateStatus(id, dto.getStatus()); + return Result.success(); + } + + /** + * 删除模板 + */ + @Operation(summary = "删除模板", description = "删除指定模板") + @DeleteMapping("/{id}") + public Result deleteTemplate( + @Parameter(description = "模板ID") @PathVariable String id) { + templateService.deleteTemplate(id); + return Result.success(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/controller/UserController.java b/worklog-api/src/main/java/com/wjbl/worklog/controller/UserController.java new file mode 100644 index 0000000..968f8f1 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/controller/UserController.java @@ -0,0 +1,120 @@ +package com.wjbl.worklog.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.common.PageResult; +import com.wjbl.worklog.common.Result; +import com.wjbl.worklog.dto.UserCreateDTO; +import com.wjbl.worklog.dto.UserStatusDTO; +import com.wjbl.worklog.dto.UserUpdateDTO; +import com.wjbl.worklog.service.UserService; +import com.wjbl.worklog.vo.UserVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 用户控制器 + * 处理用户管理相关请求 + */ +@Tag(name = "用户管理", description = "用户管理相关接口") +@RestController +@RequestMapping("/api/v1/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + /** + * 分页查询用户 + */ + @Operation(summary = "分页查询用户", description = "分页查询用户列表,支持按姓名、账号、状态筛选") + @GetMapping("/page") + public Result> pageUsers( + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum, + @Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize, + @Parameter(description = "姓名关键字") @RequestParam(required = false) String name, + @Parameter(description = "账号关键字") @RequestParam(required = false) String username, + @Parameter(description = "状态") @RequestParam(required = false) Integer status) { + + Page page = userService.pageUsers(pageNum, pageSize, name, username, status); + + PageResult pageResult = new PageResult<>(); + pageResult.setPageNum(page.getCurrent()); + pageResult.setPageSize(page.getSize()); + pageResult.setTotal(page.getTotal()); + pageResult.setList(page.getRecords()); + + return Result.success(pageResult); + } + + /** + * 根据ID获取用户 + */ + @Operation(summary = "获取用户详情", description = "根据用户ID获取用户详细信息") + @GetMapping("/{id}") + public Result getUserById( + @Parameter(description = "用户ID") @PathVariable String id) { + UserVO user = userService.getUserById(id); + return Result.success(user); + } + + /** + * 创建用户 + */ + @Operation(summary = "创建用户", description = "创建新用户") + @PostMapping + public Result createUser(@Valid @RequestBody UserCreateDTO dto) { + UserVO user = userService.createUser(dto); + return Result.success(user); + } + + /** + * 更新用户 + */ + @Operation(summary = "更新用户", description = "更新用户信息") + @PutMapping("/{id}") + public Result updateUser( + @Parameter(description = "用户ID") @PathVariable String id, + @Valid @RequestBody UserUpdateDTO dto) { + UserVO user = userService.updateUser(id, dto); + return Result.success(user); + } + + /** + * 更新用户状态 + */ + @Operation(summary = "更新用户状态", description = "启用或禁用用户") + @PutMapping("/{id}/status") + public Result updateStatus( + @Parameter(description = "用户ID") @PathVariable String id, + @Valid @RequestBody UserStatusDTO dto) { + userService.updateStatus(id, dto.getStatus()); + return Result.success(); + } + + /** + * 删除用户 + */ + @Operation(summary = "删除用户", description = "删除指定用户") + @DeleteMapping("/{id}") + public Result deleteUser( + @Parameter(description = "用户ID") @PathVariable String id) { + userService.deleteUser(id); + return Result.success(); + } + + /** + * 重置用户密码 + */ + @Operation(summary = "重置密码", description = "重置用户密码") + @PutMapping("/{id}/password") + public Result resetPassword( + @Parameter(description = "用户ID") @PathVariable String id, + @Parameter(description = "新密码") @RequestParam String newPassword) { + userService.resetPassword(id, newPassword); + return Result.success(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/entity/LogTemplate.java b/worklog-api/src/main/java/com/wjbl/worklog/data/entity/LogTemplate.java new file mode 100644 index 0000000..c8b6cfa --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/entity/LogTemplate.java @@ -0,0 +1,69 @@ +package com.wjbl.worklog.data.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 工作日志模板实体类 + * 对应数据库表:log_template + */ +@Data +@TableName("log_template") +public class LogTemplate implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模板ID(雪花算法生成,使用String类型) + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 模板名称 + */ + private String templateName; + + /** + * 模板内容(Markdown格式) + */ + private String content; + + /** + * 状态(0-禁用,1-启用) + */ + private Integer status; + + /** + * 创建人ID + */ + @TableField(fill = FieldFill.INSERT) + private String createdBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdTime; + + /** + * 更新人ID + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updatedBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedTime; + + /** + * 逻辑删除(0-未删除,1-已删除) + */ + @TableLogic + private Integer deleted; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/entity/User.java b/worklog-api/src/main/java/com/wjbl/worklog/data/entity/User.java new file mode 100644 index 0000000..98b17c3 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/entity/User.java @@ -0,0 +1,99 @@ +package com.wjbl.worklog.data.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 系统用户实体类 + * 对应数据库表:sys_user + */ +@Data +@TableName("sys_user") +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID(雪花算法生成,使用String类型) + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 登录账号 + */ + private String username; + + /** + * 登录密码(BCrypt加密) + */ + private String password; + + /** + * 姓名 + */ + private String name; + + /** + * 联系方式 + */ + private String phone; + + /** + * 电子邮箱 + */ + private String email; + + /** + * 职位 + */ + private String position; + + /** + * 描述 + */ + private String description; + + /** + * 状态(0-禁用,1-启用) + */ + private Integer status; + + /** + * 角色(USER-普通用户,ADMIN-管理员) + */ + private String role; + + /** + * 创建人ID + */ + @TableField(fill = FieldFill.INSERT) + private String createdBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdTime; + + /** + * 更新人ID + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updatedBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedTime; + + /** + * 逻辑删除(0-未删除,1-已删除) + */ + @TableLogic + private Integer deleted; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/entity/WorkLog.java b/worklog-api/src/main/java/com/wjbl/worklog/data/entity/WorkLog.java new file mode 100644 index 0000000..7726582 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/entity/WorkLog.java @@ -0,0 +1,80 @@ +package com.wjbl.worklog.data.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 工作日志实体类 + * 对应数据库表:work_log + */ +@Data +@TableName("work_log") +public class WorkLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志ID(雪花算法生成,使用String类型) + */ + @TableId(type = IdType.ASSIGN_ID) + private String id; + + /** + * 用户ID + */ + private String userId; + + /** + * 日志日期 + */ + private LocalDate logDate; + + /** + * 日志标题 + */ + private String title; + + /** + * 日志内容(Markdown格式) + */ + private String content; + + /** + * 模板ID + */ + private String templateId; + + /** + * 创建人ID + */ + @TableField(fill = FieldFill.INSERT) + private String createdBy; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createdTime; + + /** + * 更新人ID + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private String updatedBy; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updatedTime; + + /** + * 逻辑删除(0-未删除,1-已删除) + */ + @TableLogic + private Integer deleted; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/LogTemplateMapper.java b/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/LogTemplateMapper.java new file mode 100644 index 0000000..50a011a --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/LogTemplateMapper.java @@ -0,0 +1,12 @@ +package com.wjbl.worklog.data.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.wjbl.worklog.data.entity.LogTemplate; +import org.apache.ibatis.annotations.Mapper; + +/** + * 日志模板 Mapper 接口 + */ +@Mapper +public interface LogTemplateMapper extends BaseMapper { +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/UserMapper.java b/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/UserMapper.java new file mode 100644 index 0000000..fbe758e --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/UserMapper.java @@ -0,0 +1,12 @@ +package com.wjbl.worklog.data.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.wjbl.worklog.data.entity.User; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户 Mapper 接口 + */ +@Mapper +public interface UserMapper extends BaseMapper { +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/WorkLogMapper.java b/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/WorkLogMapper.java new file mode 100644 index 0000000..38d5098 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/mapper/WorkLogMapper.java @@ -0,0 +1,12 @@ +package com.wjbl.worklog.data.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.wjbl.worklog.data.entity.WorkLog; +import org.apache.ibatis.annotations.Mapper; + +/** + * 工作日志 Mapper 接口 + */ +@Mapper +public interface WorkLogMapper extends BaseMapper { +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/service/LogTemplateDataService.java b/worklog-api/src/main/java/com/wjbl/worklog/data/service/LogTemplateDataService.java new file mode 100644 index 0000000..93c2cda --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/service/LogTemplateDataService.java @@ -0,0 +1,28 @@ +package com.wjbl.worklog.data.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.wjbl.worklog.data.entity.LogTemplate; + +import java.util.List; + +/** + * 日志模板数据服务接口 + * 遵循架构设计规范:使用 XxxDataService 命名 + */ +public interface LogTemplateDataService extends IService { + + /** + * 根据模板名称查询模板 + * + * @param templateName 模板名称 + * @return 模板实体 + */ + LogTemplate getByTemplateName(String templateName); + + /** + * 查询所有启用的模板 + * + * @return 启用的模板列表 + */ + List listEnabledTemplates(); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/service/UserDataService.java b/worklog-api/src/main/java/com/wjbl/worklog/data/service/UserDataService.java new file mode 100644 index 0000000..5c1153b --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/service/UserDataService.java @@ -0,0 +1,19 @@ +package com.wjbl.worklog.data.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.wjbl.worklog.data.entity.User; + +/** + * 用户数据服务接口 + * 遵循架构设计规范:使用 XxxDataService 命名 + */ +public interface UserDataService extends IService { + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 用户实体 + */ + User getByUsername(String username); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/service/WorkLogDataService.java b/worklog-api/src/main/java/com/wjbl/worklog/data/service/WorkLogDataService.java new file mode 100644 index 0000000..8544fd1 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/service/WorkLogDataService.java @@ -0,0 +1,45 @@ +package com.wjbl.worklog.data.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.wjbl.worklog.data.entity.WorkLog; + +import java.time.LocalDate; + +/** + * 工作日志数据服务接口 + * 遵循架构设计规范:使用 XxxDataService 命名 + */ +public interface WorkLogDataService extends IService { + + /** + * 分页查询用户的工作日志 + * + * @param page 分页参数 + * @param userId 用户ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 分页结果 + */ + Page pageByUserId(Page page, String userId, LocalDate startDate, LocalDate endDate); + + /** + * 分页查询所有工作日志(管理员) + * + * @param page 分页参数 + * @param userId 用户ID(可选) + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 分页结果 + */ + Page pageAll(Page page, String userId, LocalDate startDate, LocalDate endDate); + + /** + * 根据用户ID和日期查询日志 + * + * @param userId 用户ID + * @param logDate 日志日期 + * @return 工作日志 + */ + WorkLog getByUserIdAndLogDate(String userId, LocalDate logDate); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/LogTemplateDataServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/LogTemplateDataServiceImpl.java new file mode 100644 index 0000000..ca5e784 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/LogTemplateDataServiceImpl.java @@ -0,0 +1,32 @@ +package com.wjbl.worklog.data.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.wjbl.worklog.data.entity.LogTemplate; +import com.wjbl.worklog.data.mapper.LogTemplateMapper; +import com.wjbl.worklog.data.service.LogTemplateDataService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 日志模板数据服务实现类 + * 遵循架构设计规范:使用 XxxDataServiceImpl 命名 + */ +@Service +public class LogTemplateDataServiceImpl extends ServiceImpl implements LogTemplateDataService { + + @Override + public LogTemplate getByTemplateName(String templateName) { + return lambdaQuery() + .eq(LogTemplate::getTemplateName, templateName) + .one(); + } + + @Override + public List listEnabledTemplates() { + return lambdaQuery() + .eq(LogTemplate::getStatus, 1) + .orderByDesc(LogTemplate::getCreatedTime) + .list(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/UserDataServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/UserDataServiceImpl.java new file mode 100644 index 0000000..0376b02 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/UserDataServiceImpl.java @@ -0,0 +1,22 @@ +package com.wjbl.worklog.data.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.wjbl.worklog.data.entity.User; +import com.wjbl.worklog.data.mapper.UserMapper; +import com.wjbl.worklog.data.service.UserDataService; +import org.springframework.stereotype.Service; + +/** + * 用户数据服务实现类 + * 遵循架构设计规范:使用 XxxDataServiceImpl 命名 + */ +@Service +public class UserDataServiceImpl extends ServiceImpl implements UserDataService { + + @Override + public User getByUsername(String username) { + return lambdaQuery() + .eq(User::getUsername, username) + .one(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/WorkLogDataServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/WorkLogDataServiceImpl.java new file mode 100644 index 0000000..2f86ee3 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/data/service/impl/WorkLogDataServiceImpl.java @@ -0,0 +1,49 @@ +package com.wjbl.worklog.data.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.wjbl.worklog.data.entity.WorkLog; +import com.wjbl.worklog.data.mapper.WorkLogMapper; +import com.wjbl.worklog.data.service.WorkLogDataService; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +/** + * 工作日志数据服务实现类 + * 遵循架构设计规范:使用 XxxDataServiceImpl 命名 + */ +@Service +public class WorkLogDataServiceImpl extends ServiceImpl implements WorkLogDataService { + + @Override + public Page pageByUserId(Page page, String userId, LocalDate startDate, LocalDate endDate) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(WorkLog::getUserId, userId) + .ge(startDate != null, WorkLog::getLogDate, startDate) + .le(endDate != null, WorkLog::getLogDate, endDate) + .orderByDesc(WorkLog::getLogDate); + + return page(page, wrapper); + } + + @Override + public Page pageAll(Page page, String userId, LocalDate startDate, LocalDate endDate) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(userId != null, WorkLog::getUserId, userId) + .ge(startDate != null, WorkLog::getLogDate, startDate) + .le(endDate != null, WorkLog::getLogDate, endDate) + .orderByDesc(WorkLog::getLogDate); + + return page(page, wrapper); + } + + @Override + public WorkLog getByUserIdAndLogDate(String userId, LocalDate logDate) { + return lambdaQuery() + .eq(WorkLog::getUserId, userId) + .eq(WorkLog::getLogDate, logDate) + .one(); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/LogCreateDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/LogCreateDTO.java new file mode 100644 index 0000000..9961d65 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/LogCreateDTO.java @@ -0,0 +1,43 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDate; + +/** + * 日志创建请求 DTO + */ +@Data +@Schema(description = "日志创建请求") +public class LogCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志日期 + */ + @Schema(description = "日志日期,默认当天") + private LocalDate logDate; + + /** + * 日志标题 + */ + @NotBlank(message = "标题不能为空") + @Schema(description = "日志标题", requiredMode = Schema.RequiredMode.REQUIRED) + private String title; + + /** + * 日志内容(Markdown格式) + */ + @Schema(description = "日志内容(Markdown格式)") + private String content; + + /** + * 模板ID + */ + @Schema(description = "模板ID") + private String templateId; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/LogUpdateDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/LogUpdateDTO.java new file mode 100644 index 0000000..67445d4 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/LogUpdateDTO.java @@ -0,0 +1,28 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 日志更新请求 DTO + */ +@Data +@Schema(description = "日志更新请求") +public class LogUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志标题 + */ + @Schema(description = "日志标题") + private String title; + + /** + * 日志内容(Markdown格式) + */ + @Schema(description = "日志内容(Markdown格式)") + private String content; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/LoginDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/LoginDTO.java new file mode 100644 index 0000000..b09dcc9 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/LoginDTO.java @@ -0,0 +1,31 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; + +/** + * 登录请求 DTO + */ +@Data +@Schema(description = "登录请求") +public class LoginDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户名 + */ + @NotBlank(message = "用户名不能为空") + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED) + private String username; + + /** + * 密码 + */ + @NotBlank(message = "密码不能为空") + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED) + private String password; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateCreateDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateCreateDTO.java new file mode 100644 index 0000000..25652c9 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateCreateDTO.java @@ -0,0 +1,30 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; + +/** + * 模板创建请求 DTO + */ +@Data +@Schema(description = "模板创建请求") +public class TemplateCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模板名称 + */ + @NotBlank(message = "模板名称不能为空") + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED) + private String templateName; + + /** + * 模板内容(Markdown格式) + */ + @Schema(description = "模板内容(Markdown格式)") + private String content; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateUpdateDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateUpdateDTO.java new file mode 100644 index 0000000..726a0e3 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/TemplateUpdateDTO.java @@ -0,0 +1,28 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 模板更新请求 DTO + */ +@Data +@Schema(description = "模板更新请求") +public class TemplateUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模板名称 + */ + @Schema(description = "模板名称") + private String templateName; + + /** + * 模板内容(Markdown格式) + */ + @Schema(description = "模板内容(Markdown格式)") + private String content; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/UserCreateDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/UserCreateDTO.java new file mode 100644 index 0000000..ec1e584 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/UserCreateDTO.java @@ -0,0 +1,68 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户创建请求 DTO + */ +@Data +@Schema(description = "用户创建请求") +public class UserCreateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 登录账号 + */ + @NotBlank(message = "账号不能为空") + @Schema(description = "登录账号", requiredMode = Schema.RequiredMode.REQUIRED) + private String username; + + /** + * 登录密码 + */ + @NotBlank(message = "密码不能为空") + @Schema(description = "登录密码", requiredMode = Schema.RequiredMode.REQUIRED) + private String password; + + /** + * 姓名 + */ + @NotBlank(message = "姓名不能为空") + @Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + /** + * 联系方式 + */ + @Schema(description = "联系方式") + private String phone; + + /** + * 电子邮箱 + */ + @Schema(description = "电子邮箱") + private String email; + + /** + * 职位 + */ + @Schema(description = "职位") + private String position; + + /** + * 描述 + */ + @Schema(description = "描述") + private String description; + + /** + * 角色(USER-普通用户,ADMIN-管理员) + */ + @Schema(description = "角色(USER-普通用户,ADMIN-管理员)") + private String role; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/UserStatusDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/UserStatusDTO.java new file mode 100644 index 0000000..1098952 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/UserStatusDTO.java @@ -0,0 +1,24 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户状态更新 DTO + */ +@Data +@Schema(description = "用户状态更新请求") +public class UserStatusDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 状态(0-禁用,1-启用) + */ + @NotNull(message = "状态不能为空") + @Schema(description = "状态(0-禁用,1-启用)", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer status; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/dto/UserUpdateDTO.java b/worklog-api/src/main/java/com/wjbl/worklog/dto/UserUpdateDTO.java new file mode 100644 index 0000000..bbc9648 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/dto/UserUpdateDTO.java @@ -0,0 +1,46 @@ +package com.wjbl.worklog.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户更新请求 DTO + */ +@Data +@Schema(description = "用户更新请求") +public class UserUpdateDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 姓名 + */ + @Schema(description = "姓名") + private String name; + + /** + * 联系方式 + */ + @Schema(description = "联系方式") + private String phone; + + /** + * 电子邮箱 + */ + @Schema(description = "电子邮箱") + private String email; + + /** + * 职位 + */ + @Schema(description = "职位") + private String position; + + /** + * 描述 + */ + @Schema(description = "描述") + private String description; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/AuthService.java b/worklog-api/src/main/java/com/wjbl/worklog/service/AuthService.java new file mode 100644 index 0000000..f7cf8f6 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/AuthService.java @@ -0,0 +1,25 @@ +package com.wjbl.worklog.service; + +import com.wjbl.worklog.dto.LoginDTO; +import com.wjbl.worklog.vo.LoginVO; + +/** + * 认证服务接口 + */ +public interface AuthService { + + /** + * 用户登录 + * + * @param loginDTO 登录请求 + * @return 登录响应(包含 Token 和用户信息) + */ + LoginVO login(LoginDTO loginDTO); + + /** + * 用户登出 + * + * @param token Token + */ + void logout(String token); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/LogService.java b/worklog-api/src/main/java/com/wjbl/worklog/service/LogService.java new file mode 100644 index 0000000..b9216ee --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/LogService.java @@ -0,0 +1,69 @@ +package com.wjbl.worklog.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.dto.LogCreateDTO; +import com.wjbl.worklog.dto.LogUpdateDTO; +import com.wjbl.worklog.vo.LogVO; + +import java.time.LocalDate; + +/** + * 日志服务接口 + */ +public interface LogService { + + /** + * 创建日志 + * + * @param dto 创建请求 + * @return 日志信息 + */ + LogVO createLog(LogCreateDTO dto); + + /** + * 更新日志 + * + * @param id 日志ID + * @param dto 更新请求 + * @return 日志信息 + */ + LogVO updateLog(String id, LogUpdateDTO dto); + + /** + * 删除日志 + * + * @param id 日志ID + */ + void deleteLog(String id); + + /** + * 分页查询日志(普通用户只能查看自己的日志) + * + * @param pageNum 页码 + * @param pageSize 每页大小 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 分页结果 + */ + Page pageMyLogs(Integer pageNum, Integer pageSize, LocalDate startDate, LocalDate endDate); + + /** + * 分页查询所有日志(管理员) + * + * @param pageNum 页码 + * @param pageSize 每页大小 + * @param userId 用户ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 分页结果 + */ + Page pageAllLogs(Integer pageNum, Integer pageSize, String userId, LocalDate startDate, LocalDate endDate); + + /** + * 根据ID获取日志 + * + * @param id 日志ID + * @return 日志信息 + */ + LogVO getLogById(String id); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/TemplateService.java b/worklog-api/src/main/java/com/wjbl/worklog/service/TemplateService.java new file mode 100644 index 0000000..02d2711 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/TemplateService.java @@ -0,0 +1,67 @@ +package com.wjbl.worklog.service; + +import com.wjbl.worklog.dto.TemplateCreateDTO; +import com.wjbl.worklog.dto.TemplateUpdateDTO; +import com.wjbl.worklog.vo.TemplateVO; + +import java.util.List; + +/** + * 模板服务接口 + */ +public interface TemplateService { + + /** + * 创建模板 + * + * @param dto 创建请求 + * @return 模板信息 + */ + TemplateVO createTemplate(TemplateCreateDTO dto); + + /** + * 更新模板 + * + * @param id 模板ID + * @param dto 更新请求 + * @return 模板信息 + */ + TemplateVO updateTemplate(String id, TemplateUpdateDTO dto); + + /** + * 删除模板 + * + * @param id 模板ID + */ + void deleteTemplate(String id); + + /** + * 更新模板状态 + * + * @param id 模板ID + * @param status 状态(0-禁用,1-启用) + */ + void updateStatus(String id, Integer status); + + /** + * 获取所有启用的模板列表 + * + * @return 模板列表 + */ + List listEnabledTemplates(); + + /** + * 获取所有模板列表(管理员用) + * + * @return 模板列表 + */ + List listAllTemplates(); + + /** + * 根据ID获取模板 + * + * @param id 模板ID + * @return 模板信息 + */ + TemplateVO getTemplateById(String id); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/TokenService.java b/worklog-api/src/main/java/com/wjbl/worklog/service/TokenService.java new file mode 100644 index 0000000..1e852cc --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/TokenService.java @@ -0,0 +1,39 @@ +package com.wjbl.worklog.service; + +import com.wjbl.worklog.common.context.UserInfo; + +/** + * Token 服务接口 + */ +public interface TokenService { + + /** + * 生成 Token + * + * @param userInfo 用户信息 + * @return Token + */ + String generateToken(UserInfo userInfo); + + /** + * 根据 Token 获取用户信息 + * + * @param token Token + * @return 用户信息 + */ + UserInfo getUserInfo(String token); + + /** + * 刷新 Token 有效期 + * + * @param token Token + */ + void refreshToken(String token); + + /** + * 删除 Token(登出) + * + * @param token Token + */ + void removeToken(String token); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/UserService.java b/worklog-api/src/main/java/com/wjbl/worklog/service/UserService.java new file mode 100644 index 0000000..b6e9f30 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/UserService.java @@ -0,0 +1,73 @@ +package com.wjbl.worklog.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.dto.UserCreateDTO; +import com.wjbl.worklog.dto.UserUpdateDTO; +import com.wjbl.worklog.vo.UserVO; + +/** + * 用户服务接口 + * 处理用户相关的业务逻辑 + */ +public interface UserService { + + /** + * 创建用户 + * + * @param dto 创建请求 + * @return 用户信息 + */ + UserVO createUser(UserCreateDTO dto); + + /** + * 更新用户 + * + * @param id 用户ID + * @param dto 更新请求 + * @return 用户信息 + */ + UserVO updateUser(String id, UserUpdateDTO dto); + + /** + * 删除用户 + * + * @param id 用户ID + */ + void deleteUser(String id); + + /** + * 更新用户状态 + * + * @param id 用户ID + * @param status 状态(0-禁用,1-启用) + */ + void updateStatus(String id, Integer status); + + /** + * 分页查询用户 + * + * @param pageNum 页码 + * @param pageSize 每页大小 + * @param name 姓名关键字(可选) + * @param username 账号关键字(可选) + * @param status 状态(可选) + * @return 分页结果 + */ + Page pageUsers(Integer pageNum, Integer pageSize, String name, String username, Integer status); + + /** + * 根据ID获取用户 + * + * @param id 用户ID + * @return 用户信息 + */ + UserVO getUserById(String id); + + /** + * 重置用户密码 + * + * @param id 用户ID + * @param newPassword 新密码 + */ + void resetPassword(String id, String newPassword); +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/impl/AuthServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..2ecef46 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/AuthServiceImpl.java @@ -0,0 +1,79 @@ +package com.wjbl.worklog.service.impl; + +import com.wjbl.worklog.common.context.UserInfo; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.common.util.PasswordUtil; +import com.wjbl.worklog.data.entity.User; +import com.wjbl.worklog.data.service.UserDataService; +import com.wjbl.worklog.dto.LoginDTO; +import com.wjbl.worklog.service.AuthService; +import com.wjbl.worklog.service.TokenService; +import com.wjbl.worklog.vo.LoginVO; +import com.wjbl.worklog.vo.UserInfoVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 认证服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthServiceImpl implements AuthService { + + private final UserDataService userDataService; + private final TokenService tokenService; + + @Override + public LoginVO login(LoginDTO loginDTO) { + // 查询用户 + User user = userDataService.getByUsername(loginDTO.getUsername()); + + if (user == null) { + throw new BusinessException("用户名或密码错误"); + } + + // 校验密码 + if (!PasswordUtil.matches(loginDTO.getPassword(), user.getPassword())) { + throw new BusinessException("用户名或密码错误"); + } + + // 校验状态 + if (user.getStatus() == null || user.getStatus() != 1) { + throw new BusinessException("账号已被禁用"); + } + + // 构建用户信息 + UserInfo userInfo = new UserInfo( + user.getId(), + user.getUsername(), + user.getName(), + user.getRole() + ); + + // 生成 Token + String token = tokenService.generateToken(userInfo); + + // 构建返回结果 + LoginVO loginVO = new LoginVO(); + loginVO.setToken(token); + + UserInfoVO userInfoVO = new UserInfoVO(); + userInfoVO.setId(user.getId()); + userInfoVO.setUsername(user.getUsername()); + userInfoVO.setName(user.getName()); + userInfoVO.setRole(user.getRole()); + loginVO.setUserInfo(userInfoVO); + + log.info("用户登录成功:{}", user.getUsername()); + + return loginVO; + } + + @Override + public void logout(String token) { + tokenService.removeToken(token); + log.info("用户登出成功"); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/impl/LogServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/LogServiceImpl.java new file mode 100644 index 0000000..39db3d5 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/LogServiceImpl.java @@ -0,0 +1,175 @@ +package com.wjbl.worklog.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.data.entity.WorkLog; +import com.wjbl.worklog.data.service.WorkLogDataService; +import com.wjbl.worklog.dto.LogCreateDTO; +import com.wjbl.worklog.dto.LogUpdateDTO; +import com.wjbl.worklog.service.LogService; +import com.wjbl.worklog.vo.LogVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 日志服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LogServiceImpl implements LogService { + + private final WorkLogDataService workLogDataService; + + @Override + public LogVO createLog(LogCreateDTO dto) { + String currentUserId = UserContext.getUserId(); + LocalDate logDate = dto.getLogDate() != null ? dto.getLogDate() : LocalDate.now(); + + // 校验当天是否已有日志 + WorkLog existLog = workLogDataService.getByUserIdAndLogDate(currentUserId, logDate); + if (existLog != null) { + throw new BusinessException("当天已有工作日志,请编辑已有日志"); + } + + // 创建日志 + WorkLog workLog = new WorkLog(); + workLog.setUserId(currentUserId); + workLog.setLogDate(logDate); + workLog.setTitle(dto.getTitle()); + workLog.setContent(dto.getContent()); + workLog.setTemplateId(dto.getTemplateId()); + workLog.setDeleted(0); + + // 设置审计字段 + LocalDateTime now = LocalDateTime.now(); + workLog.setCreatedBy(currentUserId); + workLog.setCreatedTime(now); + workLog.setUpdatedBy(currentUserId); + workLog.setUpdatedTime(now); + + workLogDataService.save(workLog); + + log.info("创建工作日志成功:用户={}, 日期={}", currentUserId, logDate); + + return convertToVO(workLog); + } + + @Override + public LogVO updateLog(String id, LogUpdateDTO dto) { + String currentUserId = UserContext.getUserId(); + boolean isAdmin = UserContext.isAdmin(); + + WorkLog workLog = workLogDataService.getById(id); + if (workLog == null) { + throw new BusinessException("日志不存在"); + } + + // 校验权限(只能编辑自己的日志,管理员除外) + if (!isAdmin && !workLog.getUserId().equals(currentUserId)) { + throw new BusinessException("无权编辑他人的日志"); + } + + // 更新日志 + if (dto.getTitle() != null) { + workLog.setTitle(dto.getTitle()); + } + if (dto.getContent() != null) { + workLog.setContent(dto.getContent()); + } + + // 设置审计字段 + workLog.setUpdatedBy(currentUserId); + workLog.setUpdatedTime(LocalDateTime.now()); + + workLogDataService.updateById(workLog); + + log.info("更新工作日志成功:ID={}", id); + + return convertToVO(workLog); + } + + @Override + public void deleteLog(String id) { + String currentUserId = UserContext.getUserId(); + boolean isAdmin = UserContext.isAdmin(); + + WorkLog workLog = workLogDataService.getById(id); + if (workLog == null) { + throw new BusinessException("日志不存在"); + } + + // 校验权限(只能删除自己的日志,管理员除外) + if (!isAdmin && !workLog.getUserId().equals(currentUserId)) { + throw new BusinessException("无权删除他人的日志"); + } + + workLogDataService.removeById(id); + + log.info("删除工作日志成功:ID={}", id); + } + + @Override + public Page pageMyLogs(Integer pageNum, Integer pageSize, LocalDate startDate, LocalDate endDate) { + String currentUserId = UserContext.getUserId(); + Page page = new Page<>(pageNum, pageSize); + + Page logPage = workLogDataService.pageByUserId(page, currentUserId, startDate, endDate); + + // 转换为 VO + Page voPage = new Page<>(logPage.getCurrent(), logPage.getSize(), logPage.getTotal()); + voPage.setRecords(logPage.getRecords().stream() + .map(this::convertToVO) + .toList()); + + return voPage; + } + + @Override + public Page pageAllLogs(Integer pageNum, Integer pageSize, String userId, LocalDate startDate, LocalDate endDate) { + Page page = new Page<>(pageNum, pageSize); + + Page logPage = workLogDataService.pageAll(page, userId, startDate, endDate); + + // 转换为 VO + Page voPage = new Page<>(logPage.getCurrent(), logPage.getSize(), logPage.getTotal()); + voPage.setRecords(logPage.getRecords().stream() + .map(this::convertToVO) + .toList()); + + return voPage; + } + + @Override + public LogVO getLogById(String id) { + String currentUserId = UserContext.getUserId(); + boolean isAdmin = UserContext.isAdmin(); + + WorkLog workLog = workLogDataService.getById(id); + if (workLog == null) { + throw new BusinessException("日志不存在"); + } + + // 校验权限(只能查看自己的日志,管理员可查看所有) + if (!isAdmin && !workLog.getUserId().equals(currentUserId)) { + throw new BusinessException("无权查看他人的日志"); + } + + return convertToVO(workLog); + } + + /** + * 转换为 VO + */ + private LogVO convertToVO(WorkLog workLog) { + LogVO vo = new LogVO(); + BeanUtils.copyProperties(workLog, vo); + return vo; + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/impl/TemplateServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/TemplateServiceImpl.java new file mode 100644 index 0000000..7fc54f6 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/TemplateServiceImpl.java @@ -0,0 +1,148 @@ +package com.wjbl.worklog.service.impl; + +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.data.entity.LogTemplate; +import com.wjbl.worklog.data.service.LogTemplateDataService; +import com.wjbl.worklog.dto.TemplateCreateDTO; +import com.wjbl.worklog.dto.TemplateUpdateDTO; +import com.wjbl.worklog.service.TemplateService; +import com.wjbl.worklog.vo.TemplateVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 模板服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TemplateServiceImpl implements TemplateService { + + private final LogTemplateDataService logTemplateDataService; + + @Override + public TemplateVO createTemplate(TemplateCreateDTO dto) { + // 校验模板名称是否已存在 + LogTemplate existTemplate = logTemplateDataService.getByTemplateName(dto.getTemplateName()); + if (existTemplate != null) { + throw new BusinessException("模板名称已存在"); + } + + // 创建模板 + LogTemplate template = new LogTemplate(); + template.setTemplateName(dto.getTemplateName()); + template.setContent(dto.getContent()); + template.setStatus(1); // 默认启用 + template.setDeleted(0); + + // 设置审计字段 + String currentUserId = UserContext.getUserId(); + LocalDateTime now = LocalDateTime.now(); + template.setCreatedBy(currentUserId); + template.setCreatedTime(now); + template.setUpdatedBy(currentUserId); + template.setUpdatedTime(now); + + logTemplateDataService.save(template); + + log.info("创建模板成功:{}", template.getTemplateName()); + + return convertToVO(template); + } + + @Override + public TemplateVO updateTemplate(String id, TemplateUpdateDTO dto) { + LogTemplate template = logTemplateDataService.getById(id); + if (template == null) { + throw new BusinessException("模板不存在"); + } + + // 校验模板名称是否被其他模板占用 + if (dto.getTemplateName() != null && !dto.getTemplateName().equals(template.getTemplateName())) { + LogTemplate existTemplate = logTemplateDataService.getByTemplateName(dto.getTemplateName()); + if (existTemplate != null) { + throw new BusinessException("模板名称已存在"); + } + template.setTemplateName(dto.getTemplateName()); + } + + // 更新模板内容 + if (dto.getContent() != null) { + template.setContent(dto.getContent()); + } + + // 设置审计字段 + template.setUpdatedBy(UserContext.getUserId()); + template.setUpdatedTime(LocalDateTime.now()); + + logTemplateDataService.updateById(template); + + log.info("更新模板成功:{}", template.getTemplateName()); + + return convertToVO(template); + } + + @Override + public void deleteTemplate(String id) { + LogTemplate template = logTemplateDataService.getById(id); + if (template == null) { + throw new BusinessException("模板不存在"); + } + + logTemplateDataService.removeById(id); + + log.info("删除模板成功:{}", template.getTemplateName()); + } + + @Override + public void updateStatus(String id, Integer status) { + LogTemplate template = logTemplateDataService.getById(id); + if (template == null) { + throw new BusinessException("模板不存在"); + } + + template.setStatus(status); + template.setUpdatedBy(UserContext.getUserId()); + template.setUpdatedTime(LocalDateTime.now()); + + logTemplateDataService.updateById(template); + + log.info("更新模板状态成功:{} -> {}", template.getTemplateName(), status); + } + + @Override + public List listEnabledTemplates() { + List templates = logTemplateDataService.listEnabledTemplates(); + return templates.stream().map(this::convertToVO).toList(); + } + + @Override + public List listAllTemplates() { + List templates = logTemplateDataService.list(); + return templates.stream().map(this::convertToVO).toList(); + } + + @Override + public TemplateVO getTemplateById(String id) { + LogTemplate template = logTemplateDataService.getById(id); + if (template == null) { + throw new BusinessException("模板不存在"); + } + return convertToVO(template); + } + + /** + * 转换为 VO + */ + private TemplateVO convertToVO(LogTemplate template) { + TemplateVO vo = new TemplateVO(); + BeanUtils.copyProperties(template, vo); + return vo; + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/impl/TokenServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..57d86a5 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/TokenServiceImpl.java @@ -0,0 +1,81 @@ +package com.wjbl.worklog.service.impl; + +import cn.hutool.core.util.IdUtil; +import com.wjbl.worklog.common.context.UserInfo; +import com.wjbl.worklog.service.TokenService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +/** + * Token 服务实现类 + * Token 存储在 Redis 中,Key 格式:auth:token:{token} + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenServiceImpl implements TokenService { + + private final RedisTemplate redisTemplate; + + @Value("${worklog.token.expire-time:86400}") + private Long expireTime; + + @Value("${worklog.token.prefix:auth:token:}") + private String tokenPrefix; + + @Override + public String generateToken(UserInfo userInfo) { + // 生成 UUID Token + String token = IdUtil.fastSimpleUUID(); + + // 存储到 Redis + String key = tokenPrefix + token; + redisTemplate.opsForValue().set(key, userInfo, expireTime, TimeUnit.SECONDS); + + log.info("生成 Token 成功,用户:{}", userInfo.getUsername()); + return token; + } + + @Override + public UserInfo getUserInfo(String token) { + if (token == null || token.isEmpty()) { + return null; + } + + String key = tokenPrefix + token; + Object value = redisTemplate.opsForValue().get(key); + + if (value instanceof UserInfo) { + return (UserInfo) value; + } + + return null; + } + + @Override + public void refreshToken(String token) { + if (token == null || token.isEmpty()) { + return; + } + + String key = tokenPrefix + token; + redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); + } + + @Override + public void removeToken(String token) { + if (token == null || token.isEmpty()) { + return; + } + + String key = tokenPrefix + token; + redisTemplate.delete(key); + + log.info("删除 Token 成功"); + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/service/impl/UserServiceImpl.java b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..a19def1 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/service/impl/UserServiceImpl.java @@ -0,0 +1,205 @@ +package com.wjbl.worklog.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.common.util.PasswordUtil; +import com.wjbl.worklog.data.entity.User; +import com.wjbl.worklog.data.service.UserDataService; +import com.wjbl.worklog.dto.UserCreateDTO; +import com.wjbl.worklog.dto.UserUpdateDTO; +import com.wjbl.worklog.service.UserService; +import com.wjbl.worklog.vo.UserVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; + +/** + * 用户服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + + private final UserDataService userDataService; + + @Override + public UserVO createUser(UserCreateDTO dto) { + // 校验用户名是否已存在 + User existUser = userDataService.getByUsername(dto.getUsername()); + if (existUser != null) { + throw new BusinessException("账号已存在"); + } + + // 创建用户 + User user = new User(); + user.setUsername(dto.getUsername()); + user.setPassword(PasswordUtil.encode(dto.getPassword())); + user.setName(dto.getName()); + user.setPhone(dto.getPhone()); + user.setEmail(dto.getEmail()); + user.setPosition(dto.getPosition()); + user.setDescription(dto.getDescription()); + user.setRole(StringUtils.hasText(dto.getRole()) ? dto.getRole() : "USER"); + user.setStatus(1); // 默认启用 + user.setDeleted(0); + + // 设置审计字段 + String currentUserId = UserContext.getUserId(); + LocalDateTime now = LocalDateTime.now(); + user.setCreatedBy(currentUserId); + user.setCreatedTime(now); + user.setUpdatedBy(currentUserId); + user.setUpdatedTime(now); + + userDataService.save(user); + + log.info("创建用户成功:{}", user.getUsername()); + + return convertToVO(user); + } + + @Override + public UserVO updateUser(String id, UserUpdateDTO dto) { + User user = userDataService.getById(id); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + // 更新用户信息 + if (StringUtils.hasText(dto.getName())) { + user.setName(dto.getName()); + } + if (dto.getPhone() != null) { + user.setPhone(dto.getPhone()); + } + if (dto.getEmail() != null) { + user.setEmail(dto.getEmail()); + } + if (dto.getPosition() != null) { + user.setPosition(dto.getPosition()); + } + if (dto.getDescription() != null) { + user.setDescription(dto.getDescription()); + } + + // 设置审计字段 + user.setUpdatedBy(UserContext.getUserId()); + user.setUpdatedTime(LocalDateTime.now()); + + userDataService.updateById(user); + + log.info("更新用户成功:{}", user.getUsername()); + + return convertToVO(user); + } + + @Override + public void deleteUser(String id) { + User user = userDataService.getById(id); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + // 不能删除自己 + if (id.equals(UserContext.getUserId())) { + throw new BusinessException("不能删除自己"); + } + + // 不能删除管理员 + if ("ADMIN".equals(user.getRole())) { + throw new BusinessException("不能删除管理员"); + } + + userDataService.removeById(id); + + log.info("删除用户成功:{}", user.getUsername()); + } + + @Override + public void updateStatus(String id, Integer status) { + User user = userDataService.getById(id); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + // 不能禁用自己 + if (id.equals(UserContext.getUserId())) { + throw new BusinessException("不能禁用自己"); + } + + // 不能禁用管理员 + if ("ADMIN".equals(user.getRole())) { + throw new BusinessException("不能禁用管理员"); + } + + user.setStatus(status); + user.setUpdatedBy(UserContext.getUserId()); + user.setUpdatedTime(LocalDateTime.now()); + + userDataService.updateById(user); + + log.info("更新用户状态成功:{} -> {}", user.getUsername(), status); + } + + @Override + public Page pageUsers(Integer pageNum, Integer pageSize, String name, String username, Integer status) { + Page page = new Page<>(pageNum, pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(StringUtils.hasText(name), User::getName, name) + .like(StringUtils.hasText(username), User::getUsername, username) + .eq(status != null, User::getStatus, status) + .orderByDesc(User::getCreatedTime); + + Page userPage = userDataService.page(page, wrapper); + + // 转换为 VO + Page voPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal()); + voPage.setRecords(userPage.getRecords().stream() + .map(this::convertToVO) + .toList()); + + return voPage; + } + + @Override + public UserVO getUserById(String id) { + User user = userDataService.getById(id); + if (user == null) { + throw new BusinessException("用户不存在"); + } + return convertToVO(user); + } + + @Override + public void resetPassword(String id, String newPassword) { + User user = userDataService.getById(id); + if (user == null) { + throw new BusinessException("用户不存在"); + } + + user.setPassword(PasswordUtil.encode(newPassword)); + user.setUpdatedBy(UserContext.getUserId()); + user.setUpdatedTime(LocalDateTime.now()); + + userDataService.updateById(user); + + log.info("重置用户密码成功:{}", user.getUsername()); + } + + /** + * 转换为 VO + */ + private UserVO convertToVO(User user) { + UserVO vo = new UserVO(); + BeanUtils.copyProperties(user, vo); + return vo; + } +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/vo/LogVO.java b/worklog-api/src/main/java/com/wjbl/worklog/vo/LogVO.java new file mode 100644 index 0000000..17cfd51 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/vo/LogVO.java @@ -0,0 +1,66 @@ +package com.wjbl.worklog.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 日志信息 VO + */ +@Data +@Schema(description = "日志信息") +public class LogVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志ID + */ + @Schema(description = "日志ID") + private String id; + + /** + * 用户ID + */ + @Schema(description = "用户ID") + private String userId; + + /** + * 日志日期 + */ + @Schema(description = "日志日期") + private LocalDate logDate; + + /** + * 日志标题 + */ + @Schema(description = "日志标题") + private String title; + + /** + * 日志内容(Markdown格式) + */ + @Schema(description = "日志内容(Markdown格式)") + private String content; + + /** + * 模板ID + */ + @Schema(description = "模板ID") + private String templateId; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private LocalDateTime createdTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private LocalDateTime updatedTime; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/vo/LoginVO.java b/worklog-api/src/main/java/com/wjbl/worklog/vo/LoginVO.java new file mode 100644 index 0000000..b352eb2 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/vo/LoginVO.java @@ -0,0 +1,28 @@ +package com.wjbl.worklog.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 登录响应 VO + */ +@Data +@Schema(description = "登录响应") +public class LoginVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Token + */ + @Schema(description = "Token") + private String token; + + /** + * 用户信息 + */ + @Schema(description = "用户信息") + private UserInfoVO userInfo; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/vo/TemplateVO.java b/worklog-api/src/main/java/com/wjbl/worklog/vo/TemplateVO.java new file mode 100644 index 0000000..72e7dd1 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/vo/TemplateVO.java @@ -0,0 +1,53 @@ +package com.wjbl.worklog.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 模板信息 VO + */ +@Data +@Schema(description = "模板信息") +public class TemplateVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 模板ID + */ + @Schema(description = "模板ID") + private String id; + + /** + * 模板名称 + */ + @Schema(description = "模板名称") + private String templateName; + + /** + * 模板内容(Markdown格式) + */ + @Schema(description = "模板内容(Markdown格式)") + private String content; + + /** + * 状态(0-禁用,1-启用) + */ + @Schema(description = "状态(0-禁用,1-启用)") + private Integer status; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private LocalDateTime createdTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private LocalDateTime updatedTime; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/vo/UserInfoVO.java b/worklog-api/src/main/java/com/wjbl/worklog/vo/UserInfoVO.java new file mode 100644 index 0000000..ff36c47 --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/vo/UserInfoVO.java @@ -0,0 +1,40 @@ +package com.wjbl.worklog.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户信息 VO + */ +@Data +@Schema(description = "用户信息") +public class UserInfoVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Schema(description = "用户ID") + private String id; + + /** + * 用户名 + */ + @Schema(description = "用户名") + private String username; + + /** + * 姓名 + */ + @Schema(description = "姓名") + private String name; + + /** + * 角色(USER-普通用户,ADMIN-管理员) + */ + @Schema(description = "角色") + private String role; +} diff --git a/worklog-api/src/main/java/com/wjbl/worklog/vo/UserVO.java b/worklog-api/src/main/java/com/wjbl/worklog/vo/UserVO.java new file mode 100644 index 0000000..c834ffc --- /dev/null +++ b/worklog-api/src/main/java/com/wjbl/worklog/vo/UserVO.java @@ -0,0 +1,83 @@ +package com.wjbl.worklog.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户信息 VO + */ +@Data +@Schema(description = "用户信息") +public class UserVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 用户ID + */ + @Schema(description = "用户ID") + private String id; + + /** + * 登录账号 + */ + @Schema(description = "登录账号") + private String username; + + /** + * 姓名 + */ + @Schema(description = "姓名") + private String name; + + /** + * 联系方式 + */ + @Schema(description = "联系方式") + private String phone; + + /** + * 电子邮箱 + */ + @Schema(description = "电子邮箱") + private String email; + + /** + * 职位 + */ + @Schema(description = "职位") + private String position; + + /** + * 描述 + */ + @Schema(description = "描述") + private String description; + + /** + * 状态(0-禁用,1-启用) + */ + @Schema(description = "状态(0-禁用,1-启用)") + private Integer status; + + /** + * 角色(USER-普通用户,ADMIN-管理员) + */ + @Schema(description = "角色") + private String role; + + /** + * 创建时间 + */ + @Schema(description = "创建时间") + private LocalDateTime createdTime; + + /** + * 更新时间 + */ + @Schema(description = "更新时间") + private LocalDateTime updatedTime; +} diff --git a/worklog-api/src/main/resources/env.properties b/worklog-api/src/main/resources/env.properties new file mode 100644 index 0000000..a1cee8a --- /dev/null +++ b/worklog-api/src/main/resources/env.properties @@ -0,0 +1,86 @@ +# ==================================================== +# 工作日志服务平台 - 统一环境配置 +# 说明:所有服务共用的环境配置,包括数据库、缓存、注册中心等 +# ==================================================== + +# ==================== 数据库配置 ==================== +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=worklog +DB_USERNAME=worklog +DB_PASSWORD=Wlog@123 +DB_URL=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + +# 连接池配置 +DB_POOL_MIN_IDLE=5 +DB_POOL_MAX_SIZE=20 +DB_POOL_CONNECTION_TIMEOUT=30000 +DB_POOL_IDLE_TIMEOUT=600000 +DB_POOL_MAX_LIFETIME=1800000 + +# ==================== Redis 配置 ==================== +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=zjf@123456 +REDIS_DATABASE=0 +REDIS_TIMEOUT=5000 + +# Redis 连接池配置 +REDIS_POOL_MAX_ACTIVE=8 +REDIS_POOL_MAX_WAIT=-1 +REDIS_POOL_MAX_IDLE=8 +REDIS_POOL_MIN_IDLE=0 + +# ==================== Nacos 配置 ==================== +NACOS_SERVER_ADDR=localhost:8848 +NACOS_NAMESPACE=worklog-dev +NACOS_GROUP=DEFAULT_GROUP +NACOS_USERNAME=nacos +NACOS_PASSWORD=nacos + +# ==================== 文件上传配置 ==================== +FILE_UPLOAD_MAX_SIZE=50MB +FILE_UPLOAD_MAX_REQUEST_SIZE=100MB +FILE_STORAGE_PATH=./uploads + +# ==================== 腾讯云 COS 配置 ==================== +COS_ENABLED=false +COS_APP_ID=1308258046 +COS_SECRET_ID=AKIDukKfkY5LK2SbU6QTM7csugCSSDjzyiDS +COS_SECRET_KEY=0lHXYIn20jDRP7ZlhNnyub3GEwObZHjw +COS_BUCKET_NAME=test-1308258046 +COS_BUCKET_HOST=https://test-1308258046.cos.ap-beijing.myqcloud.com +COS_REGION=ap-beijing + +# ==================== 日志配置 ==================== +# 日志路径(扁平目录结构,无子目录) +LOG_PATH=./logs + +# 日志级别 +LOG_LEVEL_ROOT=INFO +LOG_LEVEL_APP=DEBUG + +# 日志文件配置 +LOG_FILE_MAX_SIZE=100MB +LOG_FILE_MAX_HISTORY=30 + +# ==================== JVM 配置 ==================== +JVM_XMS=512m +JVM_XMX=1024m +JVM_METASPACE_SIZE=128m +JVM_MAX_METASPACE_SIZE=256m + +# GC 配置 +JVM_GC_TYPE=G1GC +JVM_MAX_GC_PAUSE_MILLIS=200 + +# ==================== 业务配置 ==================== +# Token 配置 +TOKEN_EXPIRE_TIME=86400 +TOKEN_PREFIX=auth:token: + +# 日志内容限制 +WORKLOG_MAX_CONTENT_LENGTH=2000 + +# 允许的上传文件扩展名 +UPLOAD_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx diff --git a/worklog-api/src/main/resources/env.properties.example b/worklog-api/src/main/resources/env.properties.example new file mode 100644 index 0000000..b700e85 --- /dev/null +++ b/worklog-api/src/main/resources/env.properties.example @@ -0,0 +1,89 @@ +# ==================================================== +# 工作日志服务平台 - 统一环境配置示例 +# 说明: +# 1. 复制本文件为 env.properties +# 2. 根据实际环境修改配置参数 +# 3. 敏感信息请勿提交到代码仓库 +# ==================================================== + +# ==================== 数据库配置 ==================== +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=worklog +DB_USERNAME=your_username +DB_PASSWORD=your_password +DB_URL=jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true + +# 连接池配置 +DB_POOL_MIN_IDLE=5 +DB_POOL_MAX_SIZE=20 +DB_POOL_CONNECTION_TIMEOUT=30000 +DB_POOL_IDLE_TIMEOUT=600000 +DB_POOL_MAX_LIFETIME=1800000 + +# ==================== Redis 配置 ==================== +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=your_redis_password +REDIS_DATABASE=0 +REDIS_TIMEOUT=5000 + +# Redis 连接池配置 +REDIS_POOL_MAX_ACTIVE=8 +REDIS_POOL_MAX_WAIT=-1 +REDIS_POOL_MAX_IDLE=8 +REDIS_POOL_MIN_IDLE=0 + +# ==================== Nacos 配置 ==================== +NACOS_SERVER_ADDR=localhost:8848 +NACOS_NAMESPACE=worklog-dev +NACOS_GROUP=DEFAULT_GROUP +NACOS_USERNAME=nacos +NACOS_PASSWORD=nacos + +# ==================== 文件上传配置 ==================== +FILE_UPLOAD_MAX_SIZE=50MB +FILE_UPLOAD_MAX_REQUEST_SIZE=100MB +FILE_STORAGE_PATH=./uploads + +# ==================== 腾讯云 COS 配置 ==================== +COS_ENABLED=false +COS_APP_ID=your_app_id +COS_SECRET_ID=your_secret_id +COS_SECRET_KEY=your_secret_key +COS_BUCKET_NAME=your_bucket_name +COS_BUCKET_HOST=https://your_bucket.cos.region.myqcloud.com +COS_REGION=ap-beijing + +# ==================== 日志配置 ==================== +# 日志路径(扁平目录结构,无子目录) +LOG_PATH=./logs + +# 日志级别 +LOG_LEVEL_ROOT=INFO +LOG_LEVEL_APP=DEBUG + +# 日志文件配置 +LOG_FILE_MAX_SIZE=100MB +LOG_FILE_MAX_HISTORY=30 + +# ==================== JVM 配置 ==================== +JVM_XMS=512m +JVM_XMX=1024m +JVM_METASPACE_SIZE=128m +JVM_MAX_METASPACE_SIZE=256m + +# GC 配置 +JVM_GC_TYPE=G1GC +JVM_MAX_GC_PAUSE_MILLIS=200 + +# ==================== 业务配置 ==================== +# Token 配置 +TOKEN_EXPIRE_TIME=86400 +TOKEN_PREFIX=auth:token: + +# 日志内容限制 +WORKLOG_MAX_CONTENT_LENGTH=2000 + +# 允许的上传文件扩展名 +UPLOAD_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx diff --git a/worklog-api/src/main/resources/logback-spring.xml b/worklog-api/src/main/resources/logback-spring.xml index 9f3b57a..83a646d 100644 --- a/worklog-api/src/main/resources/logback-spring.xml +++ b/worklog-api/src/main/resources/logback-spring.xml @@ -1,21 +1,27 @@ - - + + + + + + + + - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{50} - %msg%n + ${LOG_PATTERN} UTF-8 - + ${LOG_PATH}/app.log - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{50} - %msg%n + ${LOG_PATTERN} UTF-8 @@ -27,11 +33,11 @@ - + ${LOG_PATH}/sql.log - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{50} - %msg%n + ${LOG_PATTERN} UTF-8 @@ -48,9 +54,15 @@ + + + + + + - + diff --git a/worklog-api/src/main/resources/service.properties b/worklog-api/src/main/resources/service.properties new file mode 100644 index 0000000..a811892 --- /dev/null +++ b/worklog-api/src/main/resources/service.properties @@ -0,0 +1,29 @@ +# ==================================================== +# 工作日志服务平台 - 服务个性化配置 +# 说明:当前服务独立的配置,可覆盖 env.properties 中的同名参数 +# ==================================================== + +# ==================== 服务基本信息 ==================== +# 服务名称 +APP_NAME=worklog-api + +# 实例名称(多租户场景使用,默认与 APP_NAME 相同) +INSTANCE_NAME=${APP_NAME} + +# 租户标识(多租户场景使用,用于路由,单租户留空) +TENANT_ID= + +# ==================== 服务端口配置 ==================== +# 服务端口(可覆盖 application.yml 中配置) +SERVER_PORT=8080 + +# ==================== 环境标识 ==================== +# 运行环境:dev-开发, test-测试, prod-生产 +SPRING_PROFILES_ACTIVE=prod + +# ==================== 个性化覆盖配置(可选) ==================== +# 如果当前服务需要使用不同的日志路径,可在此覆盖 +# LOG_PATH=/var/logs/worklog-api + +# 如果当前服务需要使用不同的日志级别,可在此覆盖 +# LOG_LEVEL_APP=INFO diff --git a/worklog-api/src/main/resources/service.properties.example b/worklog-api/src/main/resources/service.properties.example new file mode 100644 index 0000000..3d827f0 --- /dev/null +++ b/worklog-api/src/main/resources/service.properties.example @@ -0,0 +1,32 @@ +# ==================================================== +# 工作日志服务平台 - 服务个性化配置示例 +# 说明: +# 1. 复制本文件为 service.properties +# 2. 根据实际环境修改配置参数 +# 3. 可覆盖 env.properties 中的同名参数 +# ==================================================== + +# ==================== 服务基本信息 ==================== +# 服务名称 +APP_NAME=worklog-api + +# 实例名称(多租户场景使用,默认与 APP_NAME 相同) +INSTANCE_NAME=${APP_NAME} + +# 租户标识(多租户场景使用,用于路由,单租户留空) +TENANT_ID= + +# ==================== 服务端口配置 ==================== +# 服务端口(可覆盖 application.yml 中配置) +SERVER_PORT=8080 + +# ==================== 环境标识 ==================== +# 运行环境:dev-开发, test-测试, prod-生产 +SPRING_PROFILES_ACTIVE=prod + +# ==================== 个性化覆盖配置(可选) ==================== +# 如果当前服务需要使用不同的日志路径,可在此覆盖 +# LOG_PATH=/var/logs/worklog-api + +# 如果当前服务需要使用不同的日志级别,可在此覆盖 +# LOG_LEVEL_APP=INFO diff --git a/worklog-api/src/test/java/com/wjbl/worklog/service/LogServiceTest.java b/worklog-api/src/test/java/com/wjbl/worklog/service/LogServiceTest.java new file mode 100644 index 0000000..6376c9a --- /dev/null +++ b/worklog-api/src/test/java/com/wjbl/worklog/service/LogServiceTest.java @@ -0,0 +1,208 @@ +package com.wjbl.worklog.service; + +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.context.UserInfo; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.data.entity.WorkLog; +import com.wjbl.worklog.data.service.WorkLogDataService; +import com.wjbl.worklog.dto.LogCreateDTO; +import com.wjbl.worklog.dto.LogUpdateDTO; +import com.wjbl.worklog.service.impl.LogServiceImpl; +import com.wjbl.worklog.vo.LogVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * LogService 单元测试 + */ +@ExtendWith(MockitoExtension.class) +class LogServiceTest { + + @Mock + private WorkLogDataService workLogDataService; + + @InjectMocks + private LogServiceImpl logService; + + private WorkLog testLog; + + @BeforeEach + void setUp() { + testLog = new WorkLog(); + testLog.setId("log-id-123"); + testLog.setUserId("user-id-123"); + testLog.setLogDate(LocalDate.now()); + testLog.setTitle("今日工作日志"); + testLog.setContent("今天完成了xxx任务"); + testLog.setDeleted(0); + + // 设置普通用户上下文 + UserInfo userInfo = new UserInfo("user-id-123", "testuser", "测试用户", "USER"); + UserContext.setUserInfo(userInfo); + } + + @Test + @DisplayName("创建日志 - 成功") + void createLog_success() { + // Given + LogCreateDTO dto = new LogCreateDTO(); + dto.setTitle("新日志"); + dto.setContent("日志内容"); + dto.setLogDate(LocalDate.now()); + + when(workLogDataService.getByUserIdAndLogDate("user-id-123", LocalDate.now())).thenReturn(null); + when(workLogDataService.save(any(WorkLog.class))).thenReturn(true); + + // When + LogVO result = logService.createLog(dto); + + // Then + assertNotNull(result); + assertEquals("新日志", result.getTitle()); + verify(workLogDataService).save(any(WorkLog.class)); + } + + @Test + @DisplayName("创建日志 - 当天已有日志") + void createLog_alreadyExists() { + // Given + LogCreateDTO dto = new LogCreateDTO(); + dto.setTitle("新日志"); + dto.setContent("日志内容"); + dto.setLogDate(LocalDate.now()); + + when(workLogDataService.getByUserIdAndLogDate("user-id-123", LocalDate.now())).thenReturn(testLog); + + // When & Then + assertThrows(BusinessException.class, () -> logService.createLog(dto)); + verify(workLogDataService, never()).save(any(WorkLog.class)); + } + + @Test + @DisplayName("更新日志 - 成功(自己的日志)") + void updateLog_success_ownLog() { + // Given + LogUpdateDTO dto = new LogUpdateDTO(); + dto.setTitle("更新后的标题"); + dto.setContent("更新后的内容"); + + when(workLogDataService.getById("log-id-123")).thenReturn(testLog); + when(workLogDataService.updateById(any(WorkLog.class))).thenReturn(true); + + // When + LogVO result = logService.updateLog("log-id-123", dto); + + // Then + assertNotNull(result); + assertEquals("更新后的标题", result.getTitle()); + } + + @Test + @DisplayName("更新日志 - 无权编辑他人日志") + void updateLog_forbidden_otherUserLog() { + // Given + WorkLog otherUserLog = new WorkLog(); + otherUserLog.setId("other-log-id"); + otherUserLog.setUserId("other-user-id"); // 其他用户的日志 + otherUserLog.setLogDate(LocalDate.now()); + + LogUpdateDTO dto = new LogUpdateDTO(); + dto.setTitle("更新后的标题"); + + when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog); + + // When & Then + assertThrows(BusinessException.class, () -> logService.updateLog("other-log-id", dto)); + verify(workLogDataService, never()).updateById(any(WorkLog.class)); + } + + @Test + @DisplayName("更新日志 - 管理员可编辑他人日志") + void updateLog_success_adminCanEditOtherLog() { + // Given - 设置为管理员 + UserInfo adminInfo = new UserInfo("admin-id", "admin", "管理员", "ADMIN"); + UserContext.setUserInfo(adminInfo); + + WorkLog otherUserLog = new WorkLog(); + otherUserLog.setId("other-log-id"); + otherUserLog.setUserId("other-user-id"); + otherUserLog.setLogDate(LocalDate.now()); + + LogUpdateDTO dto = new LogUpdateDTO(); + dto.setTitle("管理员更新"); + + when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog); + when(workLogDataService.updateById(any(WorkLog.class))).thenReturn(true); + + // When + LogVO result = logService.updateLog("other-log-id", dto); + + // Then + assertNotNull(result); + assertEquals("管理员更新", result.getTitle()); + } + + @Test + @DisplayName("删除日志 - 成功") + void deleteLog_success() { + // Given + when(workLogDataService.getById("log-id-123")).thenReturn(testLog); + when(workLogDataService.removeById("log-id-123")).thenReturn(true); + + // When + logService.deleteLog("log-id-123"); + + // Then + verify(workLogDataService).removeById("log-id-123"); + } + + @Test + @DisplayName("删除日志 - 无权删除他人日志") + void deleteLog_forbidden_otherUserLog() { + // Given + WorkLog otherUserLog = new WorkLog(); + otherUserLog.setId("other-log-id"); + otherUserLog.setUserId("other-user-id"); + + when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog); + + // When & Then + assertThrows(BusinessException.class, () -> logService.deleteLog("other-log-id")); + verify(workLogDataService, never()).removeById(any()); + } + + @Test + @DisplayName("获取日志详情 - 成功") + void getLogById_success() { + // Given + when(workLogDataService.getById("log-id-123")).thenReturn(testLog); + + // When + LogVO result = logService.getLogById("log-id-123"); + + // Then + assertNotNull(result); + assertEquals("今日工作日志", result.getTitle()); + } + + @Test + @DisplayName("获取日志详情 - 日志不存在") + void getLogById_notFound() { + // Given + when(workLogDataService.getById("nonexistent")).thenReturn(null); + + // When & Then + assertThrows(BusinessException.class, () -> logService.getLogById("nonexistent")); + } +} diff --git a/worklog-api/src/test/java/com/wjbl/worklog/service/TemplateServiceTest.java b/worklog-api/src/test/java/com/wjbl/worklog/service/TemplateServiceTest.java new file mode 100644 index 0000000..18fc423 --- /dev/null +++ b/worklog-api/src/test/java/com/wjbl/worklog/service/TemplateServiceTest.java @@ -0,0 +1,210 @@ +package com.wjbl.worklog.service; + +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.context.UserInfo; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.data.entity.LogTemplate; +import com.wjbl.worklog.data.service.LogTemplateDataService; +import com.wjbl.worklog.dto.TemplateCreateDTO; +import com.wjbl.worklog.dto.TemplateUpdateDTO; +import com.wjbl.worklog.service.impl.TemplateServiceImpl; +import com.wjbl.worklog.vo.TemplateVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * TemplateService 单元测试 + */ +@ExtendWith(MockitoExtension.class) +class TemplateServiceTest { + + @Mock + private LogTemplateDataService logTemplateDataService; + + @InjectMocks + private TemplateServiceImpl templateService; + + private LogTemplate testTemplate; + + @BeforeEach + void setUp() { + testTemplate = new LogTemplate(); + testTemplate.setId("template-id-123"); + testTemplate.setTemplateName("日报模板"); + testTemplate.setContent("# 今日工作\n\n## 完成事项\n\n## 明日计划"); + testTemplate.setStatus(1); + testTemplate.setDeleted(0); + + // 设置用户上下文 + UserInfo userInfo = new UserInfo("admin-id", "admin", "管理员", "ADMIN"); + UserContext.setUserInfo(userInfo); + } + + @Test + @DisplayName("创建模板 - 成功") + void createTemplate_success() { + // Given + TemplateCreateDTO dto = new TemplateCreateDTO(); + dto.setTemplateName("周报模板"); + dto.setContent("# 本周工作\n\n## 完成事项\n\n## 下周计划"); + + when(logTemplateDataService.getByTemplateName("周报模板")).thenReturn(null); + when(logTemplateDataService.save(any(LogTemplate.class))).thenReturn(true); + + // When + TemplateVO result = templateService.createTemplate(dto); + + // Then + assertNotNull(result); + assertEquals("周报模板", result.getTemplateName()); + verify(logTemplateDataService).save(any(LogTemplate.class)); + } + + @Test + @DisplayName("创建模板 - 模板名称已存在") + void createTemplate_nameExists() { + // Given + TemplateCreateDTO dto = new TemplateCreateDTO(); + dto.setTemplateName("日报模板"); + dto.setContent("内容"); + + when(logTemplateDataService.getByTemplateName("日报模板")).thenReturn(testTemplate); + + // When & Then + assertThrows(BusinessException.class, () -> templateService.createTemplate(dto)); + verify(logTemplateDataService, never()).save(any(LogTemplate.class)); + } + + @Test + @DisplayName("更新模板 - 成功") + void updateTemplate_success() { + // Given + TemplateUpdateDTO dto = new TemplateUpdateDTO(); + dto.setContent("更新后的内容"); + + when(logTemplateDataService.getById("template-id-123")).thenReturn(testTemplate); + when(logTemplateDataService.updateById(any(LogTemplate.class))).thenReturn(true); + + // When + TemplateVO result = templateService.updateTemplate("template-id-123", dto); + + // Then + assertNotNull(result); + assertEquals("更新后的内容", result.getContent()); + } + + @Test + @DisplayName("更新模板 - 模板不存在") + void updateTemplate_notFound() { + // Given + TemplateUpdateDTO dto = new TemplateUpdateDTO(); + dto.setContent("内容"); + + when(logTemplateDataService.getById("nonexistent")).thenReturn(null); + + // When & Then + assertThrows(BusinessException.class, () -> templateService.updateTemplate("nonexistent", dto)); + } + + @Test + @DisplayName("更新模板 - 新名称已被占用") + void updateTemplate_nameConflict() { + // Given + TemplateUpdateDTO dto = new TemplateUpdateDTO(); + dto.setTemplateName("已存在的模板名"); + + LogTemplate existingTemplate = new LogTemplate(); + existingTemplate.setId("other-template-id"); + existingTemplate.setTemplateName("已存在的模板名"); + + when(logTemplateDataService.getById("template-id-123")).thenReturn(testTemplate); + when(logTemplateDataService.getByTemplateName("已存在的模板名")).thenReturn(existingTemplate); + + // When & Then + assertThrows(BusinessException.class, () -> templateService.updateTemplate("template-id-123", dto)); + } + + @Test + @DisplayName("删除模板 - 成功") + void deleteTemplate_success() { + // Given + when(logTemplateDataService.getById("template-id-123")).thenReturn(testTemplate); + when(logTemplateDataService.removeById("template-id-123")).thenReturn(true); + + // When + templateService.deleteTemplate("template-id-123"); + + // Then + verify(logTemplateDataService).removeById("template-id-123"); + } + + @Test + @DisplayName("删除模板 - 模板不存在") + void deleteTemplate_notFound() { + // Given + when(logTemplateDataService.getById("nonexistent")).thenReturn(null); + + // When & Then + assertThrows(BusinessException.class, () -> templateService.deleteTemplate("nonexistent")); + verify(logTemplateDataService, never()).removeById(any()); + } + + @Test + @DisplayName("更新模板状态 - 成功") + void updateStatus_success() { + // Given + when(logTemplateDataService.getById("template-id-123")).thenReturn(testTemplate); + when(logTemplateDataService.updateById(any(LogTemplate.class))).thenReturn(true); + + // When + templateService.updateStatus("template-id-123", 0); + + // Then + verify(logTemplateDataService).updateById(any(LogTemplate.class)); + } + + @Test + @DisplayName("获取启用的模板列表 - 成功") + void listEnabledTemplates_success() { + // Given + when(logTemplateDataService.listEnabledTemplates()).thenReturn(List.of(testTemplate)); + + // When + List result = templateService.listEnabledTemplates(); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("日报模板", result.get(0).getTemplateName()); + } + + @Test + @DisplayName("获取所有模板列表 - 成功") + void listAllTemplates_success() { + // Given + LogTemplate disabledTemplate = new LogTemplate(); + disabledTemplate.setId("disabled-id"); + disabledTemplate.setTemplateName("禁用模板"); + disabledTemplate.setStatus(0); + + when(logTemplateDataService.list()).thenReturn(List.of(testTemplate, disabledTemplate)); + + // When + List result = templateService.listAllTemplates(); + + // Then + assertNotNull(result); + assertEquals(2, result.size()); + } +} diff --git a/worklog-api/src/test/java/com/wjbl/worklog/service/UserServiceTest.java b/worklog-api/src/test/java/com/wjbl/worklog/service/UserServiceTest.java new file mode 100644 index 0000000..8128936 --- /dev/null +++ b/worklog-api/src/test/java/com/wjbl/worklog/service/UserServiceTest.java @@ -0,0 +1,217 @@ +package com.wjbl.worklog.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.wjbl.worklog.common.context.UserContext; +import com.wjbl.worklog.common.context.UserInfo; +import com.wjbl.worklog.common.exception.BusinessException; +import com.wjbl.worklog.data.entity.User; +import com.wjbl.worklog.data.service.UserDataService; +import com.wjbl.worklog.dto.UserCreateDTO; +import com.wjbl.worklog.dto.UserUpdateDTO; +import com.wjbl.worklog.service.impl.UserServiceImpl; +import com.wjbl.worklog.vo.UserVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * UserService 单元测试 + */ +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserDataService userDataService; + + @InjectMocks + private UserServiceImpl userService; + + private User testUser; + + @BeforeEach + void setUp() { + testUser = new User(); + testUser.setId("1234567890"); + testUser.setUsername("testuser"); + testUser.setPassword("$2a$10$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + testUser.setName("测试用户"); + testUser.setRole("USER"); + testUser.setStatus(1); + testUser.setDeleted(0); + + // 设置当前用户上下文 + UserInfo userInfo = new UserInfo("admin-id", "admin", "管理员", "ADMIN"); + UserContext.setUserInfo(userInfo); + } + + @Test + @DisplayName("创建用户 - 成功") + void createUser_success() { + // Given + UserCreateDTO dto = new UserCreateDTO(); + dto.setUsername("newuser"); + dto.setPassword("password123"); + dto.setName("新用户"); + + when(userDataService.getByUsername("newuser")).thenReturn(null); + when(userDataService.save(any(User.class))).thenReturn(true); + + // When + UserVO result = userService.createUser(dto); + + // Then + assertNotNull(result); + assertEquals("newuser", result.getUsername()); + assertEquals("新用户", result.getName()); + verify(userDataService).save(any(User.class)); + } + + @Test + @DisplayName("创建用户 - 用户名已存在") + void createUser_usernameExists() { + // Given + UserCreateDTO dto = new UserCreateDTO(); + dto.setUsername("testuser"); + dto.setPassword("password123"); + dto.setName("新用户"); + + when(userDataService.getByUsername("testuser")).thenReturn(testUser); + + // When & Then + assertThrows(BusinessException.class, () -> userService.createUser(dto)); + verify(userDataService, never()).save(any(User.class)); + } + + @Test + @DisplayName("更新用户 - 成功") + void updateUser_success() { + // Given + UserUpdateDTO dto = new UserUpdateDTO(); + dto.setName("更新后的名字"); + + when(userDataService.getById("1234567890")).thenReturn(testUser); + when(userDataService.updateById(any(User.class))).thenReturn(true); + + // When + UserVO result = userService.updateUser("1234567890", dto); + + // Then + assertNotNull(result); + assertEquals("更新后的名字", result.getName()); + } + + @Test + @DisplayName("更新用户 - 用户不存在") + void updateUser_userNotFound() { + // Given + UserUpdateDTO dto = new UserUpdateDTO(); + dto.setName("更新后的名字"); + + when(userDataService.getById("nonexistent")).thenReturn(null); + + // When & Then + assertThrows(BusinessException.class, () -> userService.updateUser("nonexistent", dto)); + } + + @Test + @DisplayName("删除用户 - 成功") + void deleteUser_success() { + // Given + User userToDelete = new User(); + userToDelete.setId("user-to-delete"); + userToDelete.setUsername("deleteuser"); + userToDelete.setRole("USER"); + + when(userDataService.getById("user-to-delete")).thenReturn(userToDelete); + when(userDataService.removeById("user-to-delete")).thenReturn(true); + + // When + userService.deleteUser("user-to-delete"); + + // Then + verify(userDataService).removeById("user-to-delete"); + } + + @Test + @DisplayName("删除用户 - 不能删除自己") + void deleteUser_cannotDeleteSelf() { + // Given + User currentUser = new User(); + currentUser.setId("admin-id"); + currentUser.setUsername("admin"); + currentUser.setRole("ADMIN"); + + when(userDataService.getById("admin-id")).thenReturn(currentUser); + + // When & Then + assertThrows(BusinessException.class, () -> userService.deleteUser("admin-id")); + verify(userDataService, never()).removeById(any()); + } + + @Test + @DisplayName("删除用户 - 不能删除管理员") + void deleteUser_cannotDeleteAdmin() { + // Given + User adminUser = new User(); + adminUser.setId("other-admin"); + adminUser.setUsername("otheradmin"); + adminUser.setRole("ADMIN"); + + when(userDataService.getById("other-admin")).thenReturn(adminUser); + + // When & Then + assertThrows(BusinessException.class, () -> userService.deleteUser("other-admin")); + verify(userDataService, never()).removeById(any()); + } + + @Test + @DisplayName("分页查询用户 - 成功") + void pageUsers_success() { + // Given + Page userPage = new Page<>(1, 10); + userPage.setRecords(java.util.List.of(testUser)); + userPage.setTotal(1); + + when(userDataService.page(any(Page.class), any())).thenReturn(userPage); + + // When + Page result = userService.pageUsers(1, 10, null, null, null); + + // Then + assertNotNull(result); + assertEquals(1, result.getTotal()); + assertEquals(1, result.getRecords().size()); + } + + @Test + @DisplayName("根据ID获取用户 - 成功") + void getUserById_success() { + // Given + when(userDataService.getById("1234567890")).thenReturn(testUser); + + // When + UserVO result = userService.getUserById("1234567890"); + + // Then + assertNotNull(result); + assertEquals("testuser", result.getUsername()); + } + + @Test + @DisplayName("根据ID获取用户 - 用户不存在") + void getUserById_userNotFound() { + // Given + when(userDataService.getById("nonexistent")).thenReturn(null); + + // When & Then + assertThrows(BusinessException.class, () -> userService.getUserById("nonexistent")); + } +}