增加档案功能

This commit is contained in:
2026-08-29 22:06:00 +08:00
parent 2dfc6d6aaf
commit 79217187ac
21 changed files with 2015 additions and 488 deletions
+144 -30
View File
@@ -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