diff --git a/backend/src/api/employeeFile.js b/backend/src/api/employeeFile.js index 296ac59..0532358 100644 --- a/backend/src/api/employeeFile.js +++ b/backend/src/api/employeeFile.js @@ -37,6 +37,11 @@ export function updateEmployeeFile(id, data) { return request({ url: `${BASE}/update/${id}`, method: 'post', data }); } +/** 更新已建档员工的学历照片、身份证正反面 */ +export function updateEmployeeFileCertificates(id, data) { + return request({ url: `${BASE}/certificates/${id}`, method: 'post', data }); +} + /** 删除档案(软删除) */ export function deleteEmployeeFile(id) { return request({ url: `${BASE}/delete/${id}`, method: 'delete' }); diff --git a/backend/src/api/oaCompensation.js b/backend/src/api/oaCompensation.js new file mode 100644 index 0000000..73cdacd --- /dev/null +++ b/backend/src/api/oaCompensation.js @@ -0,0 +1,96 @@ +import request from '@/utils/request' + +// OA 薪酬管理 + +export function getCompensationEmployees(params) { + return request({ + url: '/backend/oa/compensation/employees', + method: 'get', + params + }) +} + +export function getCompensationDashboard(params) { + return request({ + url: '/backend/oa/compensation/dashboard', + method: 'get', + params + }) +} + +export function getCompensationSchemes(params) { + return request({ + url: '/backend/oa/compensation/schemes', + method: 'get', + params + }) +} + +export function createCompensationScheme(data) { + return request({ + url: '/backend/oa/compensation/schemes', + method: 'post', + data + }) +} + +export function updateCompensationScheme(id, data) { + return request({ + url: `/backend/oa/compensation/schemes/${id}`, + method: 'post', + data + }) +} + +export function deleteCompensationScheme(id) { + return request({ + url: `/backend/oa/compensation/schemes/${id}`, + method: 'delete' + }) +} + +export function getPayrollList(params) { + return request({ + url: '/backend/oa/compensation/payrolls', + method: 'get', + params + }) +} + +export function getPayrollDetail(id) { + return request({ + url: `/backend/oa/compensation/payrolls/${id}`, + method: 'get' + }) +} + +export function createPayroll(data) { + return request({ + url: '/backend/oa/compensation/payrolls', + method: 'post', + data + }) +} + +export function updatePayroll(id, data) { + return request({ + url: `/backend/oa/compensation/payrolls/${id}`, + method: 'post', + data + }) +} + +export function deletePayroll(id) { + return request({ + url: `/backend/oa/compensation/payrolls/${id}`, + method: 'delete' + }) +} + +export function updatePayrollStatus(id, status) { + return request({ + url: `/backend/oa/compensation/payrolls/${id}/status`, + method: 'post', + data: { status } + }) +} diff --git a/backend/src/router/index.js b/backend/src/router/index.js index 4408225..1e23ac2 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -69,65 +69,6 @@ const staticMainChildren = [ component: () => import("@/views/user/userProfile.vue"), meta: { requiresAuth: true, title: "用户中心" } }, - // 组织架构(组织 / 员工 / 职位) - // 进销存(erp)与办公自动化(oa)各有独立页面,但读写同一份组织数据。 - { - path: "/apps/erp/organization", - name: "ErpOrganization", - component: () => import("@/views/apps/erp/organization/index.vue"), - meta: { requiresAuth: true, title: "组织架构", modulePath: "/apps/erp" } - }, - { - path: "/apps/erp/employee", - name: "ErpEmployee", - component: () => import("@/views/apps/erp/employee/index.vue"), - meta: { requiresAuth: true, title: "员工管理", modulePath: "/apps/erp" } - }, - { - path: "/apps/erp/position", - name: "ErpPosition", - component: () => import("@/views/apps/erp/position/index.vue"), - meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/erp" } - }, - { - path: "/apps/oa/organization", - name: "Organization", - component: () => import("@/views/apps/organization/components/OrganizationPage.vue"), - props: { module: "oa" }, - meta: { requiresAuth: true, title: "组织架构", modulePath: "/apps/oa" } - }, - { - path: "/apps/oa/employee", - name: "Employee", - component: () => import("@/views/apps/organization/components/EmployeePage.vue"), - props: { module: "oa" }, - meta: { requiresAuth: true, title: "人员管理", modulePath: "/apps/oa" } - }, - { - path: "/apps/oa/position", - name: "Position", - component: () => import("@/views/apps/organization/components/PositionPage.vue"), - props: { module: "oa" }, - meta: { requiresAuth: true, title: "职位管理", modulePath: "/apps/oa" } - }, - { - path: "/apps/oa/employeefile", - name: "EmployeeFile", - component: () => import("@/views/apps/oa/employeefile/index.vue"), - meta: { requiresAuth: true, title: "员工档案", modulePath: "/apps/oa" } - }, - { - path: "/apps/oa/schedule", - name: "OaSchedule", - component: () => import("@/views/apps/oa/schedule/index.vue"), - meta: { requiresAuth: true, title: "日程管理", modulePath: "/apps/oa" } - }, - { - path: "/tools/passwordStore", - name: "BackendPasswordStore", - component: () => import("@/views/tools/passwordStore/index.vue"), - meta: { requiresAuth: true, title: "密码存储" } - }, // 兼容拼写错误的路径重定向 { path: "/apps/erp/dashborad", diff --git a/backend/src/views/apps/oa/compensation/index.vue b/backend/src/views/apps/oa/compensation/index.vue new file mode 100644 index 0000000..ae978d1 --- /dev/null +++ b/backend/src/views/apps/oa/compensation/index.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue b/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue index a8c0019..0a5ff68 100644 --- a/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue +++ b/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue @@ -72,44 +72,13 @@ {{ file.remark || '-' }} -
证照资料
-
-
-
- - - -
- - PDF 附件 -
-
-
未上传
-
{{ cert.label }}
-
-
@@ -122,7 +91,7 @@ 新增{{ conf.label }}
- + @@ -138,20 +107,47 @@ {{ contractStatus(row).text }} + + + - - diff --git a/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue b/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue index 91a4883..0dd52c1 100644 --- a/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue +++ b/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue @@ -14,7 +14,14 @@ + - +
- 证照 + 附件预览 +
+ + 已上传{{ attachmentLabel }} +
- 上传证照 + 上传{{ attachmentLabel }}
- - 移除附件 - -
支持图片 / PDF,大小不超过 10MB
+
+ 查看 / 下载 + 移除附件 +
+
{{ uploadTip }}
@@ -85,7 +96,7 @@ diff --git a/backend/src/views/basicSettings/organization/index.vue b/backend/src/views/basicSettings/organization/index.vue new file mode 100644 index 0000000..fa4b451 --- /dev/null +++ b/backend/src/views/basicSettings/organization/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/backend/src/views/basicSettings/position/index.vue b/backend/src/views/basicSettings/position/index.vue new file mode 100644 index 0000000..f54cae9 --- /dev/null +++ b/backend/src/views/basicSettings/position/index.vue @@ -0,0 +1,7 @@ + + + diff --git a/go/controllers/backend_employee_file.go b/go/controllers/backend_employee_file.go index 38e13f8..ae91460 100644 --- a/go/controllers/backend_employee_file.go +++ b/go/controllers/backend_employee_file.go @@ -3,6 +3,7 @@ package controllers import ( "encoding/json" "io" + "log" "regexp" "strconv" "strings" @@ -68,15 +69,25 @@ func efIsValidDate(s string) bool { return err == nil } -// efNormalizeDate 将 ORM 读出的日期值统一为 "YYYY-MM-DD"。 -// DSN 开启 parseTime=True 后 DATE 列会被驱动解析为 time.Time, -// 赋给 string 字段后是 Go 默认格式,取前 10 位(与 OA 日程一致)。 -func efNormalizeDate(s string) string { - s = strings.TrimSpace(s) - if len(s) >= 10 { - return s[:10] +// efNormalizeDate 将可空 DATE 列统一输出为 "YYYY-MM-DD"。 +func efNormalizeDate(value *time.Time) string { + if value == nil { + return "" } - return s + return value.Format("2006-01-02") +} + +// efDatePointer 将前端可选日期转为可空 DATE 值;空字符串明确保存为 SQL NULL。 +func efDatePointer(value string) *time.Time { + value = strings.TrimSpace(value) + if value == "" { + return nil + } + date, err := time.ParseInLocation("2006-01-02", value, time.Local) + if err != nil { + return nil + } + return &date } // efQuery 档案查询基座:强制附加租户过滤与未删除条件。 @@ -204,6 +215,48 @@ type employeeFileDTO struct { Nation string `json:"nation"` HomeAddress string `json:"home_address"` AccountStatus int8 `json:"account_status"` + HireDate string `json:"hire_date"` + RegularDate string `json:"regular_date"` + LeaveDate string `json:"leave_date"` + Completeness int `json:"completeness"` +} + +// efFileCompleteness 计算档案资料完整度。仅统计应由档案模块维护的有效字段: +// 离职日期仅在离职时要求,转正日期仅在正式员工时要求,备注为可选项不计入。 +func efFileCompleteness(file models.BackendEmployeeFile) int { + items := []bool{ + strings.TrimSpace(file.IDCard) != "", + strings.TrimSpace(file.PoliticalStatus) != "", + file.MaritalStatus > 0, + strings.TrimSpace(file.NativePlace) != "", + strings.TrimSpace(file.HouseholdAddress) != "", + strings.TrimSpace(file.CurrentAddress) != "", + strings.TrimSpace(file.WorkEmail) != "", + file.HireDate != nil, + strings.TrimSpace(file.EmergencyContact) != "", + strings.TrimSpace(file.EmergencyPhone) != "", + strings.TrimSpace(file.EmergencyRelation) != "", + strings.TrimSpace(file.EducationPhoto) != "", + strings.TrimSpace(file.IDCardFront) != "", + strings.TrimSpace(file.IDCardBack) != "", + } + if file.EmploymentStatus == 2 { + items = append(items, file.RegularDate != nil) + } + if file.EmploymentStatus == 3 { + items = append(items, file.LeaveDate != nil) + } + + filled := 0 + for _, item := range items { + if item { + filled++ + } + } + if len(items) == 0 { + return 0 + } + return filled * 100 / len(items) } // efAssembleDTOList 档案列表批量组装员工展示信息。 @@ -230,6 +283,7 @@ func (c *BackendEmployeeFileController) efAssembleDTOList(tid int, files []model for _, f := range files { dto := employeeFileDTO{BackendEmployeeFile: f} + dto.Completeness = efFileCompleteness(f) dto.HireDate = efNormalizeDate(f.HireDate) dto.RegularDate = efNormalizeDate(f.RegularDate) dto.LeaveDate = efNormalizeDate(f.LeaveDate) @@ -386,11 +440,6 @@ func (c *BackendEmployeeFileController) Detail() { if records == nil { records = []models.BackendEmployeeFileRecord{} } - for i := range records { - records[i].StartDate = efNormalizeDate(records[i].StartDate) - records[i].EndDate = efNormalizeDate(records[i].EndDate) - } - list := c.efAssembleDTOList(tid, []models.BackendEmployeeFile{file}) c.efOk(map[string]interface{}{"file": list[0], "records": records}) } @@ -507,6 +556,11 @@ func (c *BackendEmployeeFileController) Create() { fileNo = "EF" + time.Now().Format("20060102150405") } + // 证照资料只能在建档完成后,于档案详情中维护;创建接口不接受附件。 + payload.IDCardFront = "" + payload.IDCardBack = "" + payload.EducationPhoto = "" + now := time.Now() item := &models.BackendEmployeeFile{ Tid: tid, @@ -525,9 +579,9 @@ func (c *BackendEmployeeFileController) Create() { EmergencyPhone: payload.EmergencyPhone, EmergencyRelation: payload.EmergencyRelation, WorkEmail: payload.WorkEmail, - HireDate: strings.TrimSpace(payload.HireDate), - RegularDate: strings.TrimSpace(payload.RegularDate), - LeaveDate: strings.TrimSpace(payload.LeaveDate), + HireDate: efDatePointer(payload.HireDate), + RegularDate: efDatePointer(payload.RegularDate), + LeaveDate: efDatePointer(payload.LeaveDate), EmploymentStatus: payload.EmploymentStatus, Remark: payload.Remark, IsDeleted: 0, @@ -535,7 +589,8 @@ func (c *BackendEmployeeFileController) Create() { UpdateTime: &now, } if _, err := models.Orm.Insert(item); err != nil { - c.efErr(500, 500, "保存失败") + log.Printf("员工档案创建失败: tid=%d employee_id=%d err=%v", tid, payload.EmployeeID, err) + c.efErr(500, 500, "保存失败,请检查员工档案表字段及日期数据") return } @@ -572,9 +627,7 @@ func (c *BackendEmployeeFileController) Update() { now := time.Now() item.FileNo = payload.FileNo item.IDCard = payload.IDCard - item.IDCardFront = payload.IDCardFront - item.IDCardBack = payload.IDCardBack - item.EducationPhoto = payload.EducationPhoto + // 证照字段由 UpdateCertificates 独立维护,基础资料保存不得覆盖已有附件。 item.PoliticalStatus = payload.PoliticalStatus item.MaritalStatus = payload.MaritalStatus item.NativePlace = payload.NativePlace @@ -584,20 +637,81 @@ func (c *BackendEmployeeFileController) Update() { item.EmergencyPhone = payload.EmergencyPhone item.EmergencyRelation = payload.EmergencyRelation item.WorkEmail = payload.WorkEmail - item.HireDate = strings.TrimSpace(payload.HireDate) - item.RegularDate = strings.TrimSpace(payload.RegularDate) - item.LeaveDate = strings.TrimSpace(payload.LeaveDate) + item.HireDate = efDatePointer(payload.HireDate) + item.RegularDate = efDatePointer(payload.RegularDate) + item.LeaveDate = efDatePointer(payload.LeaveDate) item.EmploymentStatus = payload.EmploymentStatus item.Remark = payload.Remark item.UpdateTime = &now if _, err := models.Orm.Update(&item, - "FileNo", "IDCard", "IDCardFront", "IDCardBack", "EducationPhoto", - "PoliticalStatus", "MaritalStatus", "NativePlace", + "FileNo", "IDCard", "PoliticalStatus", "MaritalStatus", "NativePlace", "HouseholdAddress", "CurrentAddress", "EmergencyContact", "EmergencyPhone", "EmergencyRelation", "WorkEmail", "HireDate", "RegularDate", "LeaveDate", "EmploymentStatus", "Remark", "UpdateTime"); err != nil { - c.efErr(500, 500, "保存失败") + log.Printf("员工档案更新失败: tid=%d file_id=%d err=%v", tid, id, err) + c.efErr(500, 500, "保存失败,请检查员工档案表字段及日期数据") + return + } + + c.efOk(item) +} + +// employeeFileCertificatesPayload 证照资料单独更新请求体。 +// 该接口仅在档案详情中使用,避免上传附件时覆盖基础资料。 +type employeeFileCertificatesPayload struct { + IDCardFront string `json:"id_card_front"` + IDCardBack string `json:"id_card_back"` + EducationPhoto string `json:"education_photo"` +} + +// UpdateCertificates POST /backend/oa/employeefile/certificates/:id +// 仅更新学历照片、身份证正反面;档案必须已创建,不能通过建档接口上传。 +func (c *BackendEmployeeFileController) UpdateCertificates() { + claims, err := c.efClaims() + if err != nil { + c.efErr(401, 401, "未登录或无权限") + return + } + tid := claims.TenantId + + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.efErr(400, 400, "无效ID") + return + } + + var payload employeeFileCertificatesPayload + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil || json.Unmarshal(raw, &payload) != nil { + c.efErr(400, 400, "参数错误") + return + } + for label, url := range map[string]string{ + "身份证正面": payload.IDCardFront, + "身份证反面": payload.IDCardBack, + "学历照片": payload.EducationPhoto, + } { + if len(url) > 500 { + c.efErr(400, 400, label+"地址过长") + return + } + } + + var item models.BackendEmployeeFile + if err := c.efQuery(tid).Filter("id", id).One(&item); err != nil { + c.efErr(404, 404, "档案不存在") + return + } + + now := time.Now() + item.IDCardFront = strings.TrimSpace(payload.IDCardFront) + item.IDCardBack = strings.TrimSpace(payload.IDCardBack) + item.EducationPhoto = strings.TrimSpace(payload.EducationPhoto) + item.UpdateTime = &now + if _, err := models.Orm.Update(&item, + "IDCardFront", "IDCardBack", "EducationPhoto", "UpdateTime"); err != nil { + c.efErr(500, 500, "证照保存失败") return } @@ -743,8 +857,8 @@ func (c *BackendEmployeeFileController) CreateRecord() { Title: payload.Title, SubTitle: payload.SubTitle, Extra: payload.Extra, - StartDate: payload.StartDate, - EndDate: payload.EndDate, + StartDate: efDatePointer(payload.StartDate), + EndDate: efDatePointer(payload.EndDate), Description: payload.Description, AttachmentURL: payload.AttachmentURL, Sort: payload.Sort, @@ -795,8 +909,8 @@ func (c *BackendEmployeeFileController) UpdateRecord() { item.Title = payload.Title item.SubTitle = payload.SubTitle item.Extra = payload.Extra - item.StartDate = payload.StartDate - item.EndDate = payload.EndDate + item.StartDate = efDatePointer(payload.StartDate) + item.EndDate = efDatePointer(payload.EndDate) item.Description = payload.Description item.AttachmentURL = payload.AttachmentURL item.Sort = payload.Sort diff --git a/go/controllers/backend_oa_compensation.go b/go/controllers/backend_oa_compensation.go new file mode 100644 index 0000000..bb9c650 --- /dev/null +++ b/go/controllers/backend_oa_compensation.go @@ -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}) +} diff --git a/go/models/backend_employee_file.go b/go/models/backend_employee_file.go index c958d63..a7a2bf2 100644 --- a/go/models/backend_employee_file.go +++ b/go/models/backend_employee_file.go @@ -24,9 +24,9 @@ type BackendEmployeeFile struct { EmergencyPhone string `orm:"column(emergency_phone);size(20);default()" json:"emergency_phone"` EmergencyRelation string `orm:"column(emergency_relation);size(30);default()" json:"emergency_relation"` WorkEmail string `orm:"column(work_email);size(100);default()" json:"work_email"` - HireDate string `orm:"column(hire_date);type(date);null" json:"hire_date"` - RegularDate string `orm:"column(regular_date);type(date);null" json:"regular_date"` - LeaveDate string `orm:"column(leave_date);type(date);null" json:"leave_date"` + HireDate *time.Time `orm:"column(hire_date);type(date);null" json:"hire_date"` + RegularDate *time.Time `orm:"column(regular_date);type(date);null" json:"regular_date"` + LeaveDate *time.Time `orm:"column(leave_date);type(date);null" json:"leave_date"` EmploymentStatus int8 `orm:"column(employment_status);default(2)" json:"employment_status"` Remark string `orm:"column(remark);type(text);null" json:"remark"` IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` @@ -51,8 +51,8 @@ type BackendEmployeeFileRecord struct { Title string `orm:"column(title);size(100);default()" json:"title"` SubTitle string `orm:"column(sub_title);size(100);default()" json:"sub_title"` Extra string `orm:"column(extra);size(100);default()" json:"extra"` - StartDate string `orm:"column(start_date);type(date);null" json:"start_date"` - EndDate string `orm:"column(end_date);type(date);null" json:"end_date"` + StartDate *time.Time `orm:"column(start_date);type(date);null" json:"start_date"` + EndDate *time.Time `orm:"column(end_date);type(date);null" json:"end_date"` Description string `orm:"column(description);type(text);null" json:"description"` AttachmentURL string `orm:"column(attachment_url);size(500);default()" json:"attachment_url"` Sort int `orm:"column(sort);default(0)" json:"sort"` diff --git a/go/models/backend_oa_compensation.go b/go/models/backend_oa_compensation.go new file mode 100644 index 0000000..37938d3 --- /dev/null +++ b/go/models/backend_oa_compensation.go @@ -0,0 +1,97 @@ +package models + +import "time" + +// BackendOaCompensationScheme 员工薪酬方案表 yz_backend_oa_compensation_schemes。 +// 一个员工可保留多份历史方案,生效中的方案由 effective_date 与 status 共同决定。 +type BackendOaCompensationScheme struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"` + SchemeName string `orm:"column(scheme_name);size(100)" json:"scheme_name"` + EffectiveDate time.Time `orm:"column(effective_date);type(date)" json:"effective_date"` + ExpiryDate *time.Time `orm:"column(expiry_date);null;type(date)" json:"expiry_date"` + BaseSalary float64 `orm:"column(base_salary);type(decimal);digits(12);decimals(2);default(0)" json:"base_salary"` + PostAllowance float64 `orm:"column(post_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"post_allowance"` + PerformanceSalary float64 `orm:"column(performance_salary);type(decimal);digits(12);decimals(2);default(0)" json:"performance_salary"` + TransportAllowance float64 `orm:"column(transport_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"transport_allowance"` + MealAllowance float64 `orm:"column(meal_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"meal_allowance"` + CommunicationAllowance float64 `orm:"column(communication_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"communication_allowance"` + SocialInsuranceBase float64 `orm:"column(social_insurance_base);type(decimal);digits(12);decimals(2);default(0)" json:"social_insurance_base"` + HousingFundBase float64 `orm:"column(housing_fund_base);type(decimal);digits(12);decimals(2);default(0)" json:"housing_fund_base"` + SocialInsuranceRate float64 `orm:"column(social_insurance_rate);type(decimal);digits(6);decimals(4);default(0)" json:"social_insurance_rate"` + HousingFundRate float64 `orm:"column(housing_fund_rate);type(decimal);digits(6);decimals(4);default(0)" json:"housing_fund_rate"` + Status int8 `orm:"column(status);default(1)" json:"status"` + Remark string `orm:"column(remark);type(text);null" json:"remark"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendOaCompensationScheme) TableName() string { + return "yz_backend_oa_compensation_schemes" +} + +// BackendOaPayroll 月度薪资单主表 yz_backend_oa_compensation_payrolls。 +// EmployeeName / Department / Position 为生成时的快照,避免组织架构变更影响历史工资单。 +type BackendOaPayroll struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"` + SchemeID *uint64 `orm:"column(scheme_id);null" json:"scheme_id"` + PayrollMonth string `orm:"column(payroll_month);size(7)" json:"payroll_month"` + EmployeeName string `orm:"column(employee_name);size(50)" json:"employee_name"` + Department string `orm:"column(department);size(100)" json:"department"` + Position string `orm:"column(position);size(100)" json:"position"` + BaseSalary float64 `orm:"column(base_salary);type(decimal);digits(12);decimals(2);default(0)" json:"base_salary"` + PostAllowance float64 `orm:"column(post_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"post_allowance"` + PerformanceSalary float64 `orm:"column(performance_salary);type(decimal);digits(12);decimals(2);default(0)" json:"performance_salary"` + TransportAllowance float64 `orm:"column(transport_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"transport_allowance"` + MealAllowance float64 `orm:"column(meal_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"meal_allowance"` + CommunicationAllowance float64 `orm:"column(communication_allowance);type(decimal);digits(12);decimals(2);default(0)" json:"communication_allowance"` + OvertimePay float64 `orm:"column(overtime_pay);type(decimal);digits(12);decimals(2);default(0)" json:"overtime_pay"` + Bonus float64 `orm:"column(bonus);type(decimal);digits(12);decimals(2);default(0)" json:"bonus"` + OtherAddition float64 `orm:"column(other_addition);type(decimal);digits(12);decimals(2);default(0)" json:"other_addition"` + LeaveDeduction float64 `orm:"column(leave_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"leave_deduction"` + LateDeduction float64 `orm:"column(late_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"late_deduction"` + OtherDeduction float64 `orm:"column(other_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"other_deduction"` + SocialInsurance float64 `orm:"column(social_insurance);type(decimal);digits(12);decimals(2);default(0)" json:"social_insurance"` + HousingFund float64 `orm:"column(housing_fund);type(decimal);digits(12);decimals(2);default(0)" json:"housing_fund"` + PersonalIncomeTax float64 `orm:"column(personal_income_tax);type(decimal);digits(12);decimals(2);default(0)" json:"personal_income_tax"` + GrossSalary float64 `orm:"column(gross_salary);type(decimal);digits(12);decimals(2);default(0)" json:"gross_salary"` + TotalDeduction float64 `orm:"column(total_deduction);type(decimal);digits(12);decimals(2);default(0)" json:"total_deduction"` + NetSalary float64 `orm:"column(net_salary);type(decimal);digits(12);decimals(2);default(0)" json:"net_salary"` + Status int8 `orm:"column(status);default(0)" json:"status"` + ConfirmedAt *time.Time `orm:"column(confirmed_at);null;type(datetime)" json:"confirmed_at"` + PaidAt *time.Time `orm:"column(paid_at);null;type(datetime)" json:"paid_at"` + Remark string `orm:"column(remark);type(text);null" json:"remark"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendOaPayroll) TableName() string { + return "yz_backend_oa_compensation_payrolls" +} + +// BackendOaPayrollItem 薪资单自定义增减项 yz_backend_oa_compensation_payroll_items。 +type BackendOaPayrollItem struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + PayrollID uint64 `orm:"column(payroll_id)" json:"payroll_id"` + ItemName string `orm:"column(item_name);size(100)" json:"item_name"` + ItemType int8 `orm:"column(item_type);default(1)" json:"item_type"` + Amount float64 `orm:"column(amount);type(decimal);digits(12);decimals(2);default(0)" json:"amount"` + Remark string `orm:"column(remark);size(500);null" json:"remark"` + SortOrder int `orm:"column(sort_order);default(0)" json:"sort_order"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendOaPayrollItem) TableName() string { + return "yz_backend_oa_compensation_payroll_items" +} diff --git a/go/models/init.go b/go/models/init.go index bda9a68..9a51ccc 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -101,6 +101,9 @@ func Init(_ string) { new(BackendScheduleReminderSendLog), new(OaSchedule), + new(BackendOaCompensationScheme), + new(BackendOaPayroll), + new(BackendOaPayrollItem), new(BackendEmployeeFile), new(BackendEmployeeFileRecord), ) diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 281ee6c..acfb9e4 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -108,6 +108,7 @@ func RegisterAuthRoutes() { beego.Router("/backend/oa/employeefile/detail/:id", &controllers.BackendEmployeeFileController{}, "get:Detail") beego.Router("/backend/oa/employeefile/create", &controllers.BackendEmployeeFileController{}, "post:Create") beego.Router("/backend/oa/employeefile/update/:id", &controllers.BackendEmployeeFileController{}, "post:Update") + beego.Router("/backend/oa/employeefile/certificates/:id", &controllers.BackendEmployeeFileController{}, "post:UpdateCertificates") beego.Router("/backend/oa/employeefile/delete/:id", &controllers.BackendEmployeeFileController{}, "delete:Delete") beego.Router("/backend/oa/employeefile/filedEmployees", &controllers.BackendEmployeeFileController{}, "get:FiledEmployees") beego.Router("/backend/oa/employeefile/record/create", &controllers.BackendEmployeeFileController{}, "post:CreateRecord") @@ -240,6 +241,15 @@ func RegisterAuthRoutes() { beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder") beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder") + // OA 薪酬管理(薪酬方案 / 月度薪资单) + beego.Router("/backend/oa/compensation/employees", &controllers.BackendOaCompensationController{}, "get:Employees") + beego.Router("/backend/oa/compensation/dashboard", &controllers.BackendOaCompensationController{}, "get:Dashboard") + beego.Router("/backend/oa/compensation/schemes", &controllers.BackendOaCompensationController{}, "get:SchemeList;post:CreateScheme") + beego.Router("/backend/oa/compensation/schemes/:id", &controllers.BackendOaCompensationController{}, "post:UpdateScheme;delete:DeleteScheme") + beego.Router("/backend/oa/compensation/payrolls", &controllers.BackendOaCompensationController{}, "get:List;post:Create") + beego.Router("/backend/oa/compensation/payrolls/:id", &controllers.BackendOaCompensationController{}, "get:Detail;post:Update;delete:Delete") + beego.Router("/backend/oa/compensation/payrolls/:id/status", &controllers.BackendOaCompensationController{}, "post:UpdateStatus") + // OA日程管理(日历待办) beego.Router("/backend/oa/schedule/list", &controllers.BackendOaScheduleController{}, "get:List") beego.Router("/backend/oa/schedule/stats", &controllers.BackendOaScheduleController{}, "get:Stats") diff --git a/sql/seed_oa_compensation_menu.sql b/sql/seed_oa_compensation_menu.sql new file mode 100644 index 0000000..5f1c0ec --- /dev/null +++ b/sql/seed_oa_compensation_menu.sql @@ -0,0 +1,69 @@ +-- 薪酬管理菜单(租户端):挂在“办公自动化”模块目录下。 +-- +-- 说明: +-- 1. 本脚本只创建/修正 yz_system_menu 菜单记录,不涉及薪酬业务数据表。 +-- 2. 路由由前端根据登录接口返回的菜单数据动态注册; +-- 不需要在 backend/src/router/index.js 中增加静态业务路由。 +-- 3. 执行完成后,请在角色菜单权限关联表中为目标角色授予“薪酬管理”菜单权限, +-- 然后退出重登或清理菜单缓存。 +-- +-- views: [2] = 租户端 +-- type: 1 = 目录,2 = 页面 +-- 脚本可重复执行:通过 path 判重,已有记录会被修正为当前配置。 + +-- 确保“办公自动化”父级目录存在。 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 + FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/apps/oa' +); + +SET @oa_pid := ( + SELECT `id` + FROM `yz_system_menu` + WHERE `path` = '/apps/oa' + ORDER BY `id` + LIMIT 1 +); + +-- 新增“薪酬管理”页面菜单。 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + @oa_pid, + '薪酬管理', + '/apps/oa/compensation', + '/apps/oa/compensation/index.vue', + 'Money', + 6, + 1, + 1, + '[2]', + 2, + '员工薪资档案与月度工资核算' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 + FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/apps/oa/compensation' +); + +-- 修正历史或手工新增菜单的动态组件路径及基础属性。 +UPDATE `yz_system_menu` +SET + `pid` = @oa_pid, + `title` = '薪酬管理', + `component_path` = '/apps/oa/compensation/index.vue', + `icon` = 'Money', + `sort` = 6, + `status` = 1, + `is_visible` = 1, + `views` = '[2]', + `type` = 2, + `remark` = '员工薪资档案与月度工资核算' +WHERE `path` = '/apps/oa/compensation'; diff --git a/sql/seed_organization_menus.sql b/sql/seed_organization_menus.sql index 3b2aef0..82ed7e7 100644 --- a/sql/seed_organization_menus.sql +++ b/sql/seed_organization_menus.sql @@ -1,96 +1,91 @@ --- 组织架构菜单(租户端):进销存与办公自动化各自一套入口,指向共用页面组件。 --- --- 前置:yz_system_menu 中已存在 path 为 /apps/erp 与 /apps/oa 的模块目录菜单。 --- 若不存在,脚本会自动创建这两个目录菜单。 --- views 字段:[1] 平台端 / [2] 租户端 / [1,2] 双端。组织架构属于租户端,固定为 [2]。 --- type 字段:1-目录 2-页面。 --- --- 脚本可重复执行:按 path 判重,已存在的菜单会更新组件路径与标题。 +-- 全局基础设置:组织架构、人员管理、职位管理 +-- 三项功能使用统一组织数据,统一归入全局“基础设置”,不再隶属 OA 或 ERP。 +-- views:[2] 租户后台;type:1-目录,2-页面。 +-- 可重复执行。最后会删除 OA / ERP 下的历史重复页面菜单。 --- --------------------------------------------------------------------------- --- 1. 确保模块目录存在 --- --------------------------------------------------------------------------- - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT 0, '进销存', '/apps/erp', '', 'Goods', 30, 1, 1, '[2]', 1, '进销存模块' +-- 1. 确保全局“基础设置”目录存在 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + 0, '基础设置', '/basic-settings', '', 'Setting', 20, 1, 1, '[2]', 1, '全局基础数据维护' FROM DUAL WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp' + SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/basic-settings' ); -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块' +SET @basic_settings_pid := ( + SELECT `id` FROM `yz_system_menu` + WHERE `path` = '/basic-settings' + ORDER BY `id` LIMIT 1 +); + +-- 2. 创建全局入口 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + @basic_settings_pid, '组织架构', '/basic-settings/organization', + '/basicSettings/organization/index.vue', 'OfficeBuilding', 1, 1, 1, '[2]', 2, + '全局组织架构维护,与所有业务模块共用' FROM DUAL WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa' + SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/basic-settings/organization' ); -SET @erp_pid := (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/apps/erp' ORDER BY `id` LIMIT 1); -SET @oa_pid := (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/apps/oa' ORDER BY `id` LIMIT 1); - --- --------------------------------------------------------------------------- --- 2. 进销存:组织架构 / 员工管理 / 职位管理 --- --------------------------------------------------------------------------- - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @erp_pid, '组织架构', '/apps/erp/organization', '/apps/erp/organization/index.vue', 'OfficeBuilding', 1, 1, 1, '[2]', 2, '与OA共用组织数据' +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + @basic_settings_pid, '人员管理', '/basic-settings/employee', + '/basicSettings/employee/index.vue', 'User', 2, 1, 1, '[2]', 2, + '全局人员维护,与所有业务模块共用' FROM DUAL WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp/organization' + SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/basic-settings/employee' ); -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @erp_pid, '员工管理', '/apps/erp/employee', '/apps/erp/employee/index.vue', 'User', 2, 1, 1, '[2]', 2, '与OA共用员工数据' +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) +SELECT + @basic_settings_pid, '职位管理', '/basic-settings/position', + '/basicSettings/position/index.vue', 'Postcard', 3, 1, 1, '[2]', 2, + '全局职位维护,与所有业务模块共用' FROM DUAL WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp/employee' + SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t + WHERE t.`path` = '/basic-settings/position' ); -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @erp_pid, '职位管理', '/apps/erp/position', '/apps/erp/position/index.vue', 'Postcard', 3, 1, 1, '[2]', 2, '与OA共用职位数据' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/erp/position' +-- 3. 修正已存在的全局入口 +UPDATE `yz_system_menu` +SET `pid` = @basic_settings_pid, `title` = '组织架构', + `component_path` = '/basicSettings/organization/index.vue', + `icon` = 'OfficeBuilding', `sort` = 1, `status` = 1, `is_visible` = 1, + `views` = '[2]', `type` = 2, `remark` = '全局组织架构维护,与所有业务模块共用' +WHERE `path` = '/basic-settings/organization'; + +UPDATE `yz_system_menu` +SET `pid` = @basic_settings_pid, `title` = '人员管理', + `component_path` = '/basicSettings/employee/index.vue', + `icon` = 'User', `sort` = 2, `status` = 1, `is_visible` = 1, + `views` = '[2]', `type` = 2, `remark` = '全局人员维护,与所有业务模块共用' +WHERE `path` = '/basic-settings/employee'; + +UPDATE `yz_system_menu` +SET `pid` = @basic_settings_pid, `title` = '职位管理', + `component_path` = '/basicSettings/position/index.vue', + `icon` = 'Postcard', `sort` = 3, `status` = 1, `is_visible` = 1, + `views` = '[2]', `type` = 2, `remark` = '全局职位维护,与所有业务模块共用' +WHERE `path` = '/basic-settings/position'; + +-- 4. 删除历史 OA / ERP 下的重复页面入口,不删除模块目录本身。 +DELETE FROM `yz_system_menu` +WHERE `path` IN ( + '/apps/erp/organization', + '/apps/erp/employee', + '/apps/erp/position', + '/apps/oa/organization', + '/apps/oa/employee', + '/apps/oa/position' ); - --- --------------------------------------------------------------------------- --- 3. 办公自动化:组织架构 / 人员管理 / 职位管理 --- --------------------------------------------------------------------------- - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @oa_pid, '组织架构', '/apps/oa/organization', '/apps/oa/organization/index.vue', 'OfficeBuilding', 1, 1, 1, '[2]', 2, '与进销存共用组织数据' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/organization' -); - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @oa_pid, '人员管理', '/apps/oa/employee', '/apps/oa/employee/index.vue', 'User', 2, 1, 1, '[2]', 2, '与进销存共用员工数据' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/employee' -); - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @oa_pid, '职位管理', '/apps/oa/position', '/apps/oa/position/index.vue', 'Postcard', 3, 1, 1, '[2]', 2, '与进销存共用职位数据' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/position' -); - --- --------------------------------------------------------------------------- --- 4. 修正已有菜单的组件路径(历史数据可能指向旧组件) --- --------------------------------------------------------------------------- - -UPDATE `yz_system_menu` SET `component_path` = '/apps/erp/organization/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/erp/organization'; -UPDATE `yz_system_menu` SET `component_path` = '/apps/erp/employee/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/erp/employee'; -UPDATE `yz_system_menu` SET `component_path` = '/apps/erp/position/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/erp/position'; -UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/organization/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/oa/organization'; -UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/employee/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/oa/employee'; -UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/position/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/oa/position';