package controllers import ( "encoding/json" "io" "log" "math" "regexp" "strconv" "strings" "time" "server/models" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // BackendOaCompensationController OA 薪酬管理接口。 // 数据按租户隔离;工资金额使用薪资单快照,避免组织资料或历史方案变动影响已生成工资。 type BackendOaCompensationController struct { beego.Controller } func (c *BackendOaCompensationController) compensationClaims() (*jwtutil.Claims, error) { auth := c.Ctx.Request.Header.Get("Authorization") parts := strings.SplitN(auth, " ", 2) if auth == "" || len(parts) != 2 || parts[0] != "Bearer" { return nil, orm.ErrNoRows } claims, err := jwtutil.ParseToken(parts[1]) if err != nil || claims.UserType != "backend" { return nil, orm.ErrNoRows } return claims, nil } func (c *BackendOaCompensationController) compensationError(httpStatus int, msg string) { c.Ctx.Output.SetStatus(httpStatus) c.Data["json"] = map[string]interface{}{"code": httpStatus, "msg": msg} _ = c.ServeJSON() } func (c *BackendOaCompensationController) compensationOK(data interface{}) { c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data} _ = c.ServeJSON() } func compensationID(raw string) (uint64, bool) { id, err := strconv.ParseUint(raw, 10, 64) return id, err == nil && id > 0 } func compensationMoney(v float64) float64 { return math.Round(v*100) / 100 } func compensationString(v *string) string { if v == nil { return "" } return *v } var compensationMonthPattern = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`) type compensationItemPayload struct { ID uint64 `json:"id"` ItemName string `json:"item_name"` ItemType int8 `json:"item_type"` // 1 增项;2 扣项 Amount float64 `json:"amount"` Remark string `json:"remark"` SortOrder int `json:"sort_order"` } type compensationSchemePayload struct { EmployeeID uint64 `json:"employee_id"` SchemeName string `json:"scheme_name"` EffectiveDate string `json:"effective_date"` ExpiryDate string `json:"expiry_date"` BaseSalary float64 `json:"base_salary"` PostAllowance float64 `json:"post_allowance"` PerformanceSalary float64 `json:"performance_salary"` TransportAllowance float64 `json:"transport_allowance"` MealAllowance float64 `json:"meal_allowance"` CommunicationAllowance float64 `json:"communication_allowance"` SocialInsuranceBase float64 `json:"social_insurance_base"` HousingFundBase float64 `json:"housing_fund_base"` SocialInsuranceRate float64 `json:"social_insurance_rate"` HousingFundRate float64 `json:"housing_fund_rate"` Status int8 `json:"status"` Remark string `json:"remark"` } type compensationPayrollPayload struct { EmployeeID uint64 `json:"employee_id"` SchemeID *uint64 `json:"scheme_id"` PayrollMonth string `json:"payroll_month"` BaseSalary float64 `json:"base_salary"` PostAllowance float64 `json:"post_allowance"` PerformanceSalary float64 `json:"performance_salary"` TransportAllowance float64 `json:"transport_allowance"` MealAllowance float64 `json:"meal_allowance"` CommunicationAllowance float64 `json:"communication_allowance"` OvertimePay float64 `json:"overtime_pay"` Bonus float64 `json:"bonus"` OtherAddition float64 `json:"other_addition"` LeaveDeduction float64 `json:"leave_deduction"` LateDeduction float64 `json:"late_deduction"` OtherDeduction float64 `json:"other_deduction"` SocialInsurance float64 `json:"social_insurance"` HousingFund float64 `json:"housing_fund"` PersonalIncomeTax float64 `json:"personal_income_tax"` Remark string `json:"remark"` Items []compensationItemPayload `json:"items"` } func (c *BackendOaCompensationController) parseBody(target interface{}) bool { raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil || json.Unmarshal(raw, target) != nil { c.compensationError(400, "参数格式错误") return false } return true } func validCompensationDate(s string) (time.Time, bool) { s = strings.TrimSpace(s) value, err := time.ParseInLocation("2006-01-02", s, time.Local) return value, err == nil } func validateNonNegative(values ...float64) bool { for _, value := range values { if value < 0 || math.IsNaN(value) || math.IsInf(value, 0) { return false } } return true } func (c *BackendOaCompensationController) employee(tenantID uint64, employeeID uint64) (*models.BackendEmployee, bool) { var employee models.BackendEmployee err := models.Orm.QueryTable(new(models.BackendEmployee)). Filter("id", employeeID). Filter("tid", tenantID). Filter("delete_time__isnull", true). One(&employee) if err != nil { return nil, false } return &employee, true } func payrollMap(row models.BackendOaPayroll) map[string]interface{} { return map[string]interface{}{ "id": row.ID, "employee_id": row.EmployeeID, "scheme_id": row.SchemeID, "payroll_month": row.PayrollMonth, "employee_name": row.EmployeeName, "department": row.Department, "position": row.Position, "base_salary": row.BaseSalary, "post_allowance": row.PostAllowance, "performance_salary": row.PerformanceSalary, "transport_allowance": row.TransportAllowance, "meal_allowance": row.MealAllowance, "communication_allowance": row.CommunicationAllowance, "overtime_pay": row.OvertimePay, "bonus": row.Bonus, "other_addition": row.OtherAddition, "leave_deduction": row.LeaveDeduction, "late_deduction": row.LateDeduction, "other_deduction": row.OtherDeduction, "social_insurance": row.SocialInsurance, "housing_fund": row.HousingFund, "personal_income_tax": row.PersonalIncomeTax, "gross_salary": row.GrossSalary, "total_deduction": row.TotalDeduction, "net_salary": row.NetSalary, "status": row.Status, "confirmed_at": row.ConfirmedAt, "paid_at": row.PaidAt, "remark": row.Remark, "create_time": row.CreateTime, } } func (c *BackendOaCompensationController) fillPayroll(row *models.BackendOaPayroll, payload compensationPayrollPayload, employee *models.BackendEmployee) { row.SchemeID = payload.SchemeID row.BaseSalary = compensationMoney(payload.BaseSalary) row.PostAllowance = compensationMoney(payload.PostAllowance) row.PerformanceSalary = compensationMoney(payload.PerformanceSalary) row.TransportAllowance = compensationMoney(payload.TransportAllowance) row.MealAllowance = compensationMoney(payload.MealAllowance) row.CommunicationAllowance = compensationMoney(payload.CommunicationAllowance) row.OvertimePay = compensationMoney(payload.OvertimePay) row.Bonus = compensationMoney(payload.Bonus) row.OtherAddition = compensationMoney(payload.OtherAddition) row.LeaveDeduction = compensationMoney(payload.LeaveDeduction) row.LateDeduction = compensationMoney(payload.LateDeduction) row.OtherDeduction = compensationMoney(payload.OtherDeduction) row.SocialInsurance = compensationMoney(payload.SocialInsurance) row.HousingFund = compensationMoney(payload.HousingFund) row.PersonalIncomeTax = compensationMoney(payload.PersonalIncomeTax) row.Remark = strings.TrimSpace(payload.Remark) row.EmployeeName = employee.Name row.Department = compensationString(employee.Department) row.Position = compensationString(employee.Position) customAdd, customDeduct := 0.0, 0.0 for _, item := range payload.Items { if item.ItemType == 1 { customAdd += item.Amount } else if item.ItemType == 2 { customDeduct += item.Amount } } row.GrossSalary = compensationMoney(row.BaseSalary + row.PostAllowance + row.PerformanceSalary + row.TransportAllowance + row.MealAllowance + row.CommunicationAllowance + row.OvertimePay + row.Bonus + row.OtherAddition + customAdd) row.TotalDeduction = compensationMoney(row.LeaveDeduction + row.LateDeduction + row.OtherDeduction + row.SocialInsurance + row.HousingFund + row.PersonalIncomeTax + customDeduct) row.NetSalary = compensationMoney(row.GrossSalary - row.TotalDeduction) } func (c *BackendOaCompensationController) saveItems(tenantID, payrollID uint64, payloads []compensationItemPayload) error { _, err := models.Orm.QueryTable(new(models.BackendOaPayrollItem)). Filter("tid", tenantID).Filter("payroll_id", payrollID).Filter("is_deleted", 0). Update(orm.Params{"is_deleted": int8(1), "delete_time": time.Now()}) if err != nil { return err } for index, payload := range payloads { name := strings.TrimSpace(payload.ItemName) if name == "" || (payload.ItemType != 1 && payload.ItemType != 2) || !validateNonNegative(payload.Amount) { return orm.ErrArgs } item := &models.BackendOaPayrollItem{ Tid: tenantID, PayrollID: payrollID, ItemName: name, ItemType: payload.ItemType, Amount: compensationMoney(payload.Amount), Remark: strings.TrimSpace(payload.Remark), SortOrder: index, IsDeleted: 0, } if _, err = models.Orm.Insert(item); err != nil { return err } } return nil } // Employees GET /backend/oa/compensation/employees func (c *BackendOaCompensationController) Employees() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } keyword := strings.TrimSpace(c.GetString("keyword")) qs := models.Orm.QueryTable(new(models.BackendEmployee)).Filter("tid", claims.TenantId).Filter("delete_time__isnull", true) if keyword != "" { qs = qs.Filter("name__icontains", keyword) } var employees []models.BackendEmployee _, err = qs.OrderBy("name", "id").All(&employees) if err != nil && err != orm.ErrNoRows { c.compensationError(500, "员工查询失败") return } list := make([]map[string]interface{}, 0, len(employees)) for _, employee := range employees { list = append(list, map[string]interface{}{ "id": employee.ID, "name": employee.Name, "account": employee.Account, "department": compensationString(employee.Department), "position": compensationString(employee.Position), }) } c.compensationOK(list) } // SchemeList GET /backend/oa/compensation/schemes func (c *BackendOaCompensationController) SchemeList() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } employeeID, _ := c.GetInt64("employee_id", 0) qs := models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).Filter("tid", claims.TenantId).Filter("is_deleted", 0) if employeeID > 0 { qs = qs.Filter("employee_id", employeeID) } var rows []models.BackendOaCompensationScheme _, err = qs.OrderBy("-effective_date", "-id").All(&rows) if err != nil && err != orm.ErrNoRows { c.compensationError(500, "查询薪酬方案失败") return } if rows == nil { rows = []models.BackendOaCompensationScheme{} } c.compensationOK(rows) } // CreateScheme POST /backend/oa/compensation/schemes func (c *BackendOaCompensationController) CreateScheme() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } var payload compensationSchemePayload if !c.parseBody(&payload) { return } date, ok := validCompensationDate(payload.EffectiveDate) if !ok || payload.EmployeeID == 0 || strings.TrimSpace(payload.SchemeName) == "" { c.compensationError(400, "请填写员工、方案名称和生效日期") return } employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID) if !ok || employee == nil { c.compensationError(400, "员工不存在或不属于当前租户") return } if !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.SocialInsuranceBase, payload.HousingFundBase, payload.SocialInsuranceRate, payload.HousingFundRate) || payload.SocialInsuranceRate > 1 || payload.HousingFundRate > 1 { c.compensationError(400, "薪酬金额或比例无效") return } var expiry *time.Time if strings.TrimSpace(payload.ExpiryDate) != "" { value, valid := validCompensationDate(payload.ExpiryDate) if !valid || value.Before(date) { c.compensationError(400, "失效日期无效") return } expiry = &value } status := payload.Status if status != 0 { status = 1 } row := &models.BackendOaCompensationScheme{ Tid: uint64(claims.TenantId), EmployeeID: payload.EmployeeID, SchemeName: strings.TrimSpace(payload.SchemeName), EffectiveDate: date, ExpiryDate: expiry, BaseSalary: compensationMoney(payload.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), HousingFundBase: compensationMoney(payload.HousingFundBase), SocialInsuranceRate: payload.SocialInsuranceRate, HousingFundRate: payload.HousingFundRate, Status: status, Remark: strings.TrimSpace(payload.Remark), } if _, err = models.Orm.Insert(row); err != nil { c.compensationError(500, "保存薪酬方案失败") return } c.compensationOK(row) } // UpdateScheme POST /backend/oa/compensation/schemes/:id func (c *BackendOaCompensationController) UpdateScheme() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } id, valid := compensationID(c.Ctx.Input.Param(":id")) if !valid { c.compensationError(400, "无效方案ID") return } var payload compensationSchemePayload if !c.parseBody(&payload) { return } var row models.BackendOaCompensationScheme if models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil { c.compensationError(404, "薪酬方案不存在") return } // 保持更新逻辑与创建一致,先软删除旧值再由统一创建验证的字段约束赋值。 date, ok := validCompensationDate(payload.EffectiveDate) if !ok || payload.EmployeeID == 0 || strings.TrimSpace(payload.SchemeName) == "" || !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.SocialInsuranceBase, payload.HousingFundBase, payload.SocialInsuranceRate, payload.HousingFundRate) || payload.SocialInsuranceRate > 1 || payload.HousingFundRate > 1 { c.compensationError(400, "方案内容无效") return } if _, ok = c.employee(uint64(claims.TenantId), payload.EmployeeID); !ok { c.compensationError(400, "员工不存在或不属于当前租户") return } var expiry *time.Time if strings.TrimSpace(payload.ExpiryDate) != "" { value, dateOK := validCompensationDate(payload.ExpiryDate) if !dateOK || value.Before(date) { c.compensationError(400, "失效日期无效") return } expiry = &value } now := time.Now() row.EmployeeID, row.SchemeName, row.EffectiveDate, row.ExpiryDate = payload.EmployeeID, strings.TrimSpace(payload.SchemeName), date, expiry row.BaseSalary, row.PostAllowance, row.PerformanceSalary = compensationMoney(payload.BaseSalary), compensationMoney(payload.PostAllowance), compensationMoney(payload.PerformanceSalary) 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 if row.Status != 0 { row.Status = 1 } if _, err = models.Orm.Update(&row); err != nil { c.compensationError(500, "更新薪酬方案失败") return } c.compensationOK(row) } // DeleteScheme DELETE /backend/oa/compensation/schemes/:id func (c *BackendOaCompensationController) DeleteScheme() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } id, valid := compensationID(c.Ctx.Input.Param(":id")) if !valid { c.compensationError(400, "无效方案ID") return } now := time.Now() count, err := models.Orm.QueryTable(new(models.BackendOaCompensationScheme)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).Update(orm.Params{"is_deleted": int8(1), "delete_time": now, "update_time": now}) if err != nil || count == 0 { c.compensationError(404, "薪酬方案不存在") return } c.compensationOK(nil) } // List GET /backend/oa/compensation/payrolls func (c *BackendOaCompensationController) List() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 20 } month, keyword := strings.TrimSpace(c.GetString("payroll_month")), strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) qs := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("is_deleted", 0) if compensationMonthPattern.MatchString(month) { qs = qs.Filter("payroll_month", month) } if keyword != "" { qs = qs.Filter("employee_name__icontains", keyword) } if status == "0" || status == "1" || status == "2" || status == "3" { value, _ := strconv.Atoi(status) qs = qs.Filter("status", int8(value)) } total, _ := qs.Count() var rows []models.BackendOaPayroll _, err = qs.OrderBy("-payroll_month", "employee_name", "-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&rows) if err != nil && err != orm.ErrNoRows { log.Printf("薪资单查询失败: tid=%d month=%q keyword=%q status=%q err=%v", claims.TenantId, month, keyword, status, err) c.compensationError(500, "查询薪资单失败,请检查薪酬管理数据表是否已创建") return } list := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { list = append(list, payrollMap(row)) } c.compensationOK(map[string]interface{}{"list": list, "total": total}) } // Detail GET /backend/oa/compensation/payrolls/:id func (c *BackendOaCompensationController) Detail() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } id, valid := compensationID(c.Ctx.Input.Param(":id")) if !valid { c.compensationError(400, "无效薪资单ID") return } var row models.BackendOaPayroll if models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil { c.compensationError(404, "薪资单不存在") return } var items []models.BackendOaPayrollItem _, _ = models.Orm.QueryTable(new(models.BackendOaPayrollItem)).Filter("tid", claims.TenantId).Filter("payroll_id", id).Filter("is_deleted", 0).OrderBy("sort_order", "id").All(&items) data := payrollMap(row) data["items"] = items c.compensationOK(data) } // Create POST /backend/oa/compensation/payrolls func (c *BackendOaCompensationController) Create() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } var payload compensationPayrollPayload if !c.parseBody(&payload) { return } if payload.EmployeeID == 0 || !compensationMonthPattern.MatchString(strings.TrimSpace(payload.PayrollMonth)) || !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.OvertimePay, payload.Bonus, payload.OtherAddition, payload.LeaveDeduction, payload.LateDeduction, payload.OtherDeduction, payload.SocialInsurance, payload.HousingFund, payload.PersonalIncomeTax) { c.compensationError(400, "员工、工资月份或金额无效") return } employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID) if !ok { c.compensationError(400, "员工不存在或不属于当前租户") return } exists := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("employee_id", payload.EmployeeID).Filter("payroll_month", payload.PayrollMonth).Filter("is_deleted", 0).Exist() if exists { c.compensationError(409, "该员工本月薪资单已存在") return } row := &models.BackendOaPayroll{Tid: uint64(claims.TenantId), EmployeeID: payload.EmployeeID, PayrollMonth: strings.TrimSpace(payload.PayrollMonth), Status: 0} c.fillPayroll(row, payload, employee) if _, err = models.Orm.Insert(row); err != nil { c.compensationError(500, "创建薪资单失败") return } if err = c.saveItems(uint64(claims.TenantId), row.ID, payload.Items); err != nil { c.compensationError(400, "自定义薪资项无效") return } c.compensationOK(payrollMap(*row)) } // Update POST /backend/oa/compensation/payrolls/:id func (c *BackendOaCompensationController) Update() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } id, valid := compensationID(c.Ctx.Input.Param(":id")) if !valid { c.compensationError(400, "无效薪资单ID") return } var payload compensationPayrollPayload if !c.parseBody(&payload) { return } var row models.BackendOaPayroll if models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil { c.compensationError(404, "薪资单不存在") return } if row.Status != 0 { c.compensationError(400, "仅草稿薪资单可以编辑") return } if payload.EmployeeID == 0 || !compensationMonthPattern.MatchString(strings.TrimSpace(payload.PayrollMonth)) || !validateNonNegative(payload.BaseSalary, payload.PostAllowance, payload.PerformanceSalary, payload.TransportAllowance, payload.MealAllowance, payload.CommunicationAllowance, payload.OvertimePay, payload.Bonus, payload.OtherAddition, payload.LeaveDeduction, payload.LateDeduction, payload.OtherDeduction, payload.SocialInsurance, payload.HousingFund, payload.PersonalIncomeTax) { c.compensationError(400, "薪资单内容无效") return } employee, ok := c.employee(uint64(claims.TenantId), payload.EmployeeID) if !ok { c.compensationError(400, "员工不存在") return } exists := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("employee_id", payload.EmployeeID).Filter("payroll_month", payload.PayrollMonth).Filter("is_deleted", 0).Filter("id__ne", id).Exist() if exists { c.compensationError(409, "该员工本月薪资单已存在") return } row.EmployeeID, row.PayrollMonth = payload.EmployeeID, strings.TrimSpace(payload.PayrollMonth) c.fillPayroll(&row, payload, employee) now := time.Now() row.UpdateTime = &now if _, err = models.Orm.Update(&row); err != nil { c.compensationError(500, "更新薪资单失败") return } if err = c.saveItems(uint64(claims.TenantId), row.ID, payload.Items); err != nil { c.compensationError(400, "自定义薪资项无效") return } c.compensationOK(payrollMap(row)) } // Delete DELETE /backend/oa/compensation/payrolls/:id func (c *BackendOaCompensationController) Delete() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } id, valid := compensationID(c.Ctx.Input.Param(":id")) if !valid { c.compensationError(400, "无效薪资单ID") return } now := time.Now() count, err := models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("status", 0).Filter("is_deleted", 0).Update(orm.Params{"is_deleted": int8(1), "delete_time": now, "update_time": now}) if err != nil || count == 0 { c.compensationError(400, "薪资单不存在或当前状态不可删除") return } c.compensationOK(nil) } // UpdateStatus POST /backend/oa/compensation/payrolls/:id/status func (c *BackendOaCompensationController) UpdateStatus() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } id, valid := compensationID(c.Ctx.Input.Param(":id")) if !valid { c.compensationError(400, "无效薪资单ID") return } var payload struct { Status int8 `json:"status"` } if !c.parseBody(&payload) { return } if payload.Status < 0 || payload.Status > 3 { c.compensationError(400, "无效状态") return } var row models.BackendOaPayroll if models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0).One(&row) != nil { c.compensationError(404, "薪资单不存在") return } if row.Status == 2 && payload.Status != 2 { c.compensationError(400, "已发放薪资单不可修改状态") return } now := time.Now() row.Status, row.UpdateTime = payload.Status, &now if payload.Status == 1 { row.ConfirmedAt = &now } if payload.Status == 2 { row.PaidAt = &now } if _, err = models.Orm.Update(&row, "Status", "ConfirmedAt", "PaidAt", "UpdateTime"); err != nil { c.compensationError(500, "更新状态失败") return } c.compensationOK(payrollMap(row)) } // Dashboard GET /backend/oa/compensation/dashboard func (c *BackendOaCompensationController) Dashboard() { claims, err := c.compensationClaims() if err != nil { c.compensationError(401, "未登录或无权限") return } month := strings.TrimSpace(c.GetString("payroll_month")) if !compensationMonthPattern.MatchString(month) { month = time.Now().Format("2006-01") } var rows []models.BackendOaPayroll _, err = models.Orm.QueryTable(new(models.BackendOaPayroll)).Filter("tid", claims.TenantId).Filter("payroll_month", month).Filter("is_deleted", 0).All(&rows) if err != nil && err != orm.ErrNoRows { log.Printf("薪酬仪表盘统计失败: tid=%d month=%q err=%v", claims.TenantId, month, err) c.compensationError(500, "统计失败,请检查薪酬管理数据表是否已创建") return } totalNet, totalGross, draft, confirmed, paid := 0.0, 0.0, 0, 0, 0 for _, row := range rows { totalNet += row.NetSalary totalGross += row.GrossSalary switch row.Status { case 0: draft++ case 1: confirmed++ case 2: paid++ } } c.compensationOK(map[string]interface{}{"payroll_month": month, "employee_count": len(rows), "total_gross_salary": compensationMoney(totalGross), "total_net_salary": compensationMoney(totalNet), "draft_count": draft, "confirmed_count": confirmed, "paid_count": paid}) }