Files
2026-10-19 01:13:17 +08:00

268 lines
8.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"strconv"
"strings"
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"server/services"
"github.com/beego/beego/v2/client/orm"
)
// 移动端员工档案「全公司视角」接口(HR 使用)。
//
// 与 Web 端(/backend/oa/employeefile/*)的关系:
// - 数据口径完全一致:一人一档(yz_backend_employee_file)+ 教育/工作/合同/证照子记录,
// DTO 组装直接复用 Web 端 efAssembleDTOList,保证两端展示字段完全一致;
// - 只读:建档、改档、离职等写操作仍只在 Web 端提供;
// - 数据一律按 JWT 中的 TenantId 隔离,前端无法指定租户。
//
// 权限口径:租户角色的 rights 里勾选了「员工档案」菜单(或其祖先节点)即视为 HR,可见全公司档案;
// 租户管理员 / 未分配角色 / 角色未配置权限等既有「全权限」情形同样放行。
// 未放行的用户仍可查看本人档案(/api/oa/employee-file),前端据此展示不同视图。
// employeeFileMenuIDs 命中「员工档案」菜单的菜单ID集合(含其祖先节点)。
// 菜单表由租户自行维护,标题/路径可能被改写,因此按 路径关键字 + 标题关键字 双重匹配,
// 与移动端菜单跳转规则(uniapp/utils/nav.js 中的 employeefile 规则)保持同一识别口径。
func employeeFileMenuIDs() map[uint64]bool {
ids := map[uint64]bool{}
if models.Orm == nil {
return ids
}
var rows []models.SystemMenu
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).
Filter("status", 1).
Filter("delete_time__isnull", true).
All(&rows); err != nil {
return ids
}
pidOf := make(map[uint64]int64, len(rows))
hit := map[uint64]bool{}
for _, m := range rows {
pidOf[m.ID] = m.Pid
hay := strings.ToLower(m.Title + " " + derefString(m.Path) + " " + derefString(m.ComponentPath))
if strings.Contains(hay, "employeefile") || strings.Contains(hay, "employee-file") ||
strings.Contains(m.Title, "员工档案") || strings.Contains(m.Title, "人事档案") {
hit[m.ID] = true
}
}
if len(hit) == 0 {
return ids
}
// 角色权限勾到父目录即代表含全部下级,因此把命中菜单的祖先一并纳入判定。
for id := range hit {
cur := id
for i := 0; i < 32; i++ { // 限制深度,防御脏数据造成的环
if _, ok := pidOf[cur]; !ok {
break
}
ids[cur] = true
pid := pidOf[cur]
if pid <= 0 {
break
}
cur = uint64(pid)
}
}
return ids
}
// canViewAllEmployeeFiles 判断当前用户是否有权查看全公司员工档案(HR 视角)。
func canViewAllEmployeeFiles(claims *jwtutil.Claims) bool {
if claims == nil || claims.TenantId <= 0 {
return false
}
// 平台管理员
if claims.UserType == "platform" {
return true
}
// 租户管理员 / 未分配角色 / 角色未配置权限 —— 与 Web 端 isTenantAdmin 同一口径
if isTenantAdmin(claims.UserID, claims.TenantId) {
return true
}
var tu models.SystemTenantUser
if models.Orm.QueryTable(new(models.SystemTenantUser)).
Filter("uid", tenantUIDOf(claims)).
Filter("tid", claims.TenantId).
Filter("delete_time__isnull", true).One(&tu) != nil {
return false
}
if tu.GroupID == 0 {
return true
}
var role models.AdminRole
if models.Orm.QueryTable(new(models.AdminRole)).
Filter("id", tu.GroupID).
Filter("cid", 2).One(&role) != nil {
return false
}
if strings.TrimSpace(role.Name) == "租户管理员" {
return true
}
rights := ""
if role.Rights != nil {
rights = strings.TrimSpace(*role.Rights)
}
if rights == "" || rights == "[]" || rights == "null" {
return true
}
allowed := services.ParseMenuRights(rights)
if len(allowed) == 0 {
return true
}
for id := range employeeFileMenuIDs() {
if allowed[id] {
return true
}
}
return false
}
// tenantUIDOf 令牌 user_id 是认证中心 identity_id,换算成本租户的老 uid。
func tenantUIDOf(claims *jwtutil.Claims) uint64 {
return services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
}
// EmployeeFileAccess GET /api/oa/employee-file/access
// 返回当前用户是否具备 HR 视角(可见全公司档案),前端据此决定展示列表还是本人档案。
func (c *AppOaController) EmployeeFileAccess() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
c.oaOk(map[string]interface{}{"can_view_all": canViewAllEmployeeFiles(claims)})
}
// EmployeeFileList GET /api/oa/employee-file/list 全公司档案分页列表(需 HR 权限)。
// 支持 keyword(档案编号/身份证/员工姓名/账号/手机)、status(在职状态)、org_id(部门,含下级)。
func (c *AppOaController) EmployeeFileList() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
if !canViewAllEmployeeFiles(claims) {
c.oaErr(403, 403, "您没有查看全部员工档案的权限")
return
}
tid := claims.TenantId
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
keyword := strings.TrimSpace(c.GetString("keyword"))
statusRaw := strings.TrimSpace(c.GetString("status"))
orgID, _ := c.GetUint64("org_id")
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
web := &BackendEmployeeFileController{}
qs := web.efQuery(tid)
if orgID > 0 {
empIDs := web.efEmployeeIDsInOrgs(tid, web.efSubtreeOrgIDs(tid, orgID))
if len(empIDs) == 0 {
c.oaOk(map[string]interface{}{"list": []employeeFileDTO{}, "total": 0, "page": page, "pageSize": pageSize})
return
}
qs = qs.Filter("employee_id__in", empIDs)
}
if statusRaw != "" {
status, convErr := strconv.Atoi(statusRaw)
if convErr != nil || status < 1 || status > 3 {
c.oaErr(400, 400, "在职状态参数无效")
return
}
qs = qs.Filter("employment_status", int8(status))
}
if keyword != "" {
// 关键词同时命中档案自身字段(编号/身份证)与员工字段(姓名/账号/手机)
empIDs := web.efEmployeeIDsByKeyword(tid, keyword)
cond := orm.NewCondition().
Or("file_no__icontains", keyword).
Or("id_card__icontains", keyword)
if len(empIDs) > 0 {
cond = cond.Or("employee_id__in", empIDs)
}
qs = qs.SetCond(orm.NewCondition().AndCond(cond))
}
total, _ := qs.Count()
var files []models.BackendEmployeeFile
if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&files); err != nil && err != orm.ErrNoRows {
c.oaErr(500, 500, "查询失败")
return
}
c.oaOk(map[string]interface{}{
"list": web.efAssembleDTOList(tid, files),
"total": total,
"page": page,
"pageSize": pageSize,
})
}
// EmployeeFileDetail GET /api/oa/employee-file/detail/:id 员工档案详情(含子记录)。
//
// 两种访问路径:
// - HR 视角:可查看任意员工的档案(租户内隔离);
// - 普通员工:仅可查看本人档案,看他人档案返回 403。
func (c *AppOaController) EmployeeFileDetail() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
tid := claims.TenantId
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if id == 0 {
c.oaErr(400, 400, "无效的档案ID")
return
}
web := &BackendEmployeeFileController{}
var file models.BackendEmployeeFile
if web.efQuery(tid).Filter("id", id).One(&file) != nil {
c.oaErr(404, 404, "档案不存在")
return
}
if !canViewAllEmployeeFiles(claims) {
emp, ok := c.oaMyEmployee(claims)
if !ok || uint64(emp.ID) != file.EmployeeID {
c.oaErr(403, 403, "您没有查看他人档案的权限")
return
}
}
var records []models.BackendEmployeeFileRecord
if _, err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
Filter("tid", tid).
Filter("is_deleted", 0).
Filter("employee_id", file.EmployeeID).
OrderBy("sort", "id").
All(&records); err != nil && err != orm.ErrNoRows {
c.oaErr(500, 500, "查询档案记录失败")
return
}
if records == nil {
records = []models.BackendEmployeeFileRecord{}
}
dtos := web.efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
c.oaOk(map[string]interface{}{"file": dtos[0], "records": records})
}