项目概览

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 引用
handlerHTTP 处理,路由,中间件依赖 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 可指定
响应codemessage
成功200-
缺少必填字段400invalid request body
用户名已存在400username 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_iduint用户 ID
usernamestring用户名
rolestring角色:admin / manager / staff
employee_id*uint关联的员工 ID,未关联时不存在此字段
响应codemessage
账号被禁用403user is disabled
响应codemessage
成功200-
用户名或密码错误401invalid username or password

获取当前用户信息

GET /auth/me

需要 JWT 认证。

响应示例:

{
  "code": 200,
  "data": {
    "id": 1,
    "username": "admin"
  }
}

错误响应:

状态码说明
401未认证
404用户不存在

注销

POST /auth/logout

需要认证。从 Redis 删除对应 Token。

响应codemessage
成功200-
未认证401unauthorized

部门

创建部门

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 只能在自己管辖范围内的父部门下创建子部门。

响应codemessage
成功200-
缺少必填字段400invalid request body
名称已存在400department name already exists
父部门不存在400parent department not found
无权限403forbidden: cannot create department outside your department scope

查询部门列表(分页+搜索+排序)

GET /depts(需认证)

查询参数:

参数类型默认值说明
keywordstring-按 name 模糊搜索
sort_bystringid排序字段:id, name
sort_descboolfalse是否降序(传 "true")
pageint1页码
page_sizeint10每页数量,最大 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 仅可查看管辖范围内的部门)

响应codemessage
成功200-
无效 ID400invalid id
无权限403forbidden: cannot access department outside your scope
不存在404department not found

更新部门

PUT /depts/:id(需认证,admin/manager 且为部门负责人,manager 仅可更新管辖范围内的部门)

请求体(只传需要更新的字段):

{
  "name": "技术部",
  "description": "技术中心",
  "leader_id": 5,
  "parent_id": 2
}

更新 parent_id 时会校验:不能设自己为父、不能产生循环引用、父部门必须存在。

响应codemessage
成功200-
无效 ID400invalid id
请求体无效400invalid request body
名称已存在400department name already exists
循环引用400cannot set parent: circular reference
无权限403forbidden: cannot update department outside your department scope
父部门不存在404department not found
部门不存在404department not found

删除部门

DELETE /depts/:id(需认证,admin)

有子部门或有员工的部门不能删除,需先迁移子部门和员工。

响应codemessage
成功200-
无效 ID400invalid id
有子部门400department has child departments
有员工400department has employees
部门不存在404department 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。

响应codemessage
成功200-
缺少必填字段400invalid request body
邮箱已存在400email already exists
user_id 已关联其他员工400employee already linked to a user
部门不存在400department not found
无权限403forbidden: cannot create employee outside your department scope

查询员工列表(分页+搜索+排序)

GET /employees(需认证,admin 查全部,manager 查本部门+子部门,staff 返回 403)

查询参数:

参数类型默认值说明
keywordstring-按 name/email/phone/id_number 模糊搜索
sort_bystringid排序字段:id, name, email, phone, status, created_at
sort_descboolfalse是否降序(传 "true")
pageint1页码
page_sizeint10每页数量,最大 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。

响应codemessage
成功200-
未认证401unauthorized
未关联员工档案404employee profile not found

查询单个员工

GET /employees/:id(需认证,admin 全量,manager 仅本部门+子部门,staff 仅本部门)

响应codemessage
成功200-
无效 ID400invalid id
无权限403forbidden: cannot access employee outside your department scope
不存在404employee not found

更新员工

PUT /employees/:id(需认证,admin/manager 且为部门负责人,manager 仅可更新管辖范围内的员工,不能将员工移出管辖范围)

请求体(只传需要更新的字段):

{
  "name": "张伟2",
  "phone": "13900000001"
}
响应codemessage
成功200-
无效 ID400invalid id
请求体无效400invalid request body
邮箱已存在400email already exists
部门不存在400department not found
无权限403forbidden: cannot update employee outside your department scope

删除员工

DELETE /employees/:id(需认证,仅 admin)

响应codemessage
成功200-
无效 ID400invalid id
不存在404employee 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。每个员工只能有一个薪资结构。

响应codemessage
成功200-
缺少必填字段400invalid request body
员工不存在400employee not found
薪资结构已存在400salary structure already exists for this employee

查询薪资结构列表(分页+搜索+排序)

GET /salaries/structures(需 admin 角色)

查询参数:

参数类型默认值说明
keywordstring-按员工姓名/邮箱模糊搜索
sort_bystringid排序字段:id, base_salary, position_allowance, performance_factor, created_at
sort_descboolfalse是否降序(传 "true")
pageint1页码
page_sizeint10每页数量,最大 100

查询员工的薪资结构

GET /salaries/structures/employees/:emp_id(需 admin 角色)

响应codemessage
成功200-
无效员工 ID400invalid employee id
不存在404salary structure not found

更新薪资结构

PUT /salaries/structures/:id(需 admin 角色)

请求体(只传需要更新的字段):

{
  "base_salary": 12000,
  "performance_factor": 1.2
}
响应codemessage
成功200-
无效 ID400invalid id
请求体无效400invalid request body
薪资结构不存在404salary structure not found

删除薪资结构

DELETE /salaries/structures/:id(需 admin 角色)

响应codemessage
成功200-
无效 ID400invalid id
薪资结构不存在404salary 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。

同一员工同一年月不能重复生成。

响应codemessage
成功200-
缺少必填字段400invalid request body
年月无效400invalid year or month
员工不存在400employee not found
薪资结构不存在400salary structure not found for this employee
记录已存在400salary 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 可选,优先级:

  1. employee_ids 不为空:为指定员工生成
  2. dept_id 不为空:为该部门下有薪资结构的在职员工生成
  3. 都不传:为所有有薪资结构的在职员工生成

已存在的记录自动跳过,不报错。

响应codemessage
成功200-
缺少必填字段400invalid request body
年月无效400invalid year or month
部门不存在400department not found

修改薪资记录

PUT /salaries/records/:id(需 admin 角色)

仅 draft 或 rejected 状态可修改。

请求体(只传需要更新的字段):

{
  "performance_factor": 1.5
}

修改 performance_factor 时自动重算 actual_salary。

响应codemessage
成功200-
无效 ID400invalid id
非草稿/驳回状态400salary record is not in draft/rejected status
不存在404salary record not found

提交审核

PUT /salaries/records/:id/submit(需 admin 角色)

draft 或 rejected → pending。

响应codemessage
成功200-
非草稿/驳回状态400salary record is not in draft/rejected status
不存在404salary record not found

审核通过

PUT /salaries/records/:id/approve(需 admin 角色)

pending → approved。

响应codemessage
成功200-
非待审核状态400salary record is not in pending status
不存在404salary record not found

审核驳回

PUT /salaries/records/:id/reject(需 admin 角色)

pending → rejected。驳回后可修改绩效系数再重新提交。

响应codemessage
成功200-
非待审核状态400salary record is not in pending status
不存在404salary record not found

确认发放

PUT /salaries/records/:id/pay(需 admin 角色)

approved → paid。发放后不可修改。

响应codemessage
成功200-
非已审核状态400salary record is not in approved status
不存在404salary record not found

查询薪资记录列表(分页+筛选+排序)

GET /salaries/records(需 admin 角色)

查询参数:

参数类型默认值说明
employee_idint-按员工 ID 筛选
yearint-按年份筛选
monthint-按月份筛选
statusstring-按状态筛选:draft, pending, approved, rejected, paid
sort_bystringid排序字段:id, year, month, actual_salary, status, created_at
sort_descboolfalse是否降序(传 "true")
pageint1页码
page_sizeint10每页数量,最大 100

查询单条薪资记录

GET /salaries/records/:id(需 admin 角色)

响应codemessage
成功200-
无效 ID400invalid id
不存在404salary record not found

考勤管理

上班打卡

POST /attendance/clock-in(需认证,staff 仅能为自己打卡)

请求体:

{
  "employee_id": 1
}

employee_id 为必填。同一天同一员工不能重复打卡。staff 角色只能传自己的 employee_id(从 JWT 的 employee_id 字段获取),否则返回 403。

响应codemessage
成功200-
缺少必填字段400invalid request body
已打卡400already clocked in today
员工不存在400employee not found
staff 替别人打卡403can only clock in for yourself
staff 未关联员工档案403no employee profile linked

下班打卡

PUT /attendance/clock-out(需认证,staff 仅能为自己打卡)

请求体:

{
  "employee_id": 1
}

必须先上班打卡才能下班打卡。staff 角色只能传自己的 employee_id,否则返回 403。

响应codemessage
成功200-
缺少必填字段400invalid request body
未上班打卡400not clocked in today
已下班打卡400already clocked out today
staff 替别人打卡403can only clock out for yourself
staff 未关联员工档案403no employee profile linked

查询打卡记录列表

GET /attendance/records(需认证,staff 仅能查看自己记录)

查询参数:

参数类型默认值说明
employee_idint-按员工 ID 筛选
start_datestring-起始日期 YYYY-MM-DD
end_datestring-结束日期 YYYY-MM-DD
keywordstring-按员工姓名/邮箱搜索
sort_bystringdate排序字段:id, date, created_at
sort_descboolfalse是否降序
pageint1页码
page_sizeint10每页数量,最大 100

查询单条打卡记录

GET /attendance/records/:id(需认证)

响应codemessage
成功200-
不存在404attendance record not found

删除打卡记录

DELETE /attendance/records/:id(需 admin 角色)

响应codemessage
成功200-
无效 ID400invalid id
不存在404attendance record not found

批量删除打卡记录

DELETE /attendance/records/batch(需 admin 角色)

请求体:

{
  "ids": [1, 2, 3]
}
响应codemessage
成功200-
缺少必填字段400invalid request body

生成考勤月度统计

POST /attendance/summaries/generate(需 admin/manager 角色,manager 仅可生成管辖范围内员工的汇总)

请求体:

{
  "employee_id": 1,
  "year": 2026,
  "month": 4
}

根据员工工作时间(WorkStartTime/WorkEndTime,默认 09:00/18:00)判定迟到/早退/缺勤。重复生成会覆盖旧数据。

响应codemessage
成功200-
缺少必填字段400invalid request body
年月无效400invalid year or month
员工不存在400employee not found
无权限403forbidden: cannot generate summary for employee outside your department scope

查询考勤统计列表

GET /attendance/summaries(需认证,staff 仅能查看自己统计)

查询参数:

参数类型默认值说明
employee_idint-按员工 ID 筛选
yearint-按年份筛选
monthint-按月份筛选
sort_bystringid排序字段:id, year, month, created_at
sort_descboolfalse是否降序
pageint1页码
page_sizeint10每页数量,最大 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 只能为管辖范围内的员工创建请假。

响应codemessage
成功200-
缺少必填字段400invalid request body
无效请假类型400invalid leave type, must be annual/sick/personal
日期无效400end date must be >= start date
员工不存在400employee not found
staff 替别人请假403can only create leave for yourself
staff 未关联员工档案403no employee profile linked
无权限403forbidden: cannot create leave for employee outside your department scope

查询请假列表

GET /leaves(需认证,staff 仅能查看自己请假)

查询参数:

参数类型默认值说明
employee_idint-按员工 ID 筛选
statusstring-按状态筛选:pending/approved/rejected
keywordstring-按员工姓名/邮箱搜索
sort_bystringid排序字段:id, type, status, start_date, created_at
sort_descboolfalse是否降序
pageint1页码
page_sizeint10每页数量,最大 100

查询单条请假申请

GET /leaves/:id(需认证)

响应codemessage
成功200-
不存在404leave request not found

审批通过

PUT /leaves/:id/approve(需 admin/manager 且为部门负责人)

pending → approved。manager 仅能审批本部门及子部门的请假。

响应codemessage
成功200-
非待审批状态400leave request is not in pending status
不存在404leave request not found
manager 审批非本部门403forbidden: cannot approve leave outside your department

审批驳回

PUT /leaves/:id/reject(需 admin/manager 且为部门负责人)

pending → rejected。manager 仅能驳回本部门及子部门的请假。

响应codemessage
成功200-
非待审批状态400leave request is not in pending status
不存在404leave request not found
manager 驳回非本部门403forbidden: cannot reject leave outside your department

获取角色列表

GET /roles(需认证,所有角色可查看)

用户管理

创建用户

POST /users(需 admin 角色)

请求体:

{
  "username": "newuser",
  "password": "123456",
  "role_ids": [2]
}
  • password 最少 6 位
  • role_ids 可选,不传则默认分配 staff 角色
响应codemessage
成功200-
用户名已存在400username already exists
请求体无效400invalid request body

获取用户列表(分页+搜索+排序)

GET /users(需 admin 角色)

查询参数:

参数类型默认值说明
keywordstring-按 username 模糊搜索
sort_bystringid排序字段:id, username, status, created_at
sort_descboolfalse是否降序(传 "true")
pageint1页码
page_sizeint10每页数量,最大 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 角色)

响应codemessage
成功200-
无效 ID400invalid id
不存在404user not found

修改用户状态

PUT /users/:id/status(需 admin 角色)

请求体:

{
  "status": "disabled"
}

status 取值:active / disabled

响应codemessage
成功200-
无效 ID400invalid id
请求体无效400invalid request body
无效状态400invalid status

分配角色

PUT /users/:id/roles(需 admin 角色)

请求体:

{
  "role_ids": [1, 2]
}
响应codemessage
成功200-
无效 ID400invalid id
请求体无效400invalid request body

修改密码

PUT /users/:id/password(需 admin 角色)

请求体:

{
  "new_password": "654321"
}

admin 可重置任意用户密码,仅需提供 new_password。new_password 最少 6 位。

响应codemessage
成功200-
无效 ID400invalid id
请求体无效400invalid request body

删除用户

DELETE /users/:id(需 admin 角色)

不能删除自己。

响应codemessage
成功200-
无效 ID400invalid id
删除自己400cannot delete yourself

权限矩阵

操作adminmanager(部门负责人)staff
查看部门/部门树YYY
创建/编辑部门YY(本部门范围)-
删除部门Y--
查看员工列表Y(全部)Y(本部门+子部门)-
查看 /employees/meYYY
创建/编辑员工YY(本部门范围)-
删除员工Y--
管理用户(含删除)Y--
查看角色列表YYY
薪资管理(全部)Y--
打卡YY仅自己
删除考勤记录Y--
查看考勤记录/统计YY仅自己
生成考勤统计YY-
提交请假YY仅自己
查看请假列表YY仅自己
审批请假YY(本部门范围)-
查看审计日志Y--
删除审计日志Y--

"本部门范围" 指该 manager 作为 leader_id 所属部门及其子部门。manager 如果不是任何部门的负责人,访问需要部门权限的接口会返回 403。

"仅自己" 指后端强制使用 JWT 中的 employee_id,忽略请求中的 employee_id 参数或列表筛选。

操作日志

所有写操作(创建、更新、删除、审批等)自动记录审计日志,包含操作人、操作类型、实体类型、实体 ID、变更内容、IP 地址和时间。

查询审计日志

GET /audit-logs(需 admin 角色)

查询参数:

参数类型默认值说明
operatorstring-按操作人筛选
entity_typestring-按实体类型筛选:user, department, employee, salary_structure, salary_record, attendance_record, attendance_summary, leave_request
start_timestring-起始时间 RFC3339 格式
end_timestring-结束时间 RFC3339 格式
sort_bystringid排序字段:id, created_at, action, entity_type
sort_descboolfalse是否降序(传 "true")
pageint1页码
page_sizeint10每页数量,最大 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 角色)

响应codemessage
成功200-
无效 ID400invalid id
不存在404audit log not found

批量删除审计日志

DELETE /audit-logs/batch(需 admin 角色)

请求体:

{
  "ids": [1, 2, 3]
}
响应codemessage
成功200-
缺少必填字段400invalid 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
  }
}
字段类型说明
dataarray当前页数据
totalint64总记录数
pageint当前页码(从 1 开始)
page_sizeint每页数量
total_pagesint总页数

分页请求参数

所有列表接口通用:

参数类型默认值说明
pageint1页码,最小 1
page_sizeint10每页数量,范围 1-100
keywordstring-模糊搜索关键词
sort_bystringid排序字段(各接口不同,见下方)
sort_descstringfalse是否降序,传 "true"

认证

注册

POST /auth/register
{
  "username": "admin",
  "password": "123456",
  "role_ids": [1]
}
字段类型必填说明
usernamestringY用户名,唯一
passwordstringY密码,最少 6 位
role_idsuint[]N角色 ID 列表,不传默认 staff;第一个用户自动 admin

成功响应 data 为 User 对象。

登录

POST /auth/login
{
  "username": "admin",
  "password": "123456"
}

响应:

{
  "code": 200,
  "data": { "token": "eyJhbGciOi..." }
}
HTTPcodemessage场景
200200-成功
401401invalid username or password用户名或密码错误
403403user is disabled账号被禁用

Token 过期时间 24 小时。前端应在 401 时跳转登录页。

获取当前用户

GET /auth/me

响应:

{
  "code": 200,
  "data": { "id": 1, "username": "admin" }
}

注销

POST /auth/logout

用户管理

所有用户接口需 admin 角色。

用户列表

GET /users

查询参数:

参数类型默认值说明
keywordstring-按 username 模糊搜索
sort_bystringid可选:id, username, status, created_at
sort_descstringfalse"true" 降序
pageint1页码
page_sizeint10每页数量

响应示例:

{
  "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 对象字段:

字段类型说明
iduint用户 ID
usernamestring用户名
statusstring状态:active / disabled
rolesRole[]角色列表
created_atstring创建时间 RFC3339
updated_atstring更新时间 RFC3339

注意:password 字段不会在 JSON 中返回。

获取单个用户

GET /users/:id
HTTPcodemessage
200200-
400400invalid id
404404user not found

修改用户状态

PUT /users/:id/status
{ "status": "disabled" }

status 取值:active / disabled

HTTPcodemessage
200200-
400400invalid id
400400invalid request body
400400invalid status

分配角色

PUT /users/:id/roles
{ "role_ids": [1, 2] }

role_ids 为完整的角色 ID 列表,会替换现有角色(不是增量追加)。

HTTPcodemessage
200200-
400400invalid id
400400invalid request body

角色 ID 参考:

IDname说明
1admin系统管理员
2manager部门经理
3staff普通员工

可通过 GET /roles 获取完整角色列表。

修改/重置密码

PUT /users/:id/password

admin 操作(重置他人密码):

{ "new_password": "654321" }

仅需 new_password,最少 6 位。

非 admin 操作(修改自己密码):

{
  "old_password": "123456",
  "new_password": "654321"
}

需同时提供 old_passwordnew_password

HTTPcodemessage
200200-
400400invalid id
400400invalid request body
400400wrong old password
403403forbidden

删除用户

DELETE /users/:id
HTTPcodemessage
200200-
400400invalid id
400400cannot delete yourself

前端应在删除前判断是否为当前登录用户,给出禁用/提示。


部门管理

部门列表

GET /depts

查询参数:

参数类型默认值说明
keywordstring-按 name 模糊搜索
sort_bystringid可选:id, name
sort_descstringfalse"true" 降序
pageint1页码
page_sizeint10每页数量

部门树

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
}
字段类型必填说明
namestringY名称,唯一
descriptionstringN描述
leader_iduintN负责人 Employee ID
parent_iduintN父部门 ID,null 为顶级

更新部门

PUT /depts/:id

只传需要更新的字段。更新 parent_id 时校验循环引用。

删除部门

DELETE /depts/:id

仅 admin。有子部门或有员工时不可删除(code 400)。


员工管理

员工列表

GET /employees

查询参数:

参数类型默认值说明
keywordstring-按 name/email/phone/id_number 模糊搜索
sort_bystringid可选:id, name, email, phone, status, created_at
sort_descstringfalse"true" 降序
pageint1页码
page_sizeint10每页数量

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"
}
字段类型必填说明
namestringY姓名
emailstringY邮箱,唯一
phonestringN手机号
id_numberstringN身份证号
dept_iduintY部门 ID
user_iduintN关联登录账号 ID,一个 User 最多关联一个 Employee
statusstringN默认 active

Employee 对象包含 department 关联字段(Preload),可直接取部门名称。

更新员工

PUT /employees/:id

只传需要更新的字段。更新 dept_idemail 时有存在性校验。

删除员工

DELETE /employees/:id

仅 admin。


薪资管理

所有薪资接口需 admin 角色。

薪资结构列表

GET /salaries/structures

查询参数:

参数类型默认值说明
keywordstring-按员工姓名/邮箱搜索
sort_bystringid可选:id, base_salary, position_allowance, performance_factor, created_at
sort_descstringfalse降序
pageint1页码
page_sizeint10每页数量

创建薪资结构

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_idint-按员工 ID
yearint-按年份
monthint-按月份
statusstring-draft/pending/approved/rejected/paid
sort_bystringid可选: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 }
HTTPcodemessage
200200-
400400invalid id
400400salary record is not in draft/rejected status
404404salary record not found

查询单条薪资记录

GET /salaries/records/:id
HTTPcodemessage
200200-
400400invalid id
404404salary 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_idint-按员工
start_datestring-起始日期 YYYY-MM-DD
end_datestring-结束日期 YYYY-MM-DD
keywordstring-按员工姓名/邮箱搜索
sort_bystringdate可选:id, date, created_at
sort_descstringfalse降序
pageint1页码
page_sizeint10每页数量

查询单条打卡记录

GET /attendance/records/:id
HTTPcodemessage
200200-
404404attendance record not found

生成考勤统计

POST /attendance/summaries/generate
{ "employee_id": 1, "year": 2026, "month": 4 }

需要 admin/manager 角色。重复生成会覆盖。

考勤统计列表

GET /attendance/summaries
参数类型默认值说明
employee_idint-按员工
yearint-按年份
monthint-按月份
sort_bystringid可选:id, year, month, created_at
sort_descstringfalse降序
pageint1页码
page_sizeint10每页数量

请假管理

提交请假

POST /leaves
{
  "employee_id": 1,
  "type": "annual",
  "start_date": "2026-05-01",
  "end_date": "2026-05-03",
  "reason": "年假"
}
字段类型必填说明
employee_iduintY员工 ID
typestringYannual/sick/personal
start_datestringY开始日期 YYYY-MM-DD
end_datestringY结束日期 YYYY-MM-DD,需 >= start_date
reasonstringN请假原因

staff 只能为本人申请。

请假列表

GET /leaves
参数类型默认值说明
employee_idint-按员工
statusstring-pending/approved/rejected
keywordstring-按员工姓名/邮箱搜索
sort_bystringid可选:id, type, status, start_date, created_at
sort_descstringfalse降序
pageint1页码
page_sizeint10每页数量

查询单条请假申请

GET /leaves/:id
HTTPcodemessage
200200-
404404leave 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
参数类型默认值说明
operatorstring-按操作人
entity_typestring-按实体类型
start_timestring-起始时间 RFC3339
end_timestring-结束时间 RFC3339
sort_bystringid可选:id, created_at, action, entity_type
sort_descstringfalse降序
pageint1页码
page_sizeint10每页数量

entity_type 取值:user, department, employee, salary_structure, salary_record, attendance_record, attendance_summary, leave_request


权限概览

操作adminmanager(部门负责人)staff
查看部门/部门树YYY
创建/编辑部门YY(本部门范围)-
删除部门Y--
查看员工列表Y(全部)Y(本部门+子部门)-
/employees/meYYY
创建/编辑员工YY(本部门范围)-
删除员工Y--
用户管理(含删除)Y--
查看角色列表YYY
薪资管理(全部)Y--
打卡YY仅自己
查看考勤记录/统计YY仅自己
生成考勤统计YY-
提交请假YY仅自己
查看请假列表YY仅自己
审批请假YY(本部门范围)-
查看审计日志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_iduint用户 ID
usernamestring用户名
rolestring角色名:admin / manager / staff
employee_id*uint关联的员工 ID,未关联时不出现
expint64过期时间戳

前端可用 jwt-decode 库解析 token 获取 role 和 employee_id。

配置说明

环境变量

变量默认值说明
DB_HOSTlocalhostPostgreSQL 主机
DB_PORT5432PostgreSQL 端口
DB_USERpostgres数据库用户名
DB_PASSWORDpostgres数据库密码
DB_NAMEhr数据库名
DB_SSLMODEdisableSSL 模式
SERVER_PORT8080服务监听端口
REDIS_HOSTlocalhostRedis 主机
REDIS_PORT6379Redis 端口
REDIS_PASSWORDRedis 密码
JWT_SECRETchange-me-in-productionJWT 签名密钥

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) 会:

  1. 用 DSN 连接 PostgreSQL(重试 30 次,2 秒间隔)
  2. 执行 AutoMigrate(自动建表/加列)

Redis 初始化

config.InitRedis(cfg) 会:

  1. 用 RedisAddr 连接 Redis(重试 30 次,2 秒间隔)
  2. 用于员工列表缓存和 JWT Token 存储

数据模型

User

文件:models/user.go

字段类型说明
IDuint主键,自增
Usernamestring用户名,非空,唯一索引
Passwordstring密码(bcrypt 哈希),JSON 不输出
Statusstring状态,非空,默认 "active",最长 20:active/disabled
Roles[]Role角色,多对多 user_roles
CreatedAttime.Time创建时间
UpdatedAttime.Time更新时间

Role

文件:models/role.go

字段类型说明
IDuint主键,自增
Namestring角色名,唯一索引
Descriptionstring角色描述

Department

文件:models/department.go

字段类型说明
IDuint主键,自增
Namestring部门名称,非空,唯一
Descriptionstring部门描述,最长 500
LeaderID*uint部门负责人 Employee ID,索引,null 表示暂无负责人
Leader*Employee关联的负责人员工对象(外键 LeaderID)
ParentID*uint父部门 ID,索引,null 表示顶级部门
Parent*Department关联的父部门对象(外键 ParentID)
Children[]Department关联的子部门列表(外键 ParentID)
CreatedAttime.Time创建时间
UpdatedAttime.Time更新时间
CreatedBystring创建人,最长 100
UpdatedBystring更新人,最长 100

DeptTreeNode(树形响应结构):

字段类型说明
IDuint部门 ID
Namestring部门名称
Descriptionstring部门描述
LeaderID*uint部门负责人 Employee ID
LeaderNamestring负责人姓名(仅树形响应)
ParentID*uint父部门 ID
Children[]DeptTreeNode子部门列表

Employee

文件:models/employee.go

字段类型说明
IDuint主键,自增
UserID*uint关联用户 ID,唯一索引,null 表示未关联登录账号
User*User关联的用户对象(外键 UserID)
Namestring姓名,非空
Emailstring邮箱,非空,唯一
Phonestring手机号,最长 20
IDNumberstring身份证号,最长 18,数据库列名 id_number
DeptIDuint所属部门 ID,外键
DepartmentDepartment关联的部门对象
Statusstring状态,默认 "active"
WorkStartTimestring上班时间,默认 "09:00",最长 5
WorkEndTimestring下班时间,默认 "18:00",最长 5
CreatedAttime.Time创建时间
UpdatedAttime.Time更新时间
CreatedBystring创建人,最长 100
UpdatedBystring更新人,最长 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

字段类型说明
IDuint主键,自增
EmployeeIDuint员工 ID,非空,唯一索引
BaseSalaryfloat64基本工资,非空,默认 0
PositionAllowancefloat64岗位津贴,非空,默认 0
PerformanceFactorfloat64绩效系数,非空,默认 1
CreatedAttime.Time创建时间
UpdatedAttime.Time更新时间
CreatedBystring创建人,最长 100
UpdatedBystring更新人,最长 100

SalaryRecord

文件:models/salary.go

字段类型说明
IDuint主键,自增
EmployeeIDuint员工 ID,非空,索引
StructureIDuint薪资结构 ID(快照),非空
Yearint年份,非空
Monthint月份,非空
BaseSalaryfloat64生成时的基本工资(快照)
PositionAllowancefloat64生成时的岗位津贴(快照)
PerformanceFactorfloat64本月绩效系数
ActualSalaryfloat64实发薪资 = (基本工资 + 岗位津贴) × 绩效系数
Statusstring状态,非空,默认 draft:draft/pending/approved/rejected/paid
ReviewedBystring审核人,最长 100
ReviewedAt*time.Time审核时间
PaidBystring发放人,最长 100
PaidAt*time.Time发放时间
CreatedAttime.Time创建时间
CreatedBystring创建人,最长 100

AttendanceRecord

文件:models/attendance.go

字段类型说明
IDuint主键,自增
EmployeeIDuint员工 ID,非空,索引
Datetime.Time日期(date 类型)
ClockIn*time.Time上班打卡时间
ClockOut*time.Time下班打卡时间
CreatedBystring创建人,最长 100
CreatedAttime.Time创建时间

AttendanceSummary

文件:models/attendance.go

字段类型说明
IDuint主键,自增
EmployeeIDuint员工 ID,非空,索引
Yearint年份,非空
Monthint月份,非空
NormalDaysint正常天数,默认 0
LateDaysint迟到天数,默认 0
EarlyDaysint早退天数,默认 0
AbsentDaysint缺勤天数,默认 0
CreatedAttime.Time创建时间

LeaveRequest

文件:models/leave.go

字段类型说明
IDuint主键,自增
EmployeeIDuint员工 ID,非空,索引
Typestring请假类型,最长 20:annual/sick/personal
StartDatetime.Time开始日期(date 类型)
EndDatetime.Time结束日期(date 类型)
Reasonstring请假原因,最长 500
Statusstring状态,默认 pending:pending/approved/rejected
ReviewedBystring审批人,最长 100
ReviewedAt*time.Time审批时间
CreatedBystring创建人,最长 100
CreatedAttime.Time创建时间

AuditLog

文件:models/audit_log.go

字段类型说明
IDuint主键,自增
Operatorstring操作人,最长 64,索引
Actionstring操作类型,最长 32,索引
EntityTypestring实体类型,最长 64,索引
EntityIDuint实体 ID,索引
Changesstring变更内容,jsonb
IPAddressstring操作 IP,最长 45
CreatedAttime.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-011.0正常出勤,完成本职工作(10000 + 2000) × 1.012000 元
2026-021.2主导项目提前交付,获评月度优秀(10000 + 2000) × 1.214400 元
2026-030.8多次迟到,季度考核不达标(10000 + 2000) × 0.89600 元
2026-041.5关键技术突破,特殊贡献奖(10000 + 2000) × 1.518000 元
2026-050.5长期请假,部分工作由他人代管(10000 + 2000) × 0.56000 元

员工李娜,薪资结构:基本工资 8000 元,岗位津贴 0 元(无津贴)。

月份绩效系数场景计算过程实发薪资
2026-011.0正常(8000 + 0) × 1.08000 元
2026-021.1加班赶工(8000 + 0) × 1.18800 元

绩效系数的设置时机

  • 薪资结构中设置默认值:创建薪资结构时指定的 performance_factor 是默认系数,通常为 1.0
  • 生成月度记录时覆盖:生成薪资记录时可以指定当月的绩效系数,不指定则使用薪资结构中的默认值
  • 草稿状态修改:薪资记录在 draft 或 rejected 状态时,可以修改绩效系数,修改时自动重算实发金额

这意味着:大多数月份直接用默认系数 1.0 生成记录即可,只在有奖惩的月份手动指定不同系数。

操作流程

1. 员工入职 → 创建薪资结构(设定基本工资、岗位津贴、默认绩效系数)
2. 每月末 → 生成月度薪资记录(单个或批量,可指定当月绩效系数)
3. 修改记录 → 在草稿/驳回状态下修改绩效系数(自动重算)
4. 提交审核 → draft/rejected → pending
5. 审核通过 → pending → approved(或驳回 → rejected)
6. 确认发放 → approved → paid(仅 admin)
7. 调薪时 → 更新薪资结构(后续月份按新结构生成,历史记录不受影响)

批量生成

每月生成记录时支持三种方式:

  1. 指定员工 ID 列表POST /salaries/records/batchemployee_ids
  2. 按部门生成:传 dept_id,为该部门下有薪资结构的在职员工生成
  3. 全员生成:不传 employee_idsdept_id,为所有有薪资结构的在职员工生成

已存在的记录自动跳过。

测试

测试策略

三层各有独立测试,互不耦合:

测试方式依赖
repositoryPostgreSQL 测试数据库真实 DB
servicemock repository内存 mock
handlermock 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_HOSTlocalhost测试数据库主机
TEST_DB_PORT5432测试数据库端口
TEST_DB_USERpostgres测试数据库用户
TEST_DB_PASSWORDpostgres测试数据库密码
TEST_DB_NAMEhr_test测试数据库名
TEST_DB_SSLMODEdisableSSL 模式

如果没有可用的 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 实体为例:

  1. repository/position_repo_test.go — 用测试数据库测试 CRUD
  2. service/position_service_test.go — 用 mock repo 测试业务逻辑
  3. 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 触发:

  1. Lintgo vet ./...
  2. Test:启动 PostgreSQL + Redis service container,运行 go test -v ./...

测试使用 GitHub Actions 的 service container 功能,自动启动 PostgreSQL 和 Redis,测试完成后自动销毁,不需要外部数据库和缓存。

Push main 触发

代码合并到 main 后,在 lint + test 通过的基础上:

  1. 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{}

命名约定

  • 接口:大写开头,如 EmployeeRepositoryEmployeeService
  • 实现:小写开头 struct,如 employeeRepositoryemployeeService
  • 构造函数:NewXxx,如 NewEmployeeRepository
  • 文件名:按实体名,如 employee_repo.goemployee_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)