feat: 完成后端核心业务模块开发

阶段二:认证授权模块
- User实体类、Mapper、DataService
- Token服务(Redis存储)、密码加密(BCrypt)
- 认证拦截器、UserContext上下文
- 登录/登出接口

阶段三:核心业务模块
- 用户管理:CRUD、状态管理、密码重置
- 模板管理:CRUD、状态管理
- 工作日志:CRUD、权限控制

配置分离架构
- env.properties(环境敏感配置)
- service.properties(服务配置)
- logback-spring.xml更新

部署脚本
- deploy/目录(Nginx配置、启停脚本、备份脚本)

单元测试:29个测试全部通过
This commit is contained in:
zhangjf 2026-02-24 16:10:26 +08:00
parent ae33bd4d6a
commit dbcc06edbc
65 changed files with 5254 additions and 57 deletions

6
.gitignore vendored
View File

@ -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/

411
deploy/DEPLOY.md Normal file
View File

@ -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 <file> # 恢复
# 日志查看
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 # 系统负载
```
## 联系方式
如有问题,请联系运维团队。

129
deploy/nginx/worklog.conf Normal file
View File

@ -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;
# }

198
deploy/scripts/backup.sh Executable file
View File

@ -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 <backup_file.sql.gz>"
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 "$@"

101
deploy/scripts/init_db.sh Executable file
View File

@ -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 "========================================"

34
deploy/scripts/restart.sh Executable file
View File

@ -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 "========================================="

110
deploy/scripts/start.sh Executable file
View File

@ -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

74
deploy/scripts/status.sh Executable file
View File

@ -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

60
deploy/scripts/stop.sh Executable file
View File

@ -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

View File

@ -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
```
---

View File

@ -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 配置加载顺序

View File

@ -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
<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="./logs"/>
<springProperty scope="context" name="LOG_LEVEL_ROOT" source="logging.level.root" defaultValue="INFO"/>
<springProperty scope="context" name="LOG_LEVEL_APP" source="logging.level.app" defaultValue="DEBUG"/>
```
2. **日志格式标准化**(强制包含 traceId 和 spanId
```xml
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n"/>
```
3. **日志文件命名**(遵循 7.4 规范):
- 主日志:`app.log`(所有级别)
- SQL 日志:`sql.log`MyBatis-Plus 独立输出)
- 所有日志输出到 `logs/` 目录(扁平结构,无子目录)
4. **新增 MyBatis-Plus 框架日志配置**
```xml
<logger name="com.baomidou.mybatisplus" level="DEBUG" additivity="false">
<appender-ref ref="SQL_FILE"/>
<appender-ref ref="CONSOLE"/>
</logger>
```
### 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.logMyBatis-Plus 独立输出)
- 所有日志包含 traceId 和 spanId便于链路追踪
4. **环境变量支持**
- 启动脚本会自动将 properties 文件的配置导出为环境变量
- Spring Boot 和 Logback 可通过环境变量或 ${} 占位符读取配置
## 后续计划
根据架构设计规范,后续可继续完善:
1. **Maven Assembly 打包配置**7.3 规范):
- 配置 assembly 插件
- 自动打包为标准目录结构bin/ lib/ conf/
2. **多环境配置优化**
- 如需支持多环境,可创建多套 env.propertiesenv-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) - 部署指南

View File

@ -54,6 +54,12 @@
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring Security Crypto (用于密码加密) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
@ -96,12 +102,6 @@
<version>${commonmark.version}</version>
</dependency>
<!-- BCrypt 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -0,0 +1,74 @@
package com.wjbl.worklog.common.context;
/**
* 用户上下文工具类
* 使用 ThreadLocal 存储当前用户信息
*/
public class UserContext {
private static final ThreadLocal<UserInfo> 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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<LoginVO> login(@Valid @RequestBody LoginDTO loginDTO) {
LoginVO loginVO = authService.login(loginDTO);
return Result.success(loginVO);
}
/**
* 用户登出
*/
@Operation(summary = "用户登出", description = "退出登录,使 Token 失效")
@PostMapping("/logout")
public Result<Void> 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();
}
}

View File

@ -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<PageResult<LogVO>> 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<LogVO> page = logService.pageMyLogs(pageNum, pageSize, startDate, endDate);
PageResult<LogVO> 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<PageResult<LogVO>> 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<LogVO> page = logService.pageAllLogs(pageNum, pageSize, userId, startDate, endDate);
PageResult<LogVO> 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<LogVO> getLogById(
@Parameter(description = "日志ID") @PathVariable String id) {
LogVO log = logService.getLogById(id);
return Result.success(log);
}
/**
* 创建日志
*/
@Operation(summary = "创建日志", description = "创建新的工作日志")
@PostMapping
public Result<LogVO> createLog(@Valid @RequestBody LogCreateDTO dto) {
LogVO log = logService.createLog(dto);
return Result.success(log);
}
/**
* 更新日志
*/
@Operation(summary = "更新日志", description = "更新日志信息")
@PutMapping("/{id}")
public Result<LogVO> 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<Void> deleteLog(
@Parameter(description = "日志ID") @PathVariable String id) {
logService.deleteLog(id);
return Result.success();
}
}

View File

@ -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<List<TemplateVO>> listEnabledTemplates() {
List<TemplateVO> templates = templateService.listEnabledTemplates();
return Result.success(templates);
}
/**
* 获取所有模板列表管理员用
*/
@Operation(summary = "获取所有模板列表", description = "管理员获取所有模板列表")
@GetMapping("/list/all")
public Result<List<TemplateVO>> listAllTemplates() {
List<TemplateVO> templates = templateService.listAllTemplates();
return Result.success(templates);
}
/**
* 根据ID获取模板
*/
@Operation(summary = "获取模板详情", description = "根据模板ID获取模板详细信息")
@GetMapping("/{id}")
public Result<TemplateVO> getTemplateById(
@Parameter(description = "模板ID") @PathVariable String id) {
TemplateVO template = templateService.getTemplateById(id);
return Result.success(template);
}
/**
* 创建模板
*/
@Operation(summary = "创建模板", description = "创建新模板")
@PostMapping
public Result<TemplateVO> createTemplate(@Valid @RequestBody TemplateCreateDTO dto) {
TemplateVO template = templateService.createTemplate(dto);
return Result.success(template);
}
/**
* 更新模板
*/
@Operation(summary = "更新模板", description = "更新模板信息")
@PutMapping("/{id}")
public Result<TemplateVO> 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<Void> 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<Void> deleteTemplate(
@Parameter(description = "模板ID") @PathVariable String id) {
templateService.deleteTemplate(id);
return Result.success();
}
}

View File

@ -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<PageResult<UserVO>> 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<UserVO> page = userService.pageUsers(pageNum, pageSize, name, username, status);
PageResult<UserVO> 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<UserVO> getUserById(
@Parameter(description = "用户ID") @PathVariable String id) {
UserVO user = userService.getUserById(id);
return Result.success(user);
}
/**
* 创建用户
*/
@Operation(summary = "创建用户", description = "创建新用户")
@PostMapping
public Result<UserVO> createUser(@Valid @RequestBody UserCreateDTO dto) {
UserVO user = userService.createUser(dto);
return Result.success(user);
}
/**
* 更新用户
*/
@Operation(summary = "更新用户", description = "更新用户信息")
@PutMapping("/{id}")
public Result<UserVO> 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<Void> 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<Void> deleteUser(
@Parameter(description = "用户ID") @PathVariable String id) {
userService.deleteUser(id);
return Result.success();
}
/**
* 重置用户密码
*/
@Operation(summary = "重置密码", description = "重置用户密码")
@PutMapping("/{id}/password")
public Result<Void> resetPassword(
@Parameter(description = "用户ID") @PathVariable String id,
@Parameter(description = "新密码") @RequestParam String newPassword) {
userService.resetPassword(id, newPassword);
return Result.success();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<LogTemplate> {
}

View File

@ -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<User> {
}

View File

@ -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<WorkLog> {
}

View File

@ -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<LogTemplate> {
/**
* 根据模板名称查询模板
*
* @param templateName 模板名称
* @return 模板实体
*/
LogTemplate getByTemplateName(String templateName);
/**
* 查询所有启用的模板
*
* @return 启用的模板列表
*/
List<LogTemplate> listEnabledTemplates();
}

View File

@ -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<User> {
/**
* 根据用户名查询用户
*
* @param username 用户名
* @return 用户实体
*/
User getByUsername(String username);
}

View File

@ -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<WorkLog> {
/**
* 分页查询用户的工作日志
*
* @param page 分页参数
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 分页结果
*/
Page<WorkLog> pageByUserId(Page<WorkLog> page, String userId, LocalDate startDate, LocalDate endDate);
/**
* 分页查询所有工作日志管理员
*
* @param page 分页参数
* @param userId 用户ID可选
* @param startDate 开始日期
* @param endDate 结束日期
* @return 分页结果
*/
Page<WorkLog> pageAll(Page<WorkLog> page, String userId, LocalDate startDate, LocalDate endDate);
/**
* 根据用户ID和日期查询日志
*
* @param userId 用户ID
* @param logDate 日志日期
* @return 工作日志
*/
WorkLog getByUserIdAndLogDate(String userId, LocalDate logDate);
}

View File

@ -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<LogTemplateMapper, LogTemplate> implements LogTemplateDataService {
@Override
public LogTemplate getByTemplateName(String templateName) {
return lambdaQuery()
.eq(LogTemplate::getTemplateName, templateName)
.one();
}
@Override
public List<LogTemplate> listEnabledTemplates() {
return lambdaQuery()
.eq(LogTemplate::getStatus, 1)
.orderByDesc(LogTemplate::getCreatedTime)
.list();
}
}

View File

@ -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<UserMapper, User> implements UserDataService {
@Override
public User getByUsername(String username) {
return lambdaQuery()
.eq(User::getUsername, username)
.one();
}
}

View File

@ -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<WorkLogMapper, WorkLog> implements WorkLogDataService {
@Override
public Page<WorkLog> pageByUserId(Page<WorkLog> page, String userId, LocalDate startDate, LocalDate endDate) {
LambdaQueryWrapper<WorkLog> 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<WorkLog> pageAll(Page<WorkLog> page, String userId, LocalDate startDate, LocalDate endDate) {
LambdaQueryWrapper<WorkLog> 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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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<LogVO> pageMyLogs(Integer pageNum, Integer pageSize, LocalDate startDate, LocalDate endDate);
/**
* 分页查询所有日志管理员
*
* @param pageNum 页码
* @param pageSize 每页大小
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 分页结果
*/
Page<LogVO> pageAllLogs(Integer pageNum, Integer pageSize, String userId, LocalDate startDate, LocalDate endDate);
/**
* 根据ID获取日志
*
* @param id 日志ID
* @return 日志信息
*/
LogVO getLogById(String id);
}

View File

@ -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<TemplateVO> listEnabledTemplates();
/**
* 获取所有模板列表管理员用
*
* @return 模板列表
*/
List<TemplateVO> listAllTemplates();
/**
* 根据ID获取模板
*
* @param id 模板ID
* @return 模板信息
*/
TemplateVO getTemplateById(String id);
}

View File

@ -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);
}

View File

@ -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<UserVO> 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);
}

View File

@ -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("用户登出成功");
}
}

View File

@ -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<LogVO> pageMyLogs(Integer pageNum, Integer pageSize, LocalDate startDate, LocalDate endDate) {
String currentUserId = UserContext.getUserId();
Page<WorkLog> page = new Page<>(pageNum, pageSize);
Page<WorkLog> logPage = workLogDataService.pageByUserId(page, currentUserId, startDate, endDate);
// 转换为 VO
Page<LogVO> voPage = new Page<>(logPage.getCurrent(), logPage.getSize(), logPage.getTotal());
voPage.setRecords(logPage.getRecords().stream()
.map(this::convertToVO)
.toList());
return voPage;
}
@Override
public Page<LogVO> pageAllLogs(Integer pageNum, Integer pageSize, String userId, LocalDate startDate, LocalDate endDate) {
Page<WorkLog> page = new Page<>(pageNum, pageSize);
Page<WorkLog> logPage = workLogDataService.pageAll(page, userId, startDate, endDate);
// 转换为 VO
Page<LogVO> 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;
}
}

View File

@ -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<TemplateVO> listEnabledTemplates() {
List<LogTemplate> templates = logTemplateDataService.listEnabledTemplates();
return templates.stream().map(this::convertToVO).toList();
}
@Override
public List<TemplateVO> listAllTemplates() {
List<LogTemplate> 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;
}
}

View File

@ -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<String, Object> 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 成功");
}
}

View File

@ -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<UserVO> pageUsers(Integer pageNum, Integer pageSize, String name, String username, Integer status) {
Page<User> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<User> 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<User> userPage = userDataService.page(page, wrapper);
// 转换为 VO
Page<UserVO> 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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -1,21 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志文件存储路径 -->
<property name="LOG_PATH" value="logs"/>
<!-- 从环境变量读取配置 -->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="worklog-api"/>
<springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="./logs"/>
<springProperty scope="context" name="LOG_LEVEL_ROOT" source="logging.level.root" defaultValue="INFO"/>
<springProperty scope="context" name="LOG_LEVEL_APP" source="logging.level.app" defaultValue="DEBUG"/>
<!-- 日志格式:强制包含 traceId 和 spanId -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{50} - %msg%n</pattern>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 应用日志文件输出 -->
<!-- 应用日志文件输出 - 直接输出到 logs/ 目录 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{50} - %msg%n</pattern>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
@ -27,11 +33,11 @@
</rollingPolicy>
</appender>
<!-- SQL日志文件输出 -->
<!-- SQL日志文件输出 - MyBatis-Plus SQL 日志独立输出 -->
<appender name="SQL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/sql.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{spanId}] %-5level %logger{50} - %msg%n</pattern>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
@ -49,8 +55,14 @@
<appender-ref ref="CONSOLE"/>
</logger>
<!-- MyBatis-Plus 框架日志 -->
<logger name="com.baomidou.mybatisplus" level="DEBUG" additivity="false">
<appender-ref ref="SQL_FILE"/>
<appender-ref ref="CONSOLE"/>
</logger>
<!-- 根日志级别 -->
<root level="INFO">
<root level="${LOG_LEVEL_ROOT}">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>

View File

@ -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

View File

@ -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

View File

@ -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"));
}
}

View File

@ -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<TemplateVO> 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<TemplateVO> result = templateService.listAllTemplates();
// Then
assertNotNull(result);
assertEquals(2, result.size());
}
}

View File

@ -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<User> 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<UserVO> 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"));
}
}