项目概览
HR 系统后端,基于 Go + Gin + GORM + PostgreSQL 实现。
提供员工和部门的 CRUD 管理,采用分层架构(handler → service → repository),支持接口抽象和依赖注入,便于测试和扩展。
技术栈
- Go 1.23
- Gin(HTTP 框架)
- GORM(ORM)
- PostgreSQL 16(数据库)
- Docker(部署)
- GitHub Actions(CI/CD)
快速开始
前置条件
- Go 1.23+
- Docker & Docker Compose(推荐)
方式一:Docker Compose(推荐)
# 克隆项目
git clone <repo-url>
cd hr
# 启动所有服务(app + postgres)
docker compose up -d
# 初始化种子数据
docker compose exec app ./server seed
服务监听 http://localhost:8080。
方式二:本地运行
# 启动 PostgreSQL
docker compose up -d db
# 安装依赖
go mod tidy
# 运行服务
go run main.go
# 另开终端,初始化种子数据
go run tools/seed.go
验证
curl http://localhost:8080/ping
# {"message":"pong"}
运行测试
# 确保 PostgreSQL 可用,然后:
go test ./...
详见测试文档。
常用操作
# 查看日志
docker compose logs -f app
# 停止服务
docker compose down
# 停止并删除数据卷
docker compose down -v
# 重新构建镜像
docker compose build --no-cache
项目结构
hr/
├── main.go # 入口:配置解析、依赖组装、启动服务
├── config/
│ ├── config.go # Config 结构体,环境变量读取
│ └── db.go # NewDB() 连接 PostgreSQL
├── models/
│ ├── employee.go # Employee 模型
│ └── department.go # Department 模型
├── repository/
│ ├── employee_repo.go # EmployeeRepository 接口 + 实现
│ ├── employee_repo_test.go
│ ├── department_repo.go # DepartmentRepository 接口 + 实现
│ ├── department_repo_test.go
│ └── test_helpers.go # 测试辅助函数
├── service/
│ ├── errors.go # 业务错误定义
│ ├── employee_service.go # EmployeeService 接口 + 实现
│ ├── employee_service_test.go
│ ├── department_service.go # DepartmentService 接口 + 实现
│ └── department_service_test.go
├── handler/
│ ├── router.go # 路由注册
│ ├── middleware.go # 中间件(CORS)
│ ├── employee_handler.go # 员工相关 handler
│ ├── department_handler.go # 部门相关 handler
│ └── employee_handler_test.go # handler 测试
├── tools/
│ └── seed.go # 种子数据工具
├── .github/workflows/
│ └── pipeline.yml # CI/CD
├── Dockerfile # 多阶段构建
├── docker-compose.yml # 本地开发环境
├── go.mod
├── go.sum
├── api_test.http # HTTP 接口测试文件(IDE 用)
└── docs/ # 项目文档(mdbook)
各目录职责
| 目录 | 职责 | 依赖方向 |
|---|---|---|
| config | 配置和数据库初始化 | 被 main 引用 |
| models | 数据结构定义,无逻辑 | 被 repository/service/handler 引用 |
| repository | 数据访问,定义接口 | 依赖 models,被 service 引用 |
| service | 业务逻辑,定义接口 | 依赖 repository 接口 + models,被 handler 引用 |
| handler | HTTP 处理,路由,中间件 | 依赖 service 接口,被 main 组装 |
| tools | 辅助脚本 | 独立运行 |
依赖方向:handler → service → repository → models,不反向依赖。
架构设计
分层架构
请求 → Handler → Service → Repository → 数据库
三层各有明确职责:
- Handler:解析 HTTP 请求(参数绑定、校验),调用 Service,返回 HTTP 响应。不包含业务逻辑。
- Service:业务逻辑(如邮箱去重、部门存在性校验),调用 Repository 持久化。不关心 HTTP 细节。
- Repository:数据访问,纯 GORM 操作。不包含业务判断。
接口抽象
每一层对外暴露接口,内部是私有实现:
// repository 层
type EmployeeRepository interface { ... }
func NewEmployeeRepository(db *gorm.DB) EmployeeRepository
// service 层
type EmployeeService interface { ... }
func NewEmployeeService(empRepo EmployeeRepository, deptRepo DepartmentRepository) EmployeeService
// handler 层
type EmployeeHandler struct { service EmployeeService }
func NewEmployeeHandler(s EmployeeService) *EmployeeHandler
这样做的好处:
- 测试时可以用 mock 替换任意层
- 替换实现(比如换数据库)不影响上层代码
- 依赖关系清晰,不会循环引用
依赖注入
所有依赖在 main.go 中组装:
func main() {
cfg := config.Load()
db := config.NewDB(cfg)
empRepo := repository.NewEmployeeRepository(db)
deptRepo := repository.NewDepartmentRepository(db)
empSvc := service.NewEmployeeService(empRepo, deptRepo)
deptSvc := service.NewDepartmentService(deptRepo)
empHandler := handler.NewEmployeeHandler(empSvc)
deptHandler := handler.NewDepartmentHandler(deptSvc)
r := handler.SetupRouter(empHandler, deptHandler)
r.Run(":" + cfg.Port)
}
不使用全局变量,所有依赖通过构造函数传入。
错误处理
业务错误定义在 service/errors.go:
var (
ErrEmailExists = errors.New("email already exists")
ErrDeptNotFound = errors.New("department not found")
)
Service 返回 sentinel error,Handler 用 errors.Is 判断后返回对应 HTTP 状态码。Repository 层直接返回 GORM 错误,不包装。
API 文档
所有接口返回格式:
{
"code": 200,
"message": "错误信息(仅失败时)",
"data": {}
}
需要认证的接口需在请求头携带:Authorization: Bearer <token>
认证
注册
POST /auth/register
请求体:
{
"username": "admin",
"password": "123456",
"role_ids": [1]
}
username、password 为必填,password 最少 6 位。role_ids 可选:
- 不传则默认 staff 角色
- 第一个注册的用户自动成为 admin(忽略传入的 role_ids)
- 非首次注册时 role_ids 仅 admin 可指定
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 用户名已存在 | 400 | username already exists |
登录
POST /auth/login
请求体:
{
"username": "admin",
"password": "123456"
}
响应:
{
"code": 200,
"data": { "token": "eyJhbGciOi..." }
}
JWT Token 存入 Redis,过期时间 24 小时。
Token payload 包含以下字段:
{
"user_id": 1,
"username": "admin",
"role": "admin",
"employee_id": 3,
"jti": "xxx",
"exp": 1746000000
}
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | uint | 用户 ID |
| username | string | 用户名 |
| role | string | 角色:admin / manager / staff |
| employee_id | *uint | 关联的员工 ID,未关联时不存在此字段 |
| 响应 | code | message |
|---|---|---|
| 账号被禁用 | 403 | user is disabled |
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 用户名或密码错误 | 401 | invalid username or password |
获取当前用户信息
GET /auth/me
需要 JWT 认证。
响应示例:
{
"code": 200,
"data": {
"id": 1,
"username": "admin"
}
}
错误响应:
| 状态码 | 说明 |
|---|---|
| 401 | 未认证 |
| 404 | 用户不存在 |
注销
POST /auth/logout
需要认证。从 Redis 删除对应 Token。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 未认证 | 401 | unauthorized |
部门
创建部门
POST /depts(需认证,admin/manager 且为部门负责人)
请求体:
{
"name": "研发部",
"description": "负责产品研发和技术创新",
"leader_id": 3,
"parent_id": null
}
name 为必填,description、leader_id、parent_id 可选。名称不能重复。leader_id 指定部门负责人(Employee ID),null 或不传表示暂无负责人。parent_id 指定父部门,null 或不传表示顶级部门。manager 只能在自己管辖范围内的父部门下创建子部门。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 名称已存在 | 400 | department name already exists |
| 父部门不存在 | 400 | parent department not found |
| 无权限 | 403 | forbidden: cannot create department outside your department scope |
查询部门列表(分页+搜索+排序)
GET /depts(需认证)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按 name 模糊搜索 |
| sort_by | string | id | 排序字段:id, name |
| sort_desc | bool | false | 是否降序(传 "true") |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
查询部门树
GET /depts/tree(需认证)
返回部门层级树结构,一次性加载全部部门。
响应示例:
{
"code": 200,
"data": [
{
"id": 1,
"name": "研发部",
"description": "负责产品研发和技术创新",
"parent_id": null,
"children": [
{
"id": 4,
"name": "前端组",
"description": "前端开发",
"parent_id": 1,
"children": []
}
]
}
]
}
查询单个部门
GET /depts/:id(需认证,manager 仅可查看管辖范围内的部门)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 无权限 | 403 | forbidden: cannot access department outside your scope |
| 不存在 | 404 | department not found |
更新部门
PUT /depts/:id(需认证,admin/manager 且为部门负责人,manager 仅可更新管辖范围内的部门)
请求体(只传需要更新的字段):
{
"name": "技术部",
"description": "技术中心",
"leader_id": 5,
"parent_id": 2
}
更新 parent_id 时会校验:不能设自己为父、不能产生循环引用、父部门必须存在。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 请求体无效 | 400 | invalid request body |
| 名称已存在 | 400 | department name already exists |
| 循环引用 | 400 | cannot set parent: circular reference |
| 无权限 | 403 | forbidden: cannot update department outside your department scope |
| 父部门不存在 | 404 | department not found |
| 部门不存在 | 404 | department not found |
删除部门
DELETE /depts/:id(需认证,admin)
有子部门或有员工的部门不能删除,需先迁移子部门和员工。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 有子部门 | 400 | department has child departments |
| 有员工 | 400 | department has employees |
| 部门不存在 | 404 | department not found |
员工
创建员工
POST /employees(需认证,admin/manager 且为部门负责人,manager 仅可在管辖范围内创建员工)
请求体:
{
"name": "张伟",
"email": "zhangwei@pms.com",
"phone": "13800000001",
"id_number": "110101199001011234",
"dept_id": 1,
"user_id": 2,
"status": "active"
}
name、email、dept_id 为必填。phone、id_number、user_id、status 可选。status 默认 "active"。user_id 关联登录账号,一个 User 最多关联一个 Employee。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 邮箱已存在 | 400 | email already exists |
| user_id 已关联其他员工 | 400 | employee already linked to a user |
| 部门不存在 | 400 | department not found |
| 无权限 | 403 | forbidden: cannot create employee outside your department scope |
查询员工列表(分页+搜索+排序)
GET /employees(需认证,admin 查全部,manager 查本部门+子部门,staff 返回 403)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按 name/email/phone/id_number 模糊搜索 |
| sort_by | string | id | 排序字段:id, name, email, phone, status, created_at |
| sort_desc | bool | false | 是否降序(传 "true") |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
响应:
{
"code": 200,
"data": {
"data": [
{
"id": 1,
"name": "张伟",
"email": "zhangwei@pms.com",
"phone": "13800000001",
"id_number": "110101199001011234",
"dept_id": 1,
"department": { "id": 1, "name": "研发部" },
"status": "active",
"created_at": "2026-04-28T12:00:00Z",
"updated_at": "2026-04-28T12:00:00Z",
"created_by": "admin",
"updated_by": "admin"
}
],
"total": 50,
"page": 1,
"page_size": 10,
"total_pages": 5
}
}
列表结果通过 Redis 缓存,过期时间 10 分钟。创建/更新/删除员工后自动清除缓存。
获取当前用户员工档案
GET /employees/me(需认证,所有角色)
根据 JWT 中的 user_id 查询关联的 Employee。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 未认证 | 401 | unauthorized |
| 未关联员工档案 | 404 | employee profile not found |
查询单个员工
GET /employees/:id(需认证,admin 全量,manager 仅本部门+子部门,staff 仅本部门)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 无权限 | 403 | forbidden: cannot access employee outside your department scope |
| 不存在 | 404 | employee not found |
更新员工
PUT /employees/:id(需认证,admin/manager 且为部门负责人,manager 仅可更新管辖范围内的员工,不能将员工移出管辖范围)
请求体(只传需要更新的字段):
{
"name": "张伟2",
"phone": "13900000001"
}
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 请求体无效 | 400 | invalid request body |
| 邮箱已存在 | 400 | email already exists |
| 部门不存在 | 400 | department not found |
| 无权限 | 403 | forbidden: cannot update employee outside your department scope |
删除员工
DELETE /employees/:id(需认证,仅 admin)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 不存在 | 404 | employee not found |
薪资结构
创建薪资结构
POST /salaries/structures(需 admin 角色)
请求体:
{
"employee_id": 1,
"base_salary": 10000,
"position_allowance": 2000,
"performance_factor": 1.0
}
employee_id、base_salary、position_allowance 为必填。performance_factor 可选,默认 1.0。每个员工只能有一个薪资结构。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 员工不存在 | 400 | employee not found |
| 薪资结构已存在 | 400 | salary structure already exists for this employee |
查询薪资结构列表(分页+搜索+排序)
GET /salaries/structures(需 admin 角色)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按员工姓名/邮箱模糊搜索 |
| sort_by | string | id | 排序字段:id, base_salary, position_allowance, performance_factor, created_at |
| sort_desc | bool | false | 是否降序(传 "true") |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
查询员工的薪资结构
GET /salaries/structures/employees/:emp_id(需 admin 角色)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效员工 ID | 400 | invalid employee id |
| 不存在 | 404 | salary structure not found |
更新薪资结构
PUT /salaries/structures/:id(需 admin 角色)
请求体(只传需要更新的字段):
{
"base_salary": 12000,
"performance_factor": 1.2
}
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 请求体无效 | 400 | invalid request body |
| 薪资结构不存在 | 404 | salary structure not found |
删除薪资结构
DELETE /salaries/structures/:id(需 admin 角色)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 薪资结构不存在 | 404 | salary structure not found |
薪资记录
生成月度薪资记录
POST /salaries/records(需 admin 角色)
请求体:
{
"employee_id": 1,
"year": 2026,
"month": 4,
"performance_factor": 1.2
}
employee_id、year、month 为必填。performance_factor 可选,传 0 或不传则使用薪资结构中的系数。
计算公式:实际薪资 = (基本工资 + 岗位津贴) × 绩效系数
生成时自动关联薪资结构(structure_id),状态为 draft。
同一员工同一年月不能重复生成。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 年月无效 | 400 | invalid year or month |
| 员工不存在 | 400 | employee not found |
| 薪资结构不存在 | 400 | salary structure not found for this employee |
| 记录已存在 | 400 | salary record already exists for this employee and month |
批量生成月度薪资记录
POST /salaries/records/batch(需 admin 角色)
请求体:
{
"year": 2026,
"month": 4,
"employee_ids": [1, 2, 3],
"dept_id": null
}
year、month 为必填。employee_ids 和 dept_id 可选,优先级:
- employee_ids 不为空:为指定员工生成
- dept_id 不为空:为该部门下有薪资结构的在职员工生成
- 都不传:为所有有薪资结构的在职员工生成
已存在的记录自动跳过,不报错。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 年月无效 | 400 | invalid year or month |
| 部门不存在 | 400 | department not found |
修改薪资记录
PUT /salaries/records/:id(需 admin 角色)
仅 draft 或 rejected 状态可修改。
请求体(只传需要更新的字段):
{
"performance_factor": 1.5
}
修改 performance_factor 时自动重算 actual_salary。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 非草稿/驳回状态 | 400 | salary record is not in draft/rejected status |
| 不存在 | 404 | salary record not found |
提交审核
PUT /salaries/records/:id/submit(需 admin 角色)
draft 或 rejected → pending。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 非草稿/驳回状态 | 400 | salary record is not in draft/rejected status |
| 不存在 | 404 | salary record not found |
审核通过
PUT /salaries/records/:id/approve(需 admin 角色)
pending → approved。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 非待审核状态 | 400 | salary record is not in pending status |
| 不存在 | 404 | salary record not found |
审核驳回
PUT /salaries/records/:id/reject(需 admin 角色)
pending → rejected。驳回后可修改绩效系数再重新提交。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 非待审核状态 | 400 | salary record is not in pending status |
| 不存在 | 404 | salary record not found |
确认发放
PUT /salaries/records/:id/pay(需 admin 角色)
approved → paid。发放后不可修改。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 非已审核状态 | 400 | salary record is not in approved status |
| 不存在 | 404 | salary record not found |
查询薪资记录列表(分页+筛选+排序)
GET /salaries/records(需 admin 角色)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 ID 筛选 |
| year | int | - | 按年份筛选 |
| month | int | - | 按月份筛选 |
| status | string | - | 按状态筛选:draft, pending, approved, rejected, paid |
| sort_by | string | id | 排序字段:id, year, month, actual_salary, status, created_at |
| sort_desc | bool | false | 是否降序(传 "true") |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
查询单条薪资记录
GET /salaries/records/:id(需 admin 角色)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 不存在 | 404 | salary record not found |
考勤管理
上班打卡
POST /attendance/clock-in(需认证,staff 仅能为自己打卡)
请求体:
{
"employee_id": 1
}
employee_id 为必填。同一天同一员工不能重复打卡。staff 角色只能传自己的 employee_id(从 JWT 的 employee_id 字段获取),否则返回 403。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 已打卡 | 400 | already clocked in today |
| 员工不存在 | 400 | employee not found |
| staff 替别人打卡 | 403 | can only clock in for yourself |
| staff 未关联员工档案 | 403 | no employee profile linked |
下班打卡
PUT /attendance/clock-out(需认证,staff 仅能为自己打卡)
请求体:
{
"employee_id": 1
}
必须先上班打卡才能下班打卡。staff 角色只能传自己的 employee_id,否则返回 403。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 未上班打卡 | 400 | not clocked in today |
| 已下班打卡 | 400 | already clocked out today |
| staff 替别人打卡 | 403 | can only clock out for yourself |
| staff 未关联员工档案 | 403 | no employee profile linked |
查询打卡记录列表
GET /attendance/records(需认证,staff 仅能查看自己记录)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 ID 筛选 |
| start_date | string | - | 起始日期 YYYY-MM-DD |
| end_date | string | - | 结束日期 YYYY-MM-DD |
| keyword | string | - | 按员工姓名/邮箱搜索 |
| sort_by | string | date | 排序字段:id, date, created_at |
| sort_desc | bool | false | 是否降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
查询单条打卡记录
GET /attendance/records/:id(需认证)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 不存在 | 404 | attendance record not found |
删除打卡记录
DELETE /attendance/records/:id(需 admin 角色)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 不存在 | 404 | attendance record not found |
批量删除打卡记录
DELETE /attendance/records/batch(需 admin 角色)
请求体:
{
"ids": [1, 2, 3]
}
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
生成考勤月度统计
POST /attendance/summaries/generate(需 admin/manager 角色,manager 仅可生成管辖范围内员工的汇总)
请求体:
{
"employee_id": 1,
"year": 2026,
"month": 4
}
根据员工工作时间(WorkStartTime/WorkEndTime,默认 09:00/18:00)判定迟到/早退/缺勤。重复生成会覆盖旧数据。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 年月无效 | 400 | invalid year or month |
| 员工不存在 | 400 | employee not found |
| 无权限 | 403 | forbidden: cannot generate summary for employee outside your department scope |
查询考勤统计列表
GET /attendance/summaries(需认证,staff 仅能查看自己统计)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 ID 筛选 |
| year | int | - | 按年份筛选 |
| month | int | - | 按月份筛选 |
| sort_by | string | id | 排序字段:id, year, month, created_at |
| sort_desc | bool | false | 是否降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
请假管理
提交请假申请
POST /leaves(需认证,staff 仅能为本人申请,manager 仅能为管辖范围内员工申请)
请求体:
{
"employee_id": 1,
"type": "annual",
"start_date": "2026-04-28",
"end_date": "2026-04-29",
"reason": "年假"
}
employee_id、type、start_date、end_date 为必填。type 取值:annual(年假)/sick(病假)/personal(事假)。staff 角色只能传自己的 employee_id,否则返回 403。manager 只能为管辖范围内的员工创建请假。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
| 无效请假类型 | 400 | invalid leave type, must be annual/sick/personal |
| 日期无效 | 400 | end date must be >= start date |
| 员工不存在 | 400 | employee not found |
| staff 替别人请假 | 403 | can only create leave for yourself |
| staff 未关联员工档案 | 403 | no employee profile linked |
| 无权限 | 403 | forbidden: cannot create leave for employee outside your department scope |
查询请假列表
GET /leaves(需认证,staff 仅能查看自己请假)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 ID 筛选 |
| status | string | - | 按状态筛选:pending/approved/rejected |
| keyword | string | - | 按员工姓名/邮箱搜索 |
| sort_by | string | id | 排序字段:id, type, status, start_date, created_at |
| sort_desc | bool | false | 是否降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
查询单条请假申请
GET /leaves/:id(需认证)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 不存在 | 404 | leave request not found |
审批通过
PUT /leaves/:id/approve(需 admin/manager 且为部门负责人)
pending → approved。manager 仅能审批本部门及子部门的请假。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 非待审批状态 | 400 | leave request is not in pending status |
| 不存在 | 404 | leave request not found |
| manager 审批非本部门 | 403 | forbidden: cannot approve leave outside your department |
审批驳回
PUT /leaves/:id/reject(需 admin/manager 且为部门负责人)
pending → rejected。manager 仅能驳回本部门及子部门的请假。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 非待审批状态 | 400 | leave request is not in pending status |
| 不存在 | 404 | leave request not found |
| manager 驳回非本部门 | 403 | forbidden: cannot reject leave outside your department |
获取角色列表
GET /roles(需认证,所有角色可查看)
用户管理
创建用户
POST /users(需 admin 角色)
请求体:
{
"username": "newuser",
"password": "123456",
"role_ids": [2]
}
password最少 6 位role_ids可选,不传则默认分配 staff 角色
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 用户名已存在 | 400 | username already exists |
| 请求体无效 | 400 | invalid request body |
获取用户列表(分页+搜索+排序)
GET /users(需 admin 角色)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按 username 模糊搜索 |
| sort_by | string | id | 排序字段:id, username, status, created_at |
| sort_desc | bool | false | 是否降序(传 "true") |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
响应:
{
"code": 200,
"data": {
"data": [
{
"id": 1,
"username": "admin",
"status": "active",
"roles": [{"id": 1, "name": "admin"}],
"created_at": "2026-04-29T12:00:00Z",
"updated_at": "2026-04-29T12:00:00Z"
}
],
"total": 10,
"page": 1,
"page_size": 10,
"total_pages": 1
}
}
获取单个用户
GET /users/:id(需 admin 角色)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 不存在 | 404 | user not found |
修改用户状态
PUT /users/:id/status(需 admin 角色)
请求体:
{
"status": "disabled"
}
status 取值:active / disabled
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 请求体无效 | 400 | invalid request body |
| 无效状态 | 400 | invalid status |
分配角色
PUT /users/:id/roles(需 admin 角色)
请求体:
{
"role_ids": [1, 2]
}
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 请求体无效 | 400 | invalid request body |
修改密码
PUT /users/:id/password(需 admin 角色)
请求体:
{
"new_password": "654321"
}
admin 可重置任意用户密码,仅需提供 new_password。new_password 最少 6 位。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 请求体无效 | 400 | invalid request body |
删除用户
DELETE /users/:id(需 admin 角色)
不能删除自己。
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 删除自己 | 400 | cannot delete yourself |
权限矩阵
| 操作 | admin | manager(部门负责人) | staff |
|---|---|---|---|
| 查看部门/部门树 | Y | Y | Y |
| 创建/编辑部门 | Y | Y(本部门范围) | - |
| 删除部门 | Y | - | - |
| 查看员工列表 | Y(全部) | Y(本部门+子部门) | - |
| 查看 /employees/me | Y | Y | Y |
| 创建/编辑员工 | Y | Y(本部门范围) | - |
| 删除员工 | Y | - | - |
| 管理用户(含删除) | Y | - | - |
| 查看角色列表 | Y | Y | Y |
| 薪资管理(全部) | Y | - | - |
| 打卡 | Y | Y | 仅自己 |
| 删除考勤记录 | Y | - | - |
| 查看考勤记录/统计 | Y | Y | 仅自己 |
| 生成考勤统计 | Y | Y | - |
| 提交请假 | Y | Y | 仅自己 |
| 查看请假列表 | Y | Y | 仅自己 |
| 审批请假 | Y | Y(本部门范围) | - |
| 查看审计日志 | Y | - | - |
| 删除审计日志 | Y | - | - |
"本部门范围" 指该 manager 作为 leader_id 所属部门及其子部门。manager 如果不是任何部门的负责人,访问需要部门权限的接口会返回 403。
"仅自己" 指后端强制使用 JWT 中的 employee_id,忽略请求中的 employee_id 参数或列表筛选。
操作日志
所有写操作(创建、更新、删除、审批等)自动记录审计日志,包含操作人、操作类型、实体类型、实体 ID、变更内容、IP 地址和时间。
查询审计日志
GET /audit-logs(需 admin 角色)
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| operator | string | - | 按操作人筛选 |
| entity_type | string | - | 按实体类型筛选:user, department, employee, salary_structure, salary_record, attendance_record, attendance_summary, leave_request |
| start_time | string | - | 起始时间 RFC3339 格式 |
| end_time | string | - | 结束时间 RFC3339 格式 |
| sort_by | string | id | 排序字段:id, created_at, action, entity_type |
| sort_desc | bool | false | 是否降序(传 "true") |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量,最大 100 |
响应示例:
{
"code": 200,
"data": {
"data": [
{
"id": 1,
"operator": "admin",
"action": "create",
"entity_type": "employee",
"entity_id": 1,
"changes": "{\"name\":\"Alice\",\"email\":\"alice@test.com\"}",
"ip_address": "192.168.1.1",
"created_at": "2026-04-28T12:00:00Z"
}
],
"total": 50,
"page": 1,
"page_size": 10,
"total_pages": 5
}
}
action 取值:create, update, delete, update_status, assign_roles, reset_password, update_password, clock_in, clock_out, generate_summary, approve, reject, submit, pay, batch_create
删除审计日志
DELETE /audit-logs/:id(需 admin 角色)
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 无效 ID | 400 | invalid id |
| 不存在 | 404 | audit log not found |
批量删除审计日志
DELETE /audit-logs/batch(需 admin 角色)
请求体:
{
"ids": [1, 2, 3]
}
| 响应 | code | message |
|---|---|---|
| 成功 | 200 | - |
| 缺少必填字段 | 400 | invalid request body |
健康检查
GET /ping
{ "message": "pong" }
前端对接参考
通用规范
请求头
所有需认证的接口:
Authorization: Bearer <token>
Content-Type: application/json
响应格式
{
"code": 200,
"message": "错误信息(仅失败时存在)",
"data": {}
}
code与 HTTP 状态码一致:200/400/401/403/404/500- 成功时
data为业务数据,写操作成功时data为 null - 失败时
message为可展示的错误描述
分页响应格式
所有列表接口统一返回:
{
"code": 200,
"data": {
"data": [],
"total": 100,
"page": 1,
"page_size": 10,
"total_pages": 10
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| data | array | 当前页数据 |
| total | int64 | 总记录数 |
| page | int | 当前页码(从 1 开始) |
| page_size | int | 每页数量 |
| total_pages | int | 总页数 |
分页请求参数
所有列表接口通用:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| page | int | 1 | 页码,最小 1 |
| page_size | int | 10 | 每页数量,范围 1-100 |
| keyword | string | - | 模糊搜索关键词 |
| sort_by | string | id | 排序字段(各接口不同,见下方) |
| sort_desc | string | false | 是否降序,传 "true" |
认证
注册
POST /auth/register
{
"username": "admin",
"password": "123456",
"role_ids": [1]
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | Y | 用户名,唯一 |
| password | string | Y | 密码,最少 6 位 |
| role_ids | uint[] | N | 角色 ID 列表,不传默认 staff;第一个用户自动 admin |
成功响应 data 为 User 对象。
登录
POST /auth/login
{
"username": "admin",
"password": "123456"
}
响应:
{
"code": 200,
"data": { "token": "eyJhbGciOi..." }
}
| HTTP | code | message | 场景 |
|---|---|---|---|
| 200 | 200 | - | 成功 |
| 401 | 401 | invalid username or password | 用户名或密码错误 |
| 403 | 403 | user is disabled | 账号被禁用 |
Token 过期时间 24 小时。前端应在 401 时跳转登录页。
获取当前用户
GET /auth/me
响应:
{
"code": 200,
"data": { "id": 1, "username": "admin" }
}
注销
POST /auth/logout
用户管理
所有用户接口需 admin 角色。
用户列表
GET /users
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按 username 模糊搜索 |
| sort_by | string | id | 可选:id, username, status, created_at |
| sort_desc | string | false | 传 "true" 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
响应示例:
{
"code": 200,
"data": {
"data": [
{
"id": 1,
"username": "admin",
"status": "active",
"roles": [{"id": 1, "name": "admin", "description": "系统管理员"}],
"created_at": "2026-04-29T12:00:00Z",
"updated_at": "2026-04-29T12:00:00Z"
}
],
"total": 10,
"page": 1,
"page_size": 10,
"total_pages": 1
}
}
User 对象字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | uint | 用户 ID |
| username | string | 用户名 |
| status | string | 状态:active / disabled |
| roles | Role[] | 角色列表 |
| created_at | string | 创建时间 RFC3339 |
| updated_at | string | 更新时间 RFC3339 |
注意:password 字段不会在 JSON 中返回。
获取单个用户
GET /users/:id
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 404 | 404 | user not found |
修改用户状态
PUT /users/:id/status
{ "status": "disabled" }
status 取值:active / disabled
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 400 | 400 | invalid request body |
| 400 | 400 | invalid status |
分配角色
PUT /users/:id/roles
{ "role_ids": [1, 2] }
role_ids 为完整的角色 ID 列表,会替换现有角色(不是增量追加)。
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 400 | 400 | invalid request body |
角色 ID 参考:
| ID | name | 说明 |
|---|---|---|
| 1 | admin | 系统管理员 |
| 2 | manager | 部门经理 |
| 3 | staff | 普通员工 |
可通过 GET /roles 获取完整角色列表。
修改/重置密码
PUT /users/:id/password
admin 操作(重置他人密码):
{ "new_password": "654321" }
仅需 new_password,最少 6 位。
非 admin 操作(修改自己密码):
{
"old_password": "123456",
"new_password": "654321"
}
需同时提供 old_password 和 new_password。
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 400 | 400 | invalid request body |
| 400 | 400 | wrong old password |
| 403 | 403 | forbidden |
删除用户
DELETE /users/:id
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 400 | 400 | cannot delete yourself |
前端应在删除前判断是否为当前登录用户,给出禁用/提示。
部门管理
部门列表
GET /depts
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按 name 模糊搜索 |
| sort_by | string | id | 可选:id, name |
| sort_desc | string | false | 传 "true" 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
部门树
GET /depts/tree
返回完整树结构,一次性加载。适合侧边栏/下拉框等场景。
{
"code": 200,
"data": [
{
"id": 1,
"name": "研发部",
"description": "...",
"parent_id": null,
"children": [...]
}
]
}
创建部门
POST /depts
需要 admin 或 manager(部门负责人)角色。
{
"name": "研发部",
"description": "负责产品研发",
"leader_id": 3,
"parent_id": null
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | Y | 名称,唯一 |
| description | string | N | 描述 |
| leader_id | uint | N | 负责人 Employee ID |
| parent_id | uint | N | 父部门 ID,null 为顶级 |
更新部门
PUT /depts/:id
只传需要更新的字段。更新 parent_id 时校验循环引用。
删除部门
DELETE /depts/:id
仅 admin。有子部门或有员工时不可删除(code 400)。
员工管理
员工列表
GET /employees
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按 name/email/phone/id_number 模糊搜索 |
| sort_by | string | id | 可选:id, name, email, phone, status, created_at |
| sort_desc | string | false | 传 "true" 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
admin 可看全部,manager 看本部门+子部门,staff 无权限(403)。
获取自己的员工档案
GET /employees/me
所有角色可访问。未关联员工档案时返回 404。
创建员工
POST /employees
需要 admin 或 manager(部门负责人)角色。
{
"name": "张伟",
"email": "zhangwei@pms.com",
"phone": "13800000001",
"id_number": "110101199001011234",
"dept_id": 1,
"user_id": 2,
"status": "active"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | Y | 姓名 |
| string | Y | 邮箱,唯一 | |
| phone | string | N | 手机号 |
| id_number | string | N | 身份证号 |
| dept_id | uint | Y | 部门 ID |
| user_id | uint | N | 关联登录账号 ID,一个 User 最多关联一个 Employee |
| status | string | N | 默认 active |
Employee 对象包含 department 关联字段(Preload),可直接取部门名称。
更新员工
PUT /employees/:id
只传需要更新的字段。更新 dept_id 和 email 时有存在性校验。
删除员工
DELETE /employees/:id
仅 admin。
薪资管理
所有薪资接口需 admin 角色。
薪资结构列表
GET /salaries/structures
查询参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| keyword | string | - | 按员工姓名/邮箱搜索 |
| sort_by | string | id | 可选:id, base_salary, position_allowance, performance_factor, created_at |
| sort_desc | string | false | 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
创建薪资结构
POST /salaries/structures
{
"employee_id": 1,
"base_salary": 10000,
"position_allowance": 2000,
"performance_factor": 1.0
}
每个员工只能有一个薪资结构。
查询员工薪资结构
GET /salaries/structures/employees/:emp_id
更新/删除薪资结构
PUT /salaries/structures/:id
DELETE /salaries/structures/:id
薪资记录列表
GET /salaries/records
新增筛选参数(在通用分页基础上):
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 ID |
| year | int | - | 按年份 |
| month | int | - | 按月份 |
| status | string | - | draft/pending/approved/rejected/paid |
| sort_by | string | id | 可选:id, year, month, actual_salary, status, created_at |
生成薪资记录
POST /salaries/records
{
"employee_id": 1,
"year": 2026,
"month": 4,
"performance_factor": 1.0
}
批量生成
POST /salaries/records/batch
{
"year": 2026,
"month": 4,
"employee_ids": [1, 2, 3]
}
employee_ids 和 dept_id 可选,优先级:employee_ids > dept_id > 全部。
修改薪资记录
PUT /salaries/records/:id
仅 draft 或 rejected 状态可修改。修改 performance_factor 时自动重算 actual_salary。
{ "performance_factor": 1.3 }
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 400 | 400 | salary record is not in draft/rejected status |
| 404 | 404 | salary record not found |
查询单条薪资记录
GET /salaries/records/:id
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 400 | 400 | invalid id |
| 404 | 404 | salary record not found |
薪资记录状态流转
PUT /salaries/records/:id/submit draft/rejected → pending
PUT /salaries/records/:id/approve pending → approved
PUT /salaries/records/:id/reject pending → rejected
PUT /salaries/records/:id/pay approved → paid
状态流转图:
draft → pending → approved → paid
↑ ↓
└── rejected
考勤管理
打卡
POST /attendance/clock-in 上班打卡
PUT /attendance/clock-out 下班打卡
{ "employee_id": 1 }
staff 只能为自己打卡(后端校验 JWT employee_id),admin/manager 可为任意员工打卡。
打卡记录列表
GET /attendance/records
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 |
| start_date | string | - | 起始日期 YYYY-MM-DD |
| end_date | string | - | 结束日期 YYYY-MM-DD |
| keyword | string | - | 按员工姓名/邮箱搜索 |
| sort_by | string | date | 可选:id, date, created_at |
| sort_desc | string | false | 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
查询单条打卡记录
GET /attendance/records/:id
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 404 | 404 | attendance record not found |
生成考勤统计
POST /attendance/summaries/generate
{ "employee_id": 1, "year": 2026, "month": 4 }
需要 admin/manager 角色。重复生成会覆盖。
考勤统计列表
GET /attendance/summaries
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 |
| year | int | - | 按年份 |
| month | int | - | 按月份 |
| sort_by | string | id | 可选:id, year, month, created_at |
| sort_desc | string | false | 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
请假管理
提交请假
POST /leaves
{
"employee_id": 1,
"type": "annual",
"start_date": "2026-05-01",
"end_date": "2026-05-03",
"reason": "年假"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| employee_id | uint | Y | 员工 ID |
| type | string | Y | annual/sick/personal |
| start_date | string | Y | 开始日期 YYYY-MM-DD |
| end_date | string | Y | 结束日期 YYYY-MM-DD,需 >= start_date |
| reason | string | N | 请假原因 |
staff 只能为本人申请。
请假列表
GET /leaves
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| employee_id | int | - | 按员工 |
| status | string | - | pending/approved/rejected |
| keyword | string | - | 按员工姓名/邮箱搜索 |
| sort_by | string | id | 可选:id, type, status, start_date, created_at |
| sort_desc | string | false | 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
查询单条请假申请
GET /leaves/:id
| HTTP | code | message |
|---|---|---|
| 200 | 200 | - |
| 404 | 404 | leave request not found |
审批
PUT /leaves/:id/approve
PUT /leaves/:id/reject
需 admin/manager(部门负责人)角色。manager 只能审批本部门范围。
角色列表
GET /roles
所有认证用户可访问。返回固定三种角色:
{
"code": 200,
"data": [
{"id": 1, "name": "admin", "description": "系统管理员"},
{"id": 2, "name": "manager", "description": "部门经理"},
{"id": 3, "name": "staff", "description": "普通员工"}
]
}
审计日志
需 admin 角色。
GET /audit-logs
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| operator | string | - | 按操作人 |
| entity_type | string | - | 按实体类型 |
| start_time | string | - | 起始时间 RFC3339 |
| end_time | string | - | 结束时间 RFC3339 |
| sort_by | string | id | 可选:id, created_at, action, entity_type |
| sort_desc | string | false | 降序 |
| page | int | 1 | 页码 |
| page_size | int | 10 | 每页数量 |
entity_type 取值:user, department, employee, salary_structure, salary_record, attendance_record, attendance_summary, leave_request
权限概览
| 操作 | admin | manager(部门负责人) | staff |
|---|---|---|---|
| 查看部门/部门树 | Y | Y | Y |
| 创建/编辑部门 | Y | Y(本部门范围) | - |
| 删除部门 | Y | - | - |
| 查看员工列表 | Y(全部) | Y(本部门+子部门) | - |
| /employees/me | Y | Y | Y |
| 创建/编辑员工 | Y | Y(本部门范围) | - |
| 删除员工 | Y | - | - |
| 用户管理(含删除) | Y | - | - |
| 查看角色列表 | Y | Y | Y |
| 薪资管理(全部) | Y | - | - |
| 打卡 | Y | Y | 仅自己 |
| 查看考勤记录/统计 | Y | Y | 仅自己 |
| 生成考勤统计 | Y | Y | - |
| 提交请假 | Y | Y | 仅自己 |
| 查看请假列表 | Y | Y | 仅自己 |
| 审批请假 | Y | Y(本部门范围) | - |
| 查看审计日志 | Y | - | - |
- "本部门范围" 指 manager 作为 leader_id 所属部门及其子部门
- "仅自己" 指后端强制使用 JWT 中的 employee_id,忽略请求中的 employee_id
前端权限控制建议
- 登录后从 JWT 解析
role,存储到全局状态 - 根据角色控制菜单/按钮显隐
- admin:全部功能可见
- manager:部门/员工管理(本部门范围)、考勤统计、请假审批可见;用户管理、薪资管理、审计日志不可见
- staff:仅 /employees/me、打卡(自己)、请假(自己)可见
- 所有角色:部门/部门树、角色列表只读可见
JWT Token Payload
{
"user_id": 1,
"username": "admin",
"role": "admin",
"employee_id": 3,
"jti": "xxx",
"exp": 1746000000
}
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | uint | 用户 ID |
| username | string | 用户名 |
| role | string | 角色名:admin / manager / staff |
| employee_id | *uint | 关联的员工 ID,未关联时不出现 |
| exp | int64 | 过期时间戳 |
前端可用 jwt-decode 库解析 token 获取 role 和 employee_id。
配置说明
环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
| DB_HOST | localhost | PostgreSQL 主机 |
| DB_PORT | 5432 | PostgreSQL 端口 |
| DB_USER | postgres | 数据库用户名 |
| DB_PASSWORD | postgres | 数据库密码 |
| DB_NAME | hr | 数据库名 |
| DB_SSLMODE | disable | SSL 模式 |
| SERVER_PORT | 8080 | 服务监听端口 |
| REDIS_HOST | localhost | Redis 主机 |
| REDIS_PORT | 6379 | Redis 端口 |
| REDIS_PASSWORD | Redis 密码 | |
| JWT_SECRET | change-me-in-production | JWT 签名密钥 |
Config 结构体
定义在 config/config.go:
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
DBSSLMode string
ServerPort string
RedisHost string
RedisPort string
RedisPassword string
JWTSecret string
}
通过 config.Load() 读取,优先使用环境变量,未设置则用默认值。
cfg.DSN() 方法生成 PostgreSQL 连接字符串。
cfg.RedisAddr() 方法生成 Redis 地址(host:port)。
数据库初始化
config.NewDB(cfg) 会:
- 用 DSN 连接 PostgreSQL(重试 30 次,2 秒间隔)
- 执行 AutoMigrate(自动建表/加列)
Redis 初始化
config.InitRedis(cfg) 会:
- 用 RedisAddr 连接 Redis(重试 30 次,2 秒间隔)
- 用于员工列表缓存和 JWT Token 存储
数据模型
User
文件:models/user.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| Username | string | 用户名,非空,唯一索引 |
| Password | string | 密码(bcrypt 哈希),JSON 不输出 |
| Status | string | 状态,非空,默认 "active",最长 20:active/disabled |
| Roles | []Role | 角色,多对多 user_roles |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 更新时间 |
Role
文件:models/role.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| Name | string | 角色名,唯一索引 |
| Description | string | 角色描述 |
Department
文件:models/department.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| Name | string | 部门名称,非空,唯一 |
| Description | string | 部门描述,最长 500 |
| LeaderID | *uint | 部门负责人 Employee ID,索引,null 表示暂无负责人 |
| Leader | *Employee | 关联的负责人员工对象(外键 LeaderID) |
| ParentID | *uint | 父部门 ID,索引,null 表示顶级部门 |
| Parent | *Department | 关联的父部门对象(外键 ParentID) |
| Children | []Department | 关联的子部门列表(外键 ParentID) |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 更新时间 |
| CreatedBy | string | 创建人,最长 100 |
| UpdatedBy | string | 更新人,最长 100 |
DeptTreeNode(树形响应结构):
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 部门 ID |
| Name | string | 部门名称 |
| Description | string | 部门描述 |
| LeaderID | *uint | 部门负责人 Employee ID |
| LeaderName | string | 负责人姓名(仅树形响应) |
| ParentID | *uint | 父部门 ID |
| Children | []DeptTreeNode | 子部门列表 |
Employee
文件:models/employee.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| UserID | *uint | 关联用户 ID,唯一索引,null 表示未关联登录账号 |
| User | *User | 关联的用户对象(外键 UserID) |
| Name | string | 姓名,非空 |
| string | 邮箱,非空,唯一 | |
| Phone | string | 手机号,最长 20 |
| IDNumber | string | 身份证号,最长 18,数据库列名 id_number |
| DeptID | uint | 所属部门 ID,外键 |
| Department | Department | 关联的部门对象 |
| Status | string | 状态,默认 "active" |
| WorkStartTime | string | 上班时间,默认 "09:00",最长 5 |
| WorkEndTime | string | 下班时间,默认 "18:00",最长 5 |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 更新时间 |
| CreatedBy | string | 创建人,最长 100 |
| UpdatedBy | string | 更新人,最长 100 |
表关系
Department 1 ←→ N Department(自引用,parent_id 外键)
Department 1 ←→ N Employee
User 1 ←→ 0..1 Employee(user_id 外键)
Employee 1 ←→ 1 SalaryStructure
Employee 1 ←→ N SalaryRecord
Employee 1 ←→ N AttendanceRecord
Employee 1 ←→ N AttendanceSummary
Employee 1 ←→ N LeaveRequest
Department 通过 ParentID 自引用建立层级树。Employee 通过 DeptID 外键关联 Department,GORM 查询时用 Preload 预加载。
SalaryStructure
文件:models/salary.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| EmployeeID | uint | 员工 ID,非空,唯一索引 |
| BaseSalary | float64 | 基本工资,非空,默认 0 |
| PositionAllowance | float64 | 岗位津贴,非空,默认 0 |
| PerformanceFactor | float64 | 绩效系数,非空,默认 1 |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 更新时间 |
| CreatedBy | string | 创建人,最长 100 |
| UpdatedBy | string | 更新人,最长 100 |
SalaryRecord
文件:models/salary.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| EmployeeID | uint | 员工 ID,非空,索引 |
| StructureID | uint | 薪资结构 ID(快照),非空 |
| Year | int | 年份,非空 |
| Month | int | 月份,非空 |
| BaseSalary | float64 | 生成时的基本工资(快照) |
| PositionAllowance | float64 | 生成时的岗位津贴(快照) |
| PerformanceFactor | float64 | 本月绩效系数 |
| ActualSalary | float64 | 实发薪资 = (基本工资 + 岗位津贴) × 绩效系数 |
| Status | string | 状态,非空,默认 draft:draft/pending/approved/rejected/paid |
| ReviewedBy | string | 审核人,最长 100 |
| ReviewedAt | *time.Time | 审核时间 |
| PaidBy | string | 发放人,最长 100 |
| PaidAt | *time.Time | 发放时间 |
| CreatedAt | time.Time | 创建时间 |
| CreatedBy | string | 创建人,最长 100 |
AttendanceRecord
文件:models/attendance.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| EmployeeID | uint | 员工 ID,非空,索引 |
| Date | time.Time | 日期(date 类型) |
| ClockIn | *time.Time | 上班打卡时间 |
| ClockOut | *time.Time | 下班打卡时间 |
| CreatedBy | string | 创建人,最长 100 |
| CreatedAt | time.Time | 创建时间 |
AttendanceSummary
文件:models/attendance.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| EmployeeID | uint | 员工 ID,非空,索引 |
| Year | int | 年份,非空 |
| Month | int | 月份,非空 |
| NormalDays | int | 正常天数,默认 0 |
| LateDays | int | 迟到天数,默认 0 |
| EarlyDays | int | 早退天数,默认 0 |
| AbsentDays | int | 缺勤天数,默认 0 |
| CreatedAt | time.Time | 创建时间 |
LeaveRequest
文件:models/leave.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| EmployeeID | uint | 员工 ID,非空,索引 |
| Type | string | 请假类型,最长 20:annual/sick/personal |
| StartDate | time.Time | 开始日期(date 类型) |
| EndDate | time.Time | 结束日期(date 类型) |
| Reason | string | 请假原因,最长 500 |
| Status | string | 状态,默认 pending:pending/approved/rejected |
| ReviewedBy | string | 审批人,最长 100 |
| ReviewedAt | *time.Time | 审批时间 |
| CreatedBy | string | 创建人,最长 100 |
| CreatedAt | time.Time | 创建时间 |
AuditLog
文件:models/audit_log.go
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键,自增 |
| Operator | string | 操作人,最长 64,索引 |
| Action | string | 操作类型,最长 32,索引 |
| EntityType | string | 实体类型,最长 64,索引 |
| EntityID | uint | 实体 ID,索引 |
| Changes | string | 变更内容,jsonb |
| IPAddress | string | 操作 IP,最长 45 |
| CreatedAt | time.Time | 操作时间,索引 |
薪资管理
概念说明
薪资管理由两部分组成:薪资结构和薪资记录。
薪资结构(SalaryStructure)
薪资结构定义了一个员工的薪资组成,每个员工有且仅有一个薪资结构。
| 字段 | 说明 |
|---|---|
| 基本工资(base_salary) | 员工的固定月薪,与岗位等级挂钩,不随月度表现波动 |
| 岗位津贴(position_allowance) | 因岗位特殊性额外发放的固定补贴,如交通补贴、通讯补贴、住房补贴等。没有则填 0 |
| 绩效系数(performance_factor) | 月度绩效考核的调节系数,默认 1.0,含义见下方详细说明 |
薪资记录(SalaryRecord)
每月为员工生成一条薪资记录,记录当月的薪资明细和实发金额。
计算公式:
实际薪资 = (基本工资 + 岗位津贴) × 绩效系数
薪资记录会快照生成时的薪资结构值(通过 structure_id 关联),后续修改薪资结构不会影响已生成的历史记录。
薪资记录状态
薪资记录有完整的生命周期:
draft(草稿) → pending(待审核) → approved(已审核) → paid(已发放)
↘ rejected(已驳回) ↗
| 状态 | 说明 | 可执行操作 |
|---|---|---|
| draft | 刚生成,可修改绩效系数等 | 修改、提交审核 |
| pending | 已提交审核,不可修改 | 审核通过、审核驳回 |
| approved | 审核通过,等待发放 | 确认发放 |
| rejected | 审核驳回 | 修改后重新提交 |
| paid | 已发放,不可修改 | 无 |
绩效系数详解
绩效系数是一个乘数,作用于"基本工资 + 岗位津贴"的合计,用来反映员工当月的工作表现。
取值含义
| 绩效系数 | 含义 | 说明 |
|---|---|---|
| 1.0 | 正常 | 完成本职工作,无特殊奖惩 |
| > 1.0 | 奖励 | 表现优秀,如加班突出贡献、项目超额完成等 |
| < 1.0 且 > 0 | 扣减 | 表现不达标,如迟到早退、工作质量问题等 |
| 0 | 全扣 | 极端情况,当月薪资为 0 |
实际例子
员工张伟,薪资结构:基本工资 10000 元,岗位津贴 2000 元。
| 月份 | 绩效系数 | 场景 | 计算过程 | 实发薪资 |
|---|---|---|---|---|
| 2026-01 | 1.0 | 正常出勤,完成本职工作 | (10000 + 2000) × 1.0 | 12000 元 |
| 2026-02 | 1.2 | 主导项目提前交付,获评月度优秀 | (10000 + 2000) × 1.2 | 14400 元 |
| 2026-03 | 0.8 | 多次迟到,季度考核不达标 | (10000 + 2000) × 0.8 | 9600 元 |
| 2026-04 | 1.5 | 关键技术突破,特殊贡献奖 | (10000 + 2000) × 1.5 | 18000 元 |
| 2026-05 | 0.5 | 长期请假,部分工作由他人代管 | (10000 + 2000) × 0.5 | 6000 元 |
员工李娜,薪资结构:基本工资 8000 元,岗位津贴 0 元(无津贴)。
| 月份 | 绩效系数 | 场景 | 计算过程 | 实发薪资 |
|---|---|---|---|---|
| 2026-01 | 1.0 | 正常 | (8000 + 0) × 1.0 | 8000 元 |
| 2026-02 | 1.1 | 加班赶工 | (8000 + 0) × 1.1 | 8800 元 |
绩效系数的设置时机
- 薪资结构中设置默认值:创建薪资结构时指定的
performance_factor是默认系数,通常为 1.0 - 生成月度记录时覆盖:生成薪资记录时可以指定当月的绩效系数,不指定则使用薪资结构中的默认值
- 草稿状态修改:薪资记录在 draft 或 rejected 状态时,可以修改绩效系数,修改时自动重算实发金额
这意味着:大多数月份直接用默认系数 1.0 生成记录即可,只在有奖惩的月份手动指定不同系数。
操作流程
1. 员工入职 → 创建薪资结构(设定基本工资、岗位津贴、默认绩效系数)
2. 每月末 → 生成月度薪资记录(单个或批量,可指定当月绩效系数)
3. 修改记录 → 在草稿/驳回状态下修改绩效系数(自动重算)
4. 提交审核 → draft/rejected → pending
5. 审核通过 → pending → approved(或驳回 → rejected)
6. 确认发放 → approved → paid(仅 admin)
7. 调薪时 → 更新薪资结构(后续月份按新结构生成,历史记录不受影响)
批量生成
每月生成记录时支持三种方式:
- 指定员工 ID 列表:
POST /salaries/records/batch传employee_ids - 按部门生成:传
dept_id,为该部门下有薪资结构的在职员工生成 - 全员生成:不传
employee_ids和dept_id,为所有有薪资结构的在职员工生成
已存在的记录自动跳过。
测试
测试策略
三层各有独立测试,互不耦合:
| 层 | 测试方式 | 依赖 |
|---|---|---|
| repository | PostgreSQL 测试数据库 | 真实 DB |
| service | mock repository | 内存 mock |
| handler | mock service | 内存 mock |
运行测试
# 全部测试(需要 PostgreSQL)
go test ./...
# 指定包
go test ./repository/...
go test ./service/...
go test ./handler/...
# 显示详细输出
go test -v ./...
# 运行单个测试
go test -run TestEmployeeService_Create ./service/...
测试数据库配置
Repository 测试需要 PostgreSQL。通过环境变量配置:
| 变量 | 默认值 | 说明 |
|---|---|---|
| TEST_DB_HOST | localhost | 测试数据库主机 |
| TEST_DB_PORT | 5432 | 测试数据库端口 |
| TEST_DB_USER | postgres | 测试数据库用户 |
| TEST_DB_PASSWORD | postgres | 测试数据库密码 |
| TEST_DB_NAME | hr_test | 测试数据库名 |
| TEST_DB_SSLMODE | disable | SSL 模式 |
如果没有可用的 PostgreSQL,测试会自动跳过(t.Skip)。
每个测试开始前清空 employees 和 departments 表,保证测试之间互不影响。
用 docker-compose 运行测试
# 启动数据库
docker compose up -d db
# 创建测试数据库
docker compose exec db psql -U postgres -c "CREATE DATABASE hr_test;"
# 运行测试
go test ./...
# 关闭
docker compose down
Repository 测试
使用真实 PostgreSQL 数据库。测试辅助函数定义在 repository/test_helpers.go:
setupTestDB(t)— 连接测试数据库,清空表,执行迁移seedDept(t, db, name)— 插入测试部门seedEmployee(t, db, name, email, deptID)— 插入测试员工
Service 测试
使用 mock 实现,不依赖数据库。通过接口 mock 可以控制 repository 的行为,专注测试业务逻辑。
Handler 测试
使用 mock service,通过 httptest 模拟 HTTP 请求。测试请求解析、响应格式、错误映射。
添加新测试
以添加 Position 实体为例:
repository/position_repo_test.go— 用测试数据库测试 CRUDservice/position_service_test.go— 用 mock repo 测试业务逻辑handler/position_handler_test.go— 用 mock service 测试 HTTP 层
Docker 部署
Dockerfile
多阶段构建,最终镜像只包含静态二进制和 ca-certificates:
- 第一阶段(builder):编译 Go 程序,CGO_ENABLED=0 静态链接
- 第二阶段:alpine 最小镜像,只拷贝二进制
镜像大小约 10-15MB。
构建命令:
docker build -t hr-backend .
运行:
docker run -d \
-p 8080:8080 \
-e DB_HOST=your-db-host \
-e DB_PASSWORD=your-password \
-e REDIS_HOST=your-redis-host \
hr-backend
docker-compose
包含三个服务:
- app:Go 后端,依赖 db 和 redis 健康检查通过后才启动
- db:PostgreSQL 16,数据持久化到 Docker volume
- redis:Redis 7,用于缓存和 JWT Token 存储
# 启动
docker compose up -d
# 查看状态
docker compose ps
# 查看日志
docker compose logs -f
# 停止
docker compose down
# 停止并删除数据库数据
docker compose down -v
数据库管理
# 连接数据库
docker compose exec db psql -U postgres -d hr
# 查看表
\dt
# 查看员工
SELECT * FROM employees;
CI/CD
使用 GitHub Actions,配置在 .github/workflows/pipeline.yml。
流水线
PR 触发
对 main 分支的 Pull Request 触发:
- Lint:
go vet ./... - Test:启动 PostgreSQL + Redis service container,运行
go test -v ./...
测试使用 GitHub Actions 的 service container 功能,自动启动 PostgreSQL 和 Redis,测试完成后自动销毁,不需要外部数据库和缓存。
Push main 触发
代码合并到 main 后,在 lint + test 通过的基础上:
- Build & Push:构建 Docker 镜像,推送到 GitHub Container Registry (ghcr.io)
镜像标签:
sha-<commit-hash>:每次提交一个标签latest:最新标签
镜像使用
# 登录 GHCR
docker login ghcr.io -u <username> -p <github-token>
# 拉取镜像
docker pull ghcr.io/<owner>/hr:latest
# 运行
docker run -d \
-p 8080:8080 \
-e DB_HOST=your-db-host \
-e DB_PASSWORD=your-password \
ghcr.io/<owner>/hr:latest
自部署
如果不用 GHCR,也可以在服务器上直接从源码构建:
git clone <repo-url>
cd hr
docker compose up -d --build
开发指南
添加新实体
以添加 Position(职位)为例,需要添加以下文件:
1. 模型
创建 models/position.go:
package models
type Position struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
}
2. Repository
创建 repository/position_repo.go:
package repository
import (
"pms-backend/models"
"gorm.io/gorm"
)
type PositionRepository interface {
Create(pos *models.Position) error
GetAll() ([]models.Position, error)
ExistsByID(id uint) (bool, error)
}
type positionRepository struct {
db *gorm.DB
}
func NewPositionRepository(db *gorm.DB) PositionRepository {
return &positionRepository{db: db}
}
// 实现接口方法...
创建 repository/position_repo_test.go,用测试 PostgreSQL 数据库测试。
3. Service
创建 service/position_service.go:
package service
import (
"pms-backend/models"
"pms-backend/repository"
)
type PositionService interface {
Create(name string) (*models.Position, error)
GetAll() ([]models.Position, error)
}
type positionService struct {
posRepo repository.PositionRepository
}
func NewPositionService(posRepo repository.PositionRepository) PositionService {
return &positionService{posRepo: posRepo}
}
// 实现接口方法...
创建 service/position_service_test.go,用 mock repo 测试。
4. Handler
创建 handler/position_handler.go:
package handler
import (
"pms-backend/service"
"github.com/gin-gonic/gin"
)
type PositionHandler struct {
service service.PositionService
}
func NewPositionHandler(s service.PositionService) *PositionHandler {
return &PositionHandler{service: s}
}
// 实现 handler 方法...
创建 handler/position_handler_test.go,用 mock service 测试。
5. 注册路由
修改 handler/router.go,在 SetupRouter 中添加:
positions := r.Group("/positions")
{
positions.POST("", posHandler.Create)
positions.GET("", posHandler.GetAll)
}
6. 组装依赖
修改 main.go,添加:
posRepo := repository.NewPositionRepository(db)
posSvc := service.NewPositionService(posRepo)
posHandler := handler.NewPositionHandler(posSvc)
7. 数据库迁移
修改 config/db.go,在 NewDB 的 AutoMigrate 中添加 &models.Position{}。
命名约定
- 接口:大写开头,如
EmployeeRepository、EmployeeService - 实现:小写开头 struct,如
employeeRepository、employeeService - 构造函数:
NewXxx,如NewEmployeeRepository - 文件名:按实体名,如
employee_repo.go、employee_service.go - 测试文件:与实现文件对应,如
employee_repo_test.go
错误处理
- 业务错误定义在
service/errors.go,用 sentinel error - Service 层返回 error,Handler 用
errors.Is判断后返回对应 HTTP 状态码 - Repository 层直接返回 GORM 错误,不包装
构建文档
# 安装 mdbook
cargo install mdbook
# 构建
cd docs && mdbook build
# 本地预览
cd docs && mdbook serve
Changelog
v0.17.0 - 2026-04-30
新增
- 部门权限范围(scope)强制校验:manager 只能操作本部门及子部门的资源,不能操作同级或上级部门
- Employee Service 的 GetByID/Update/Delete/Create 增加 scopeDeptIDs 参数,校验目标是否在管辖范围内
- Department Service 的 GetByID/Update/Create 增加 scopeDeptIDs 参数,校验目标部门/父部门是否在管辖范围内
- Attendance Service 的 GenerateSummary 增加 scopeDeptIDs 参数
- Leave Service 的 CreateRequest 增加 scopeDeptIDs 参数
- 新增 checkScope 辅助函数(service/scope_helper.go)
- 新增 getScopeDeptIDs 辅助函数(handler/employee_handler.go)
- Router 为 GET /employees/:id、GET /depts/:id、DELETE /employees/:id、POST /attendance/summaries/generate、POST /leaves 加 RequireDeptAccess 中间件
- 新增 service 层 scope 测试(employee/department/attendance/leave out-of-scope 场景)
- api_test.http 补充 scope 场景测试(zhangsan 访问人事部/财务部资源 → 403)
v0.16.0 - 2026-04-30
新增
- handler 层 500 错误现在会 log.Printf 打印详情,响应 message 包含具体错误信息(internalError 辅助函数)
- seed 全面重写,覆盖所有业务实体和流程示例
修复
- ClockIn 未匹配的 GORM ErrRecordNotFound 被裸抛为 500 的问题
v0.15.0 - 2026-04-29
新增
- POST /users 接口,admin 可直接创建用户(指定角色或默认 staff)
v0.14.0 - 2026-04-29
新增
- User 模型新增 CreatedAt、UpdatedAt 字段
- GET /users 支持分页+搜索+排序(keyword、sort_by、sort_desc、page、page_size)
- DELETE /users/:id 删除用户(不能删除自己)
改动
- UserRepository.List 签名变更:
List() ([]models.User, error)→List(query ListQuery) (*ListResult, error) - UserService.List 签名变更:同步适配新 ListQuery/ListResult
- UserService 新增 Delete(id, currentUserID) 方法
v0.13.0 - 2026-04-29
新增
- Employee 模型新增 UserID(关联 User ID,唯一索引)和 User 关联对象
- Department 模型新增 LeaderID(部门负责人 Employee ID,索引)和 Leader 关联对象
- DeptTreeNode 新增 LeaderID、LeaderName 字段
- JWT Claims 新增 Role(admin/manager/staff)和 EmployeeID 字段
- RBAC 权限控制:RequireAdminOnly、RequireRole、RequireDeptAccess 中间件
- GET /employees/me 获取当前登录用户的员工档案
- 创建员工接口支持可选 user_id 字段(关联登录账号)
- 创建/更新部门接口支持可选 leader_id 字段(指定部门负责人)
- 员工列表按角色返回不同范围:admin 全部、manager 本部门+子部门、staff 403
- 打卡/请假 staff 只能操作自己,后端强制使用 JWT 中的 employee_id
- 请假审批 manager 只有本部门范围权限
- 薪资管理全部改为 admin only
- 用户管理全部改为 admin only
- 审计日志改为 admin only
改动
- RequireRole 签名变更:不再需要 userRepo 参数,从 JWT Claims 直接读取 role
- 路由注册:RequireRole/RequireAdminOnly/RequireDeptAccess 替换旧的 RequireRole(userRepo, ...)
- POST /auth/login 返回的 JWT payload 新增 role、employee_id 字段
- PUT /users/:id/password 归入 /users 组(需 admin 角色),非 admin 暂不可修改密码
- AuthService 构造函数新增 empRepo 参数(用于登录时查找关联员工)
v0.12.0 - 2026-04-29
新增
- AuditLog 操作日志模型(操作人、操作类型、实体类型/ID、变更内容 jsonb、IP 地址)
- 所有写操作自动记录审计日志(handler 层 logAudit 辅助函数)
- GET /audit-logs 审计日志查询接口(支持按操作人/实体类型/时间范围筛选,需 admin 角色)
- InjectAuditLog 中间件:通过 gin.Context 注入 AuditLogService
- AuditLogRepository、AuditLogService、AuditLogHandler 完整实现
- handler/service/repository 三层测试用例
v0.11.0 - 2026-04-28
新增
- Department 模型新增 Description(描述,最长 500)和 ParentID(父部门 ID)字段
- Department 自引用层级树:通过 parent_id 邻接表 + 内存拼树
- DeptTreeNode 树形响应结构
- GET /depts/tree 部门树查询接口
- DepartmentRepository 新增 GetByParentID、HasChildren 方法
- 创建部门支持 description 和 parent_id 参数
- 更新部门时校验循环引用(不能设自己为父,不能形成环路)
- 删除部门时校验是否有子部门(有子部门不可删除)
- 删除部门时校验是否有员工(有员工不可删除)
- ErrDeptHasChildren、ErrDeptHasEmployees、ErrDeptCircularRef sentinel error
- 种子数据添加二级部门层级关系
v0.10.0 - 2026-04-28
新增
- AttendanceRecord 打卡记录模型(上班/下班打卡,每日一条)
- AttendanceSummary 考勤月度统计模型(正常/迟到/早退/缺勤天数)
- LeaveRequest 请假申请模型(年假/病假/事假,单级审批)
- Employee 模型新增 WorkStartTime/WorkEndTime 字段(默认 09:00/18:00)
- 上班打卡接口:POST /attendance/clock-in
- 下班打卡接口:PUT /attendance/clock-out
- 打卡记录查询:GET /attendance/records(支持日期范围筛选)
- 考勤统计生成:POST /attendance/summaries/generate
- 考勤统计查询:GET /attendance/summaries
- 提交请假:POST /leaves
- 请假审批:PUT /leaves/:id/approve、PUT /leaves/:id/reject
- 请假查询:GET /leaves(支持状态筛选)
v0.9.0 - 2026-04-28
新增
- SalaryRecord 状态生命周期:draft → pending → approved → paid,加 rejected 驳回
- SalaryRecord 添加 StructureID、Status、ReviewedBy/At、PaidBy/At 字段
- PUT /salaries/records/:id/submit 提交审核
- PUT /salaries/records/:id/approve 审核通过
- PUT /salaries/records/:id/reject 审核驳回
- PUT /salaries/records/:id/pay 确认发放(仅 admin)
- PUT /salaries/records/:id 修改薪资记录(仅 draft/rejected 状态)
- POST /salaries/records/batch 批量生成月度薪资记录(支持按员工/部门/全员)
- GET /salaries/records 支持按 status 筛选
- 修改绩效系数时自动重算 actual_salary
v0.8.0 - 2026-04-28
新增
- SalaryStructure 薪资结构模型(基本工资/岗位津贴/绩效系数)
- SalaryRecord 月度薪资记录模型
- 薪资结构 CRUD 接口:POST/GET/PUT/DELETE /salaries/structures
- 月度薪资计算与记录:POST /salaries/records
- 薪资历史查询:GET /salaries/records(支持按员工/年/月筛选)
- 薪资计算公式:(基本工资 + 岗位津贴) × 绩效系数
- 权限控制:查看需认证,创建/编辑需 admin/manager,删除需 admin
v0.7.0 - 2026-04-28
新增
- Role 模型 + 种子数据(admin/manager/staff)
- User 模型添加 Status(active/disabled)和 Roles 多对多关联
- RBAC 中间件 RequireRole,按角色控制接口访问
- JWT Claims 添加 UserID 字段
- GET /roles 角色列表接口
- GET /users 用户列表接口
- GET /users/:id 用户详情接口
- PUT /users/:id/status 启用/禁用用户接口
- PUT /users/:id/roles 分配角色接口
- PUT /users/:id/password 修改/重置密码接口
- 注册支持 role_ids 参数,首个用户自动 admin
- 被禁用用户无法登录
v0.6.0 - 2026-04-28
新增
- Employee 模型添加 updated_at、created_by、updated_by 字段
- Department 模型添加 created_at、updated_at、created_by、updated_by 字段
- 创建时自动填充 created_by 和 updated_by(从 JWT 提取当前用户名)
- 更新时自动填充 updated_by
v0.5.0 - 2026-04-28
新增
- GET /auth/me 获取当前登录用户信息接口
- UserRepository.GetByID 按 ID 查询用户
- AuthService.GetCurrentUser 通过用户名获取当前用户
- ErrUserNotFound sentinel error
- handler/service 测试用例
v0.4.0 - 2026-04-28
新增
- Redis 集成:config/redis.go,InitRedis 连接重试(30 次,2 秒间隔)
- 员工列表 Redis 缓存:按查询参数生成缓存 key,过期时间 10 分钟
- 缓存一致性:员工创建/更新/删除后自动清除所有员工缓存
- JWT Token 存入 Redis(auth:token:{jti}),过期时间与 JWT 相同(24 小时)
- JWT 中间件校验 Redis 中 Token 存在性,注销后 Token 立即失效
- POST /auth/logout 注销接口:从 Redis 删除 Token
- docker-compose.yml 添加 redis:7-alpine 服务
- 环境变量:REDIS_HOST、REDIS_PORT、REDIS_PASSWORD
- miniredis 测试:auth service、employee service 缓存测试
- config/redis_test.go:Redis 配置测试
改动
- EmployeeService 构造函数新增 *redis.Client 参数
- 缓存逻辑从 handler 层下沉到 service 层(handler 不再直接操作 Redis)
- auth_service_test.go 使用 miniredis 替代手动 mock Redis
- handler/employee_handler_test.go 移除 Redis 依赖
- repository/employee_repo_test.go 修复 interface{} 类型断言
v0.3.0 - 2026-04-28
新增
- Employee 添加 phone(手机号)、id_number(身份证号)字段
- 员工列表接口改为分页查询:支持 keyword 模糊搜索、sort_by 排序、page/page_size 分页
- 部门重名检测(创建/更新时校验)
- 部门 CRUD 补全:GetByID、Update、Delete
- 员工 GetByID 接口
- PRE_COMMIT.md 提交前检查清单
- CLAUDE.md 项目协作指南
改动
- GET /employees 从返回数组改为返回分页结构(data/total/page/page_size/total_pages)
v0.2.0 - 2026-04-28
改动
- 数据库从 SQLite 切换到 PostgreSQL 16
- 引入分层架构:handler → service → repository
- handler/service/repository 各层定义接口,支持 mock 测试
- 去掉全局 DB 变量,改为依赖注入
- 新增 service 层,业务逻辑从 handler 下沉
- 新增 CORS 中间件
- 配置改用环境变量,支持 DB_HOST/PORT/USER/PASSWORD/NAME/SSLMode
新增
- Dockerfile(多阶段构建)
- docker-compose.yml(app + postgres)
- GitHub Actions CI/CD(lint + test + build image)
- 项目文档(mdbook)
移除
- SQLite 依赖
- 全局变量 config.DB
- config/testdb.go
v0.1.0 - 2026-04-28
初始功能
- 部门 CRUD:创建、查询列表
- 员工 CRUD:创建、查询列表、更新、删除
- 员工邮箱唯一性校验
- 员工所属部门存在性校验
- SQLite 数据库
- 种子数据工具(tools/seed.go)