修复快捷工具无法显示

This commit is contained in:
hero920103 committed 2026-10-30 17:29:28 +08:00
1 parent 9898962836
commit 2e2b837a20
23 files changed
+1941 -38

No files matched your search

+675
View File
@@ -0,0 +1,675 @@
package controllers
import (
"encoding/json"
"strconv"
"strings"
"time"
"server/models"
"server/pkg/identity"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendJobRankController 职级管理接口。
//
// 职级(Job Rank)定义「职位等级」:每个部门可以各设一套职级阶梯,
// department_id 指向 yz_backend_organization.id,department_id = 0 表示
// 全公司通用职级,任何部门都能选用。
//
// 职级的使用链路:
//
// 员工 → 职位(yz_backend_position.rank_id) → 职级
// 薪酬方案(yz_backend_oa_compensation_schemes.rank_id) → 职级
//
// 所有查询与写入都强制带上 JWT 中的租户ID(tid),实现租户间隔离;
// 前端传入的 tid / tenant_id 一律忽略,避免越权读取其它租户的数据。
type BackendJobRankController struct {
beego.Controller
}
// jobRankDTO 职级返回结构。
type jobRankDTO struct {
ID uint64 `json:"id"`
Tid uint64 `json:"tid"`
TenantID uint64 `json:"tenant_id"`
RankCode string `json:"rank_code"`
RankName string `json:"rank_name"`
LevelNo int `json:"level_no"`
DepartmentID uint64 `json:"department_id"`
DepartmentName string `json:"department_name"`
SalaryMin float64 `json:"salary_min"`
SalaryMax float64 `json:"salary_max"`
DefaultRoleID uint64 `json:"default_role_id"`
Status int8 `json:"status"`
Sort int `json:"sort"`
Remark string `json:"remark"`
CreateTime string `json:"create_time"`
}
// jobRankPayload 职级新增/编辑入参。全部使用指针,区分「没传」与「传了零值」。
type jobRankPayload struct {
RankCode *string `json:"rank_code"`
RankName *string `json:"rank_name"`
LevelNo *int `json:"level_no"`
DepartmentID *uint64 `json:"department_id"`
SalaryMin *float64 `json:"salary_min"`
SalaryMax *float64 `json:"salary_max"`
DefaultRoleID *uint64 `json:"default_role_id"`
Status *int8 `json:"status"`
Sort *int `json:"sort"`
Remark *string `json:"remark"`
}
// ---------------------------------------------------------------------------
// 认证与响应
// ---------------------------------------------------------------------------
// claims 解析 Authorization 头中的 JWT,要求是租户端(backend)用户且带有效租户ID。
func (c *BackendJobRankController) claims() (*jwtutil.Claims, bool) {
claims, err := identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndBackend)
if err != nil || claims.TenantId <= 0 {
return nil, false
}
return claims, true
}
// pathUint64 读取路径参数中的无符号整数。
func (c *BackendJobRankController) pathUint64(name string) (uint64, bool) {
id, err := strconv.ParseUint(c.Ctx.Input.Param(name), 10, 64)
return id, err == nil && id > 0
}
// nowString 当前时间字符串(软删除用)。
func (c *BackendJobRankController) nowString() string {
return time.Now().Format("2006-01-02 15:04:05")
}
// tenantID 取当前登录租户ID,未通过认证时直接输出 401 并返回 false。
func (c *BackendJobRankController) tenantID() (uint64, bool) {
claims, ok := c.claims()
if !ok {
c.jsonError(401, "未登录或登录已过期")
return 0, false
}
return uint64(claims.TenantId), true
}
func (c *BackendJobRankController) jsonOK(data interface{}) {
resp := map[string]interface{}{"code": 200, "msg": "success"}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
func (c *BackendJobRankController) jsonError(code int, msg string) {
if code == 401 {
c.Ctx.Output.SetStatus(401)
}
c.Data["json"] = map[string]interface{}{"code": code, "msg": msg}
_ = c.ServeJSON()
}
// ---------------------------------------------------------------------------
// 查询基座
// ---------------------------------------------------------------------------
func (c *BackendJobRankController) rankQuery(tid uint64) orm.QuerySeter {
return models.Orm.QueryTable(new(models.BackendJobRank)).
Filter("tid", tid).
Filter("delete_time__isnull", true)
}
// jobRankPayloadOf 解析请求体(JSON 优先,兼容表单提交)。
func (c *BackendJobRankController) jobRankPayloadOf() (jobRankPayload, bool) {
var payload jobRankPayload
contentType := strings.ToLower(c.Ctx.Input.Header("Content-Type"))
if strings.Contains(contentType, "json") && len(c.Ctx.Input.RequestBody) > 0 {
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &payload); err != nil {
return payload, false
}
return payload, true
}
// 表单提交:逐字段回退
getString := func(key string) *string {
v := strings.TrimSpace(c.GetString(key))
if v == "" {
return nil
}
return &v
}
getFloat := func(key string) *float64 {
v := strings.TrimSpace(c.GetString(key))
if v == "" {
return nil
}
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil
}
return &f
}
getInt := func(key string) *int {
v := strings.TrimSpace(c.GetString(key))
if v == "" {
return nil
}
i, err := strconv.Atoi(v)
if err != nil {
return nil
}
return &i
}
getUint64 := func(key string) *uint64 {
v := strings.TrimSpace(c.GetString(key))
if v == "" {
return nil
}
i, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return nil
}
return &i
}
payload.RankCode = getString("rank_code")
payload.RankName = getString("rank_name")
payload.LevelNo = getInt("level_no")
payload.DepartmentID = getUint64("department_id")
payload.SalaryMin = getFloat("salary_min")
payload.SalaryMax = getFloat("salary_max")
payload.DefaultRoleID = getUint64("default_role_id")
payload.Sort = getInt("sort")
payload.Remark = getString("remark")
if v := getString("status"); v != nil {
status := int8(0)
if s, err := strconv.Atoi(*v); err == nil {
status = int8(s)
}
payload.Status = &status
}
return payload, true
}
// ---------------------------------------------------------------------------
// 职级 CRUD
// ---------------------------------------------------------------------------
// List 职级列表。
// GET /backend/jobrank/list
//
// 参数:
// keyword 职级名称 / 编码 模糊匹配
// department_id 适用部门筛选
// status 状态筛选(1 启用 / 0 禁用)
// scope=assign 返回「该部门可用」的职级:部门自身 + 全公司通用(department_id=0)。
// 职位、薪酬方案选择职级时用这个口径;不传则返回租户全部职级。
func (c *BackendJobRankController) List() {
tid, ok := c.tenantID()
if !ok {
return
}
cond := orm.NewCondition().And("tid", tid).And("delete_time__isnull", true)
departmentID, _ := c.GetUint64("department_id")
scope := strings.ToLower(strings.TrimSpace(c.GetString("scope")))
if scope == "assign" {
// 可选范围 = 本部门职级 + 全公司通用职级;
// 尚未指定部门时不按部门收窄,先把全部职级摆出来供选择。
if departmentID > 0 {
cond = cond.And("department_id__in", []uint64{0, departmentID})
}
} else if departmentID > 0 {
cond = cond.And("department_id", departmentID)
}
if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" {
cond = cond.AndCond(orm.NewCondition().
Or("rank_name__icontains", keyword).
Or("rank_code__icontains", keyword))
}
if raw := strings.TrimSpace(c.GetString("status")); raw != "" {
if status, err := strconv.Atoi(raw); err == nil {
cond = cond.And("status", int8(status))
}
}
var rows []models.BackendJobRank
if _, err := models.Orm.QueryTable(new(models.BackendJobRank)).
SetCond(cond).OrderBy("level_no", "sort", "id").All(&rows); err != nil {
c.jsonError(500, "查询职级失败: "+err.Error())
return
}
deptNames := jobRankDeptNameMap(tid)
list := make([]jobRankDTO, 0, len(rows))
for _, row := range rows {
list = append(list, jobRankDTOOf(row, deptNames))
}
c.jsonOK(list)
}
// Detail 职级详情。
// GET /backend/jobrank/detail/:id
func (c *BackendJobRankController) Detail() {
tid, ok := c.tenantID()
if !ok {
return
}
id, valid := c.pathUint64(":id")
if !valid {
c.jsonError(400, "无效ID")
return
}
var row models.BackendJobRank
if err := c.rankQuery(tid).Filter("id", id).One(&row); err != nil {
c.jsonError(404, "职级不存在")
return
}
c.jsonOK(jobRankDTOOf(row, jobRankDeptNameMap(tid)))
}
// Create 新增职级。
// POST /backend/jobrank/create
func (c *BackendJobRankController) Create() {
tid, ok := c.tenantID()
if !ok {
return
}
payload, ok := c.jobRankPayloadOf()
if !ok {
c.jsonError(400, "参数错误")
return
}
rankName := ""
if payload.RankName != nil {
rankName = strings.TrimSpace(*payload.RankName)
}
if rankName == "" {
c.jsonError(400, "职级名称不能为空")
return
}
rankCode := ""
if payload.RankCode != nil {
rankCode = strings.TrimSpace(*payload.RankCode)
}
if rankCode == "" {
c.jsonError(400, "职级编码不能为空")
return
}
count, err := c.rankQuery(tid).Filter("rank_code", rankCode).Count()
if err != nil {
c.jsonError(500, "校验职级编码失败: "+err.Error())
return
}
if count > 0 {
c.jsonError(400, "职级编码已存在")
return
}
departmentID := uint64(0)
if payload.DepartmentID != nil {
departmentID = *payload.DepartmentID
}
if departmentID > 0 && !jobRankOrgExists(tid, departmentID) {
c.jsonError(400, "适用部门不存在")
return
}
salaryMin, salaryMax := 0.0, 0.0
if payload.SalaryMin != nil {
salaryMin = *payload.SalaryMin
}
if payload.SalaryMax != nil {
salaryMax = *payload.SalaryMax
}
if salaryMin < 0 || salaryMax < 0 {
c.jsonError(400, "标准薪资不能为负数")
return
}
if salaryMax > 0 && salaryMax < salaryMin {
c.jsonError(400, "标准薪资上限不能小于下限")
return
}
row := models.BackendJobRank{
Tid: tid,
RankCode: rankCode,
RankName: rankName,
LevelNo: 0,
DepartmentID: departmentID,
SalaryMin: salaryMin,
SalaryMax: salaryMax,
DefaultRoleID: 0,
Status: 1,
Sort: 0,
}
if payload.LevelNo != nil {
row.LevelNo = *payload.LevelNo
}
if payload.DefaultRoleID != nil {
row.DefaultRoleID = *payload.DefaultRoleID
}
if payload.Status != nil {
row.Status = int8(1)
if *payload.Status == 0 {
row.Status = 0
}
}
if payload.Sort != nil {
row.Sort = *payload.Sort
}
if payload.Remark != nil {
row.Remark = strPtrIfNotEmpty(strings.TrimSpace(*payload.Remark))
}
id, err := models.Orm.Insert(&row)
if err != nil {
c.jsonError(500, "创建职级失败: "+err.Error())
return
}
c.jsonOK(map[string]interface{}{"id": id, "rank_code": rankCode})
}
// Update 更新职级。
// POST /backend/jobrank/update/:id
func (c *BackendJobRankController) Update() {
tid, ok := c.tenantID()
if !ok {
return
}
id, valid := c.pathUint64(":id")
if !valid {
c.jsonError(400, "无效ID")
return
}
if !c.rankQuery(tid).Filter("id", id).Exist() {
c.jsonError(404, "职级不存在")
return
}
payload, ok := c.jobRankPayloadOf()
if !ok {
c.jsonError(400, "参数错误")
return
}
update := orm.Params{}
if payload.RankCode != nil {
code := strings.TrimSpace(*payload.RankCode)
if code == "" {
c.jsonError(400, "职级编码不能为空")
return
}
count, err := c.rankQuery(tid).Filter("rank_code", code).Exclude("id", id).Count()
if err != nil {
c.jsonError(500, "校验职级编码失败: "+err.Error())
return
}
if count > 0 {
c.jsonError(400, "职级编码已存在")
return
}
update["rank_code"] = code
}
if payload.RankName != nil {
name := strings.TrimSpace(*payload.RankName)
if name == "" {
c.jsonError(400, "职级名称不能为空")
return
}
update["rank_name"] = name
}
if payload.LevelNo != nil {
update["level_no"] = *payload.LevelNo
}
if payload.DepartmentID != nil {
departmentID := *payload.DepartmentID
if departmentID > 0 && !jobRankOrgExists(tid, departmentID) {
c.jsonError(400, "适用部门不存在")
return
}
update["department_id"] = departmentID
}
if payload.SalaryMin != nil || payload.SalaryMax != nil {
// 薪资区间必须整体校验:只传一端时用另一端的现值补齐。
var row models.BackendJobRank
if err := c.rankQuery(tid).Filter("id", id).One(&row); err != nil {
c.jsonError(404, "职级不存在")
return
}
salaryMin, salaryMax := row.SalaryMin, row.SalaryMax
if payload.SalaryMin != nil {
salaryMin = *payload.SalaryMin
}
if payload.SalaryMax != nil {
salaryMax = *payload.SalaryMax
}
if salaryMin < 0 || salaryMax < 0 {
c.jsonError(400, "标准薪资不能为负数")
return
}
if salaryMax > 0 && salaryMax < salaryMin {
c.jsonError(400, "标准薪资上限不能小于下限")
return
}
update["salary_min"] = salaryMin
update["salary_max"] = salaryMax
}
if payload.DefaultRoleID != nil {
// 预留字段:本期不启用职级→角色权限绑定,仅落库保存。
update["default_role_id"] = *payload.DefaultRoleID
}
if payload.Status != nil {
status := int8(1)
if *payload.Status == 0 {
status = 0
}
update["status"] = status
}
if payload.Sort != nil {
update["sort"] = *payload.Sort
}
if payload.Remark != nil {
update["remark"] = nullableString(strings.TrimSpace(*payload.Remark))
}
if len(update) == 0 {
c.jsonError(400, "无更新字段")
return
}
if _, err := c.rankQuery(tid).Filter("id", id).Update(update); err != nil {
c.jsonError(500, "更新职级失败: "+err.Error())
return
}
c.jsonOK(nil)
}
// Delete 删除职级(软删除)。
// DELETE /backend/jobrank/delete/:id
func (c *BackendJobRankController) Delete() {
tid, ok := c.tenantID()
if !ok {
return
}
id, valid := c.pathUint64(":id")
if !valid {
c.jsonError(400, "无效ID")
return
}
// 还有职位引用该职级时不允许删除:否则职级消失后职位会指向空职级,
// 薪酬/权限按职级取数时拿不到任何标准值。
positionCount, err := models.Orm.QueryTable(new(models.BackendPosition)).
Filter("tid", tid).
Filter("rank_id", id).
Filter("delete_time__isnull", true).
Count()
if err != nil {
c.jsonError(500, "校验职级引用失败: "+err.Error())
return
}
if positionCount > 0 {
c.jsonError(400, "该职级已被 "+strconv.FormatInt(positionCount, 10)+" 个职位引用,请先调整职位的职级")
return
}
// 薪酬方案同样引用职级:删掉职级会让方案拿不到标准薪资区间,
// 历史方案也会指向空职级,因此一并拦下。
schemeCount, err := models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).
Filter("tid", tid).
Filter("rank_id", id).
Filter("is_deleted", 0).
Count()
if err != nil {
c.jsonError(500, "校验职级引用失败: "+err.Error())
return
}
if schemeCount > 0 {
c.jsonError(400, "该职级已被 "+strconv.FormatInt(schemeCount, 10)+" 个薪酬方案引用,请先调整方案的职级")
return
}
num, err := c.rankQuery(tid).Filter("id", id).
Update(orm.Params{"delete_time": c.nowString(), "status": int8(0)})
if err != nil {
c.jsonError(500, "删除职级失败: "+err.Error())
return
}
if num == 0 {
c.jsonError(404, "职级不存在")
return
}
c.jsonOK(nil)
}
// CheckCodeUnique 检查职级编码唯一性。
// GET /backend/jobrank/checkCodeUnique?rank_code=xx&exclude_id=1
func (c *BackendJobRankController) CheckCodeUnique() {
tid, ok := c.tenantID()
if !ok {
return
}
code := strings.TrimSpace(c.GetString("rank_code"))
if code == "" {
c.jsonError(400, "职级编码不能为空")
return
}
excludeID, _ := c.GetUint64("exclude_id")
qs := c.rankQuery(tid).Filter("rank_code", code)
if excludeID > 0 {
qs = qs.Exclude("id", excludeID)
}
count, err := qs.Count()
if err != nil {
c.jsonError(500, "检查编码唯一性失败: "+err.Error())
return
}
c.jsonOK(map[string]interface{}{"unique": count == 0, "message": uniqueMessage(count == 0, "编码")})
}
// ---------------------------------------------------------------------------
// 供其它模块复用的辅助函数
// ---------------------------------------------------------------------------
// jobRankDTOOf 把模型行转成 DTO,并补上部门名称。
func jobRankDTOOf(row models.BackendJobRank, deptNames map[uint64]string) jobRankDTO {
deptName := ""
if row.DepartmentID > 0 {
deptName = deptNames[row.DepartmentID]
}
if row.DepartmentID == 0 {
deptName = "全公司通用"
}
return jobRankDTO{
ID: row.ID,
Tid: row.Tid,
TenantID: row.Tid,
RankCode: row.RankCode,
RankName: row.RankName,
LevelNo: row.LevelNo,
DepartmentID: row.DepartmentID,
DepartmentName: deptName,
SalaryMin: row.SalaryMin,
SalaryMax: row.SalaryMax,
DefaultRoleID: row.DefaultRoleID,
Status: row.Status,
Sort: row.Sort,
Remark: derefString(row.Remark),
CreateTime: formatDateTime(&row.CreateTime),
}
}
// jobRankDeptNameMap 组织ID -> 名称,用于展示职级适用部门。
func jobRankDeptNameMap(tid uint64) map[uint64]string {
result := map[uint64]string{}
var rows []models.BackendOrganization
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
All(&rows, "ID", "OrgName"); err != nil {
return result
}
for _, row := range rows {
result[row.ID] = row.OrgName
}
return result
}
// jobRankOrgExists 判断部门是否存在(同租户)。
func jobRankOrgExists(tid, id uint64) bool {
if id == 0 {
return false
}
return models.Orm.QueryTable(new(models.BackendOrganization)).
Filter("tid", tid).
Filter("id", id).
Filter("delete_time__isnull", true).
Exist()
}
// JobRankMap 返回租户内的职级映射(ID -> 模型行),供职位、薪酬等模块复用。
func JobRankMap(tid uint64) map[uint64]models.BackendJobRank {
result := map[uint64]models.BackendJobRank{}
if tid == 0 {
return result
}
var rows []models.BackendJobRank
if _, err := models.Orm.QueryTable(new(models.BackendJobRank)).
Filter("tid", tid).
Filter("delete_time__isnull", true).
All(&rows); err != nil {
return result
}
for _, row := range rows {
result[row.ID] = row
}
return result
}
// ResolveEmployeeRankID 解析员工对应的职级:
// 员工上记录的职位是「职位名称」,先按 部门+职位名 找到职位,再取职位上的职级。
// 找不到职位或职位未设置职级时返回 0。
func ResolveEmployeeRankID(tid uint64, employee models.BackendEmployee) uint64 {
positionName := strings.TrimSpace(derefString(employee.Position))
if positionName == "" {
return 0
}
qs := models.Orm.QueryTable(new(models.BackendPosition)).
Filter("tid", tid).
Filter("position_name", positionName).
Filter("delete_time__isnull", true)
if departmentID, err := strconv.ParseUint(strings.TrimSpace(derefString(employee.Department)), 10, 64); err == nil && departmentID > 0 {
qs = qs.Filter("department_id", departmentID)
}
var row models.BackendPosition
if err := qs.OrderBy("id").One(&row); err != nil {
return 0
}
return row.RankID
}
+107 -7
View File
@@ -73,7 +73,11 @@ type compensationItemPayload struct {
}
type compensationSchemePayload struct {
EmployeeID uint64 `json:"employee_id"`
EmployeeID uint64 `json:"employee_id"`
// RankID 职级ID(yz_backend_job_rank.id),0=未指定。
// 传 0 时后端会按「员工 → 职位 → 职级」自动带出;带上后按职级的
// 标准薪资区间给出参考薪资(base_salary 未填时自动取下限)。
RankID uint64 `json:"rank_id"`
SchemeName string `json:"scheme_name"`
EffectiveDate string `json:"effective_date"`
ExpiryDate string `json:"expiry_date"`
@@ -299,7 +303,80 @@ func (c *BackendOaCompensationController) SchemeList() {
if rows == nil {
rows = []models.BackendOaCompensationScheme{}
}
c.compensationOK(rows)
c.compensationOK(compensationSchemeViews(uint64(claims.TenantId), rows))
}
// compensationSchemeView 给单个薪酬方案补上职级信息:
// 职级名称与该职级的标准薪资区间,前端据此展示「按职级带出的标准薪资」。
func compensationSchemeView(tid uint64, row models.BackendOaCompensationScheme) map[string]interface{} {
item := map[string]interface{}{}
if b, err := json.Marshal(row); err == nil {
_ = json.Unmarshal(b, &item)
}
item["rank_name"] = ""
item["rank_code"] = ""
item["rank_level_no"] = 0
item["rank_salary_min"] = float64(0)
item["rank_salary_max"] = float64(0)
if row.RankID > 0 {
if rank, ok := JobRankMap(tid)[row.RankID]; ok {
item["rank_name"] = rank.RankName
item["rank_code"] = rank.RankCode
item["rank_level_no"] = rank.LevelNo
item["rank_salary_min"] = rank.SalaryMin
item["rank_salary_max"] = rank.SalaryMax
}
}
return item
}
// compensationSchemeViews 给薪酬方案列表补上职级信息。
func compensationSchemeViews(tid uint64, rows []models.BackendOaCompensationScheme) []map[string]interface{} {
list := make([]map[string]interface{}, 0, len(rows))
for _, row := range rows {
list = append(list, compensationSchemeView(tid, row))
}
return list
}
// compensationResolveRank 解析薪酬方案使用的职级:
// - 显式传了 rank_id:必须属于当前租户,且适用部门要与员工部门一致
// (全公司通用职级不受限);
// - 没传:按「员工 → 职位 → 职级」自动带出。
// 第三个返回值为错误提示,空串表示解析成功。
func (c *BackendOaCompensationController) compensationResolveRank(
tid uint64, employee *models.BackendEmployee, rankID uint64,
) (uint64, models.BackendJobRank, string) {
if employee == nil {
return 0, models.BackendJobRank{}, "员工不存在或不属于当前租户"
}
ranks := JobRankMap(tid)
if rankID == 0 {
// 未指定:按员工所在职位带出职级
derived := ResolveEmployeeRankID(tid, *employee)
if derived == 0 {
return 0, models.BackendJobRank{}, ""
}
rank, ok := ranks[derived]
if !ok {
return 0, models.BackendJobRank{}, ""
}
return derived, rank, ""
}
rank, ok := ranks[rankID]
if !ok {
return 0, models.BackendJobRank{}, "职级不存在或不属于当前租户"
}
// 职级适用部门必须覆盖员工所在部门
if rank.DepartmentID != 0 {
departmentID, err := strconv.ParseUint(strings.TrimSpace(compensationString(employee.Department)), 10, 64)
if err == nil && departmentID > 0 && rank.DepartmentID != departmentID {
return 0, models.BackendJobRank{}, "职级「" + rank.RankName + "」不适用于该员工所在部门"
}
}
return rankID, rank, ""
}
// CreateScheme POST /backend/oa/compensation/schemes
@@ -342,9 +419,21 @@ func (c *BackendOaCompensationController) CreateScheme() {
if status != 0 {
status = 1
}
// 职级:未指定时按「员工 → 职位 → 职级」自动带出;
// 带出后 base_salary 留空则取职级标准薪资下限作为参考值。
rankID, rank, rankErr := c.compensationResolveRank(uint64(claims.TenantId), employee, payload.RankID)
if rankErr != "" {
c.compensationError(400, rankErr)
return
}
baseSalary := compensationMoney(payload.BaseSalary)
if baseSalary <= 0 && rank.SalaryMin > 0 {
baseSalary = compensationMoney(rank.SalaryMin)
}
row := &models.BackendOaCompensationScheme{
Tid: uint64(claims.TenantId), EmployeeID: payload.EmployeeID, SchemeName: strings.TrimSpace(payload.SchemeName),
EffectiveDate: date, ExpiryDate: expiry, BaseSalary: compensationMoney(payload.BaseSalary),
Tid: uint64(claims.TenantId), EmployeeID: payload.EmployeeID, RankID: rankID,
SchemeName: strings.TrimSpace(payload.SchemeName),
EffectiveDate: date, ExpiryDate: expiry, BaseSalary: baseSalary,
PostAllowance: compensationMoney(payload.PostAllowance), PerformanceSalary: compensationMoney(payload.PerformanceSalary),
TransportAllowance: compensationMoney(payload.TransportAllowance), MealAllowance: compensationMoney(payload.MealAllowance),
CommunicationAllowance: compensationMoney(payload.CommunicationAllowance), SocialInsuranceBase: compensationMoney(payload.SocialInsuranceBase),
@@ -355,7 +444,7 @@ func (c *BackendOaCompensationController) CreateScheme() {
c.compensationError(500, "保存薪酬方案失败")
return
}
c.compensationOK(row)
c.compensationOK(compensationSchemeView(uint64(claims.TenantId), *row))
}
// UpdateScheme POST /backend/oa/compensation/schemes/:id
@@ -385,10 +474,16 @@ func (c *BackendOaCompensationController) UpdateScheme() {
c.compensationError(400, "方案内容无效")
return
}
if _, ok = c.employee(uint64(claims.TenantId), payload.EmployeeID); !ok {
employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID)
if !ok {
c.compensationError(400, "员工不存在或不属于当前租户")
return
}
rankID, rank, rankErr := c.compensationResolveRank(uint64(claims.TenantId), employee, payload.RankID)
if rankErr != "" {
c.compensationError(400, rankErr)
return
}
var expiry *time.Time
if strings.TrimSpace(payload.ExpiryDate) != "" {
value, dateOK := validCompensationDate(payload.ExpiryDate)
@@ -400,7 +495,12 @@ func (c *BackendOaCompensationController) UpdateScheme() {
}
now := time.Now()
row.EmployeeID, row.SchemeName, row.EffectiveDate, row.ExpiryDate = payload.EmployeeID, strings.TrimSpace(payload.SchemeName), date, expiry
row.RankID = rankID
row.BaseSalary, row.PostAllowance, row.PerformanceSalary = compensationMoney(payload.BaseSalary), compensationMoney(payload.PostAllowance), compensationMoney(payload.PerformanceSalary)
// 职级带出:base_salary 未填且职级有标准薪资时取下限作为参考值
if row.BaseSalary <= 0 && rank.SalaryMin > 0 {
row.BaseSalary = compensationMoney(rank.SalaryMin)
}
row.TransportAllowance, row.MealAllowance, row.CommunicationAllowance = compensationMoney(payload.TransportAllowance), compensationMoney(payload.MealAllowance), compensationMoney(payload.CommunicationAllowance)
row.SocialInsuranceBase, row.HousingFundBase, row.SocialInsuranceRate, row.HousingFundRate = compensationMoney(payload.SocialInsuranceBase), compensationMoney(payload.HousingFundBase), payload.SocialInsuranceRate, payload.HousingFundRate
row.Status, row.Remark, row.UpdateTime = payload.Status, strings.TrimSpace(payload.Remark), &now
@@ -411,7 +511,7 @@ func (c *BackendOaCompensationController) UpdateScheme() {
c.compensationError(500, "更新薪酬方案失败")
return
}
c.compensationOK(row)
c.compensationOK(compensationSchemeView(uint64(claims.TenantId), row))
}
// DeleteScheme DELETE /backend/oa/compensation/schemes/:id
+47
View File
@@ -20,6 +20,11 @@ import (
// 前端传入的 tid / tenant_id 一律忽略,避免越权读取其它租户的数据。
type BackendOrganizationController struct {
beego.Controller
// jobRanks 职级映射缓存:beego 每请求新建一个控制器实例,
// 因此这里的缓存只在单次请求内有效,正好避免职位列表逐条查职级表(N+1)。
jobRanks map[uint64]models.BackendJobRank
jobRanksTid uint64
}
const orgSettingsCodePrefix = "backend_org_settings"
@@ -112,6 +117,9 @@ type positionDTO struct {
PositionCode string `json:"position_code"`
PositionName string `json:"position_name"`
PositionType int8 `json:"position_type"`
RankID uint64 `json:"rank_id"`
RankName string `json:"rank_name"`
RankLevelNo int `json:"rank_level_no"`
Status int8 `json:"status"`
Sort uint `json:"sort"`
Remark string `json:"remark"`
@@ -1298,12 +1306,27 @@ func (c *BackendOrganizationController) CreatePosition() {
sortVal, _ := c.getUintValue(body, "sort")
remark, _ := c.getStringValue(body, "remark")
// 职级:必须是本租户的职级,且适用部门要对得上(0=全公司通用始终可用)。
rankID, _ := c.getUint64Value(body, "rank_id")
if rankID > 0 {
rank, ok := c.jobRankMap(tid)[rankID]
if !ok {
c.jsonError(400, "职级不存在")
return
}
if rank.DepartmentID != 0 && departmentID > 0 && rank.DepartmentID != departmentID {
c.jsonError(400, "职级「"+rank.RankName+"」不属于所选部门")
return
}
}
row := models.BackendPosition{
Tid: tid,
DepartmentID: departmentID,
PositionCode: positionCode,
PositionName: positionName,
PositionType: int8(positionType),
RankID: rankID,
Status: 1,
Sort: sortVal,
Remark: strPtrIfNotEmpty(remark),
@@ -1379,6 +1402,30 @@ func (c *BackendOrganizationController) EditPosition() {
if v, has := c.getIntValue(body, "position_type"); has {
update["position_type"] = int8(v)
}
if v, has := c.getUint64Value(body, "rank_id"); has {
// 职级必须是本租户的职级;适用部门与职位所属部门不一致时拒绝。
if v > 0 {
rank, ok := c.jobRankMap(tid)[v]
if !ok {
c.jsonError(400, "职级不存在")
return
}
departmentID := uint64(0)
if cur, has := c.getUint64Value(body, "department_id"); has {
departmentID = cur
} else {
var cur models.BackendPosition
if err := c.positionQuery(tid).Filter("id", id).One(&cur); err == nil {
departmentID = cur.DepartmentID
}
}
if rank.DepartmentID != 0 && departmentID > 0 && rank.DepartmentID != departmentID {
c.jsonError(400, "职级「"+rank.RankName+"」不属于所选部门")
return
}
}
update["rank_id"] = v
}
if v, has := c.getIntValue(body, "status"); has {
update["status"] = int8(v)
}
+19 -1
View File
@@ -132,7 +132,7 @@ func (c *BackendOrganizationController) assembleEmployeeDTO(
func (c *BackendOrganizationController) positionDTO(tid uint64, row models.BackendPosition) positionDTO {
nameByID := c.orgNameMap(tid)
return positionDTO{
dto := positionDTO{
ID: row.ID,
Tid: row.Tid,
TenantID: row.Tid,
@@ -141,11 +141,29 @@ func (c *BackendOrganizationController) positionDTO(tid uint64, row models.Backe
PositionCode: row.PositionCode,
PositionName: row.PositionName,
PositionType: row.PositionType,
RankID: row.RankID,
Status: row.Status,
Sort: row.Sort,
Remark: derefString(row.Remark),
CreateTime: formatDateTime(&row.CreateTime),
}
// 职级名称:职位挂了职级才显示,未设置时留空由前端显示「-」。
if row.RankID > 0 {
if rank, ok := c.jobRankMap(tid)[row.RankID]; ok {
dto.RankName = rank.RankName
dto.RankLevelNo = rank.LevelNo
}
}
return dto
}
// jobRankMap 取租户内的职级映射,单次请求内只查一次。
func (c *BackendOrganizationController) jobRankMap(tid uint64) map[uint64]models.BackendJobRank {
if c.jobRanks == nil || c.jobRanksTid != tid {
c.jobRanks = JobRankMap(tid)
c.jobRanksTid = tid
}
return c.jobRanks
}
// ---------------------------------------------------------------------------