增加档案功能
This commit is contained in:
@@ -0,0 +1,679 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user