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 @@
+
+
+
+
+
+ 统计月份
+
+
+
+
+
核算员工
+
{{ dashboard.employee_count || 0 }}
+
{{ dashboard.payroll_month || dashboardMonth }} 薪资单
+
+
+
应发工资
+
¥{{ money(dashboard.total_gross_salary) }}
+
税前及各类补贴合计
+
+
+
实发工资
+
¥{{ money(dashboard.total_net_salary) }}
+
扣除五险一金及个税后
+
+
+
待处理
+
{{ (dashboard.draft_count || 0) + (dashboard.confirmed_count || 0) }}
+
草稿 {{ dashboard.draft_count || 0 }} / 待发放 {{ dashboard.confirmed_count || 0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+
+
+
+ ¥{{ money(row.gross_salary) }}
+
+
+ ¥{{ money(row.total_deduction) }}
+
+
+ ¥{{ money(row.net_salary) }}
+
+
+
+ {{ statusText(row.status) }}
+
+
+
+ {{ dateTime(row.update_time || row.create_time) }}
+
+
+
+ 详情
+ 编辑
+ changeStatus(row, status)">
+ 更多
+
+
+ 确认薪资单
+ 标记为已发放
+ 作废薪资单
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
应发项目
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 自定义薪资项
+ 添加项目
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
应发合计¥{{ money(grossTotal) }}
+
扣款合计¥{{ money(deductionTotal) }}
+
实发工资¥{{ money(netTotal) }}
+
+
+
+ 取消
+ 保存薪资单
+
+
+
+
+
+
+
{{ detail.employee_name }}{{ detail.department || "未设置部门" }} · {{ detail.position || "未设置职位" }}
+
{{ detail.payroll_month }} 薪资{{ statusText(detail.status) }}
+
+
+
应发工资¥{{ money(detail.gross_salary) }}
+
扣款合计¥{{ money(detail.total_deduction) }}
+
实发工资¥{{ money(detail.net_salary) }}
+
+
+ ¥{{ money(entry.value) }}
+
+
+
自定义薪资项
+
+
+ {{ row.item_type === 1 ? "增项" : "扣项" }}
+ ¥{{ money(row.amount) }}
+
+
+
+
+
+ 关闭
+
+
+
+
+ 新增方案
+
+
+
+ {{ employeeName(row.employee_id) }}
+
+ {{ row.expiry_date || "长期" }}
+ ¥{{ money(fixedSchemeAmount(row)) }}
+ {{ row.status === 0 ? "生效中" : "已停用" }}
+
+ 编辑删除
+
+
+
+
+
+
+
+
+
+
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 }}
-
+
{{ row.sub_title || '-' }}
@@ -138,20 +107,47 @@
{{ contractStatus(row).text }}
+
+
+
+ 查看附件
+
+ -
+
+
{{ row.description || '-' }}
-
- 编辑
-
- 删除
+
+
+ 删除
+
+
+
+
+ 编辑
+
+ 删除
+
@@ -161,8 +157,23 @@
-
+
+
+
+ {{ file.education_photo ? '替换学历照片' : '上传学历照片' }}
+
+
-
-
+
+
![]()
{{ item.sub_title || ' ' }}
预览
-
- 编辑
-
- 删除
+
+
+ 删除
+
+
+
+
+ 编辑
+
+ 删除
+
@@ -212,10 +236,15 @@
-
-
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';