961 lines
29 KiB
Go
961 lines
29 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"server/models"
|
|
"server/pkg/jwtutil"
|
|
|
|
"github.com/beego/beego/v2/client/orm"
|
|
beego "github.com/beego/beego/v2/server/web"
|
|
)
|
|
|
|
// BackendEmployeeFileController 员工档案管理(OA 人事档案)。
|
|
// 档案与员工(yz_backend_employee)按 tid + employee_id 一对一,
|
|
// 子记录表统一存放教育经历 / 工作经历 / 合同信息 / 证照附件。
|
|
// 数据按 JWT 中的租户隔离。
|
|
type BackendEmployeeFileController struct {
|
|
beego.Controller
|
|
}
|
|
|
|
func (c *BackendEmployeeFileController) efClaims() (*jwtutil.Claims, error) {
|
|
auth := c.Ctx.Request.Header.Get("Authorization")
|
|
if auth == "" {
|
|
return nil, orm.ErrNoRows
|
|
}
|
|
parts := strings.SplitN(auth, " ", 2)
|
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
return nil, orm.ErrNoRows
|
|
}
|
|
claims, err := jwtutil.ParseToken(parts[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if claims.UserType != "backend" {
|
|
return nil, orm.ErrNoRows
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
func (c *BackendEmployeeFileController) efOk(data interface{}) {
|
|
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
|
|
_ = c.ServeJSON()
|
|
}
|
|
|
|
func (c *BackendEmployeeFileController) efErr(httpStatus, bizCode int, msg string) {
|
|
c.Ctx.Output.SetStatus(httpStatus)
|
|
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
|
|
_ = c.ServeJSON()
|
|
}
|
|
|
|
var efDatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
|
|
|
// efIsValidDate 校验 "YYYY-MM-DD" 日期字符串;允许为空(可空字段)。
|
|
func efIsValidDate(s string) bool {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return true
|
|
}
|
|
if !efDatePattern.MatchString(s) {
|
|
return false
|
|
}
|
|
_, err := time.ParseInLocation("2006-01-02", s, time.Local)
|
|
return err == nil
|
|
}
|
|
|
|
// efNormalizeDate 将可空 DATE 列统一输出为 "YYYY-MM-DD"。
|
|
func efNormalizeDate(value *time.Time) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
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 档案查询基座:强制附加租户过滤与未删除条件。
|
|
func (c *BackendEmployeeFileController) efQuery(tid int) orm.QuerySeter {
|
|
return models.Orm.QueryTable(new(models.BackendEmployeeFile)).
|
|
Filter("tid", tid).
|
|
Filter("is_deleted", 0)
|
|
}
|
|
|
|
// efEmployeeQuery 员工查询基座(与组织架构控制器保持同一过滤口径)。
|
|
func efEmployeeQuery(tid int) orm.QuerySeter {
|
|
return models.Orm.QueryTable(new(models.BackendEmployee)).
|
|
Filter("tid", tid).
|
|
Filter("delete_time__isnull", true)
|
|
}
|
|
|
|
// efOrgNameMap 组织ID -> 名称映射,用于展示员工的隶属单位/部门名称。
|
|
func (c *BackendEmployeeFileController) efOrgNameMap(tid int) map[uint64]string {
|
|
var rows []models.BackendOrganization
|
|
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
|
Filter("tid", tid).
|
|
Filter("delete_time__isnull", true).
|
|
All(&rows, "ID", "OrgName"); err != nil {
|
|
return map[uint64]string{}
|
|
}
|
|
names := make(map[uint64]string, len(rows))
|
|
for _, row := range rows {
|
|
names[row.ID] = row.OrgName
|
|
}
|
|
return names
|
|
}
|
|
|
|
// efOrgNameByID 组织ID字符串转名称;非数字时原样返回(手输文本兜底)。
|
|
func efOrgNameByID(names map[uint64]string, id string) string {
|
|
if id == "" {
|
|
return ""
|
|
}
|
|
if v, err := strconv.ParseUint(id, 10, 64); err == nil {
|
|
return names[v]
|
|
}
|
|
return id
|
|
}
|
|
|
|
// efSubtreeOrgIDs 收集组织节点及其全部下级的ID(BFS,含环保护)。
|
|
func (c *BackendEmployeeFileController) efSubtreeOrgIDs(tid int, rootID uint64) []uint64 {
|
|
var rows []models.BackendOrganization
|
|
if _, err := models.Orm.QueryTable(new(models.BackendOrganization)).
|
|
Filter("tid", tid).
|
|
Filter("delete_time__isnull", true).
|
|
All(&rows, "ID", "ParentID"); err != nil {
|
|
return []uint64{rootID}
|
|
}
|
|
childrenOf := map[uint64][]uint64{}
|
|
for _, row := range rows {
|
|
childrenOf[row.ParentID] = append(childrenOf[row.ParentID], row.ID)
|
|
}
|
|
|
|
result := []uint64{rootID}
|
|
queue := []uint64{rootID}
|
|
visited := map[uint64]bool{rootID: true}
|
|
for len(queue) > 0 {
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
for _, child := range childrenOf[current] {
|
|
if visited[child] {
|
|
continue
|
|
}
|
|
visited[child] = true
|
|
result = append(result, child)
|
|
queue = append(queue, child)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// efEmployeeIDsInOrgs 取部门集合内的员工ID(档案按员工过滤时使用)。
|
|
func (c *BackendEmployeeFileController) efEmployeeIDsInOrgs(tid int, orgIDs []uint64) []uint64 {
|
|
values := make([]string, 0, len(orgIDs))
|
|
for _, id := range orgIDs {
|
|
values = append(values, strconv.FormatUint(id, 10))
|
|
}
|
|
var rows []models.BackendEmployee
|
|
if _, err := efEmployeeQuery(tid).Filter("department__in", values).All(&rows, "ID"); err != nil {
|
|
return nil
|
|
}
|
|
ids := make([]uint64, 0, len(rows))
|
|
for _, row := range rows {
|
|
ids = append(ids, uint64(row.ID))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// efEmployeeIDsByKeyword 取姓名/账号/手机命中的员工ID(档案关键词搜索联动员工字段)。
|
|
func (c *BackendEmployeeFileController) efEmployeeIDsByKeyword(tid int, keyword string) []uint64 {
|
|
cond := orm.NewCondition().
|
|
Or("name__icontains", keyword).
|
|
Or("account__icontains", keyword).
|
|
Or("phone__icontains", keyword)
|
|
var rows []models.BackendEmployee
|
|
if _, err := efEmployeeQuery(tid).SetCond(cond).All(&rows, "ID"); err != nil {
|
|
return nil
|
|
}
|
|
ids := make([]uint64, 0, len(rows))
|
|
for _, row := range rows {
|
|
ids = append(ids, uint64(row.ID))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// employeeFileDTO 档案 + 关联员工展示信息(JSON 平铺,前端表格/详情直接用)。
|
|
type employeeFileDTO struct {
|
|
models.BackendEmployeeFile
|
|
EmployeeName string `json:"employee_name"`
|
|
EmployeeAccount string `json:"employee_account"`
|
|
Gender int8 `json:"gender"`
|
|
Birthday string `json:"birthday"`
|
|
AffiliateUnit string `json:"affiliate_unit"`
|
|
AffiliateUnitName string `json:"affiliate_unit_name"`
|
|
Department string `json:"department"`
|
|
DepartmentName string `json:"department_name"`
|
|
Position string `json:"position"`
|
|
Phone string `json:"phone"`
|
|
Email string `json:"email"`
|
|
Education string `json:"education"`
|
|
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 档案列表批量组装员工展示信息。
|
|
func (c *BackendEmployeeFileController) efAssembleDTOList(tid int, files []models.BackendEmployeeFile) []employeeFileDTO {
|
|
list := make([]employeeFileDTO, 0, len(files))
|
|
if len(files) == 0 {
|
|
return list
|
|
}
|
|
|
|
empIDs := make([]uint64, 0, len(files))
|
|
for _, f := range files {
|
|
empIDs = append(empIDs, f.EmployeeID)
|
|
}
|
|
var emps []models.BackendEmployee
|
|
if _, err := efEmployeeQuery(tid).Filter("id__in", empIDs).All(&emps); err != nil {
|
|
emps = nil
|
|
}
|
|
empByID := make(map[uint64]models.BackendEmployee, len(emps))
|
|
for _, e := range emps {
|
|
empByID[uint64(e.ID)] = e
|
|
}
|
|
|
|
orgNames := c.efOrgNameMap(tid)
|
|
|
|
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)
|
|
if emp, ok := empByID[f.EmployeeID]; ok {
|
|
birthday := ""
|
|
if emp.Birthday != nil {
|
|
birthday = emp.Birthday.Format("2006-01-02")
|
|
}
|
|
affiliate := derefString(emp.AffiliateUnit)
|
|
department := derefString(emp.Department)
|
|
dto.EmployeeName = emp.Name
|
|
dto.EmployeeAccount = emp.Account
|
|
dto.Gender = emp.Gender
|
|
dto.Birthday = birthday
|
|
dto.AffiliateUnit = affiliate
|
|
dto.AffiliateUnitName = efOrgNameByID(orgNames, affiliate)
|
|
dto.Department = department
|
|
dto.DepartmentName = efOrgNameByID(orgNames, department)
|
|
dto.Position = derefString(emp.Position)
|
|
dto.Phone = derefString(emp.Phone)
|
|
dto.Email = derefString(emp.Email)
|
|
dto.Education = derefString(emp.Education)
|
|
dto.Nation = derefString(emp.Nation)
|
|
dto.HomeAddress = derefString(emp.HomeAddress)
|
|
dto.AccountStatus = emp.AccountStatus
|
|
}
|
|
list = append(list, dto)
|
|
}
|
|
return list
|
|
}
|
|
|
|
// List GET /backend/oa/employeefile/list
|
|
// 分页查询档案,支持 keyword(档案编号/身份证/员工姓名/账号/手机)、
|
|
// status(在职状态)、org_id(部门,含下级)筛选。
|
|
func (c *BackendEmployeeFileController) List() {
|
|
claims, err := c.efClaims()
|
|
if err != nil {
|
|
c.efErr(401, 401, "未登录或无权限")
|
|
return
|
|
}
|
|
tid := claims.TenantId
|
|
|
|
page, _ := c.GetInt("page", 1)
|
|
pageSize, _ := c.GetInt("pageSize", 10)
|
|
keyword := strings.TrimSpace(c.GetString("keyword"))
|
|
statusRaw := strings.TrimSpace(c.GetString("status"))
|
|
orgID, _ := c.GetUint64("org_id")
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 200 {
|
|
pageSize = 10
|
|
}
|
|
|
|
qs := c.efQuery(tid)
|
|
|
|
if orgID > 0 {
|
|
empIDs := c.efEmployeeIDsInOrgs(tid, c.efSubtreeOrgIDs(tid, orgID))
|
|
if len(empIDs) == 0 {
|
|
c.efOk(map[string]interface{}{"list": []employeeFileDTO{}, "total": 0})
|
|
return
|
|
}
|
|
qs = qs.Filter("employee_id__in", empIDs)
|
|
}
|
|
if statusRaw != "" {
|
|
status, convErr := strconv.Atoi(statusRaw)
|
|
if convErr != nil || status < 1 || status > 3 {
|
|
c.efErr(400, 400, "在职状态参数无效")
|
|
return
|
|
}
|
|
qs = qs.Filter("employment_status", int8(status))
|
|
}
|
|
if keyword != "" {
|
|
// 关键词同时命中档案自身字段(编号/身份证)与员工字段(姓名/账号/手机)
|
|
empIDs := c.efEmployeeIDsByKeyword(tid, keyword)
|
|
cond := orm.NewCondition().
|
|
Or("file_no__icontains", keyword).
|
|
Or("id_card__icontains", keyword)
|
|
if len(empIDs) > 0 {
|
|
cond = cond.Or("employee_id__in", empIDs)
|
|
}
|
|
qs = qs.SetCond(orm.NewCondition().AndCond(cond))
|
|
}
|
|
|
|
total, _ := qs.Count()
|
|
|
|
var files []models.BackendEmployeeFile
|
|
if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&files); err != nil && err != orm.ErrNoRows {
|
|
c.efErr(500, 500, "查询失败")
|
|
return
|
|
}
|
|
|
|
c.efOk(map[string]interface{}{
|
|
"list": c.efAssembleDTOList(tid, files),
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
// Stats GET /backend/oa/employeefile/stats
|
|
// 返回档案总数与在职状态分布,供页面顶部统计卡片展示。
|
|
func (c *BackendEmployeeFileController) Stats() {
|
|
claims, err := c.efClaims()
|
|
if err != nil {
|
|
c.efErr(401, 401, "未登录或无权限")
|
|
return
|
|
}
|
|
tid := claims.TenantId
|
|
|
|
total, _ := c.efQuery(tid).Count()
|
|
probation, _ := c.efQuery(tid).Filter("employment_status", 1).Count()
|
|
active, _ := c.efQuery(tid).Filter("employment_status", 2).Count()
|
|
resigned, _ := c.efQuery(tid).Filter("employment_status", 3).Count()
|
|
|
|
c.efOk(map[string]interface{}{
|
|
"total": total,
|
|
"probation": probation,
|
|
"active": active,
|
|
"resigned": resigned,
|
|
})
|
|
}
|
|
|
|
// Detail GET /backend/oa/employeefile/detail/:id
|
|
// 返回档案详情(含员工展示信息)与全部子记录(教育/工作/合同/证照)。
|
|
func (c *BackendEmployeeFileController) Detail() {
|
|
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 file models.BackendEmployeeFile
|
|
if err := c.efQuery(tid).Filter("id", id).One(&file); err != nil {
|
|
c.efErr(404, 404, "档案不存在")
|
|
return
|
|
}
|
|
|
|
var records []models.BackendEmployeeFileRecord
|
|
if _, err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
|
Filter("tid", tid).
|
|
Filter("is_deleted", 0).
|
|
Filter("employee_id", file.EmployeeID).
|
|
OrderBy("sort", "id").
|
|
All(&records); err != nil && err != orm.ErrNoRows {
|
|
c.efErr(500, 500, "查询档案记录失败")
|
|
return
|
|
}
|
|
if records == nil {
|
|
records = []models.BackendEmployeeFileRecord{}
|
|
}
|
|
list := c.efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
|
|
c.efOk(map[string]interface{}{"file": list[0], "records": records})
|
|
}
|
|
|
|
// employeeFilePayload 建档/编辑档案请求体。
|
|
type employeeFilePayload struct {
|
|
EmployeeID uint64 `json:"employee_id"`
|
|
FileNo string `json:"file_no"`
|
|
IDCard string `json:"id_card"`
|
|
IDCardFront string `json:"id_card_front"`
|
|
IDCardBack string `json:"id_card_back"`
|
|
EducationPhoto string `json:"education_photo"`
|
|
PoliticalStatus string `json:"political_status"`
|
|
MaritalStatus int8 `json:"marital_status"`
|
|
NativePlace string `json:"native_place"`
|
|
HouseholdAddress string `json:"household_address"`
|
|
CurrentAddress string `json:"current_address"`
|
|
EmergencyContact string `json:"emergency_contact"`
|
|
EmergencyPhone string `json:"emergency_phone"`
|
|
EmergencyRelation string `json:"emergency_relation"`
|
|
WorkEmail string `json:"work_email"`
|
|
HireDate string `json:"hire_date"`
|
|
RegularDate string `json:"regular_date"`
|
|
LeaveDate string `json:"leave_date"`
|
|
EmploymentStatus int8 `json:"employment_status"`
|
|
Remark string `json:"remark"`
|
|
}
|
|
|
|
// efParsePayload 读取并校验建档/编辑档案的请求体;校验失败时直接输出错误响应。
|
|
func (c *BackendEmployeeFileController) efParsePayload() (employeeFilePayload, bool) {
|
|
var payload employeeFilePayload
|
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
|
if err != nil || json.Unmarshal(raw, &payload) != nil {
|
|
c.efErr(400, 400, "参数错误")
|
|
return payload, false
|
|
}
|
|
|
|
payload.FileNo = strings.TrimSpace(payload.FileNo)
|
|
payload.IDCard = strings.TrimSpace(payload.IDCard)
|
|
payload.WorkEmail = strings.TrimSpace(payload.WorkEmail)
|
|
if payload.IDCard != "" && len(payload.IDCard) > 30 {
|
|
c.efErr(400, 400, "身份证号过长")
|
|
return payload, false
|
|
}
|
|
// 证照附件地址长度限制(与列宽一致)
|
|
for label, url := range map[string]string{
|
|
"身份证正面": payload.IDCardFront,
|
|
"身份证反面": payload.IDCardBack,
|
|
"学历照片": payload.EducationPhoto,
|
|
} {
|
|
if len(url) > 500 {
|
|
c.efErr(400, 400, label+"地址过长")
|
|
return payload, false
|
|
}
|
|
}
|
|
if payload.WorkEmail != "" && !strings.Contains(payload.WorkEmail, "@") {
|
|
c.efErr(400, 400, "工作邮箱格式无效")
|
|
return payload, false
|
|
}
|
|
for label, value := range map[string]string{
|
|
"入职日期": payload.HireDate,
|
|
"转正日期": payload.RegularDate,
|
|
"离职日期": payload.LeaveDate,
|
|
} {
|
|
if !efIsValidDate(value) {
|
|
c.efErr(400, 400, label+"格式无效")
|
|
return payload, false
|
|
}
|
|
}
|
|
if payload.EmploymentStatus < 1 || payload.EmploymentStatus > 3 {
|
|
payload.EmploymentStatus = 2
|
|
}
|
|
if payload.MaritalStatus < 0 || payload.MaritalStatus > 4 {
|
|
payload.MaritalStatus = 0
|
|
}
|
|
return payload, true
|
|
}
|
|
|
|
// Create POST /backend/oa/employeefile/create
|
|
// 为员工建档;一人一档,重复建档会被唯一约束与前置校验拦截。
|
|
func (c *BackendEmployeeFileController) Create() {
|
|
claims, err := c.efClaims()
|
|
if err != nil {
|
|
c.efErr(401, 401, "未登录或无权限")
|
|
return
|
|
}
|
|
tid := claims.TenantId
|
|
|
|
payload, ok := c.efParsePayload()
|
|
if !ok {
|
|
return
|
|
}
|
|
if payload.EmployeeID == 0 {
|
|
c.efErr(400, 400, "请选择员工")
|
|
return
|
|
}
|
|
|
|
// 员工必须存在
|
|
var emp models.BackendEmployee
|
|
if err := efEmployeeQuery(tid).Filter("id", payload.EmployeeID).One(&emp); err != nil {
|
|
c.efErr(404, 404, "员工不存在")
|
|
return
|
|
}
|
|
// 一人一档
|
|
dup, _ := c.efQuery(tid).Filter("employee_id", payload.EmployeeID).Count()
|
|
if dup > 0 {
|
|
c.efErr(400, 400, "该员工已建档,请勿重复创建")
|
|
return
|
|
}
|
|
|
|
// 档案编号留空自动生成:EF + 时间戳
|
|
fileNo := payload.FileNo
|
|
if fileNo == "" {
|
|
fileNo = "EF" + time.Now().Format("20060102150405")
|
|
}
|
|
|
|
// 证照资料只能在建档完成后,于档案详情中维护;创建接口不接受附件。
|
|
payload.IDCardFront = ""
|
|
payload.IDCardBack = ""
|
|
payload.EducationPhoto = ""
|
|
|
|
now := time.Now()
|
|
item := &models.BackendEmployeeFile{
|
|
Tid: tid,
|
|
EmployeeID: payload.EmployeeID,
|
|
FileNo: fileNo,
|
|
IDCard: payload.IDCard,
|
|
IDCardFront: payload.IDCardFront,
|
|
IDCardBack: payload.IDCardBack,
|
|
EducationPhoto: payload.EducationPhoto,
|
|
PoliticalStatus: payload.PoliticalStatus,
|
|
MaritalStatus: payload.MaritalStatus,
|
|
NativePlace: payload.NativePlace,
|
|
HouseholdAddress: payload.HouseholdAddress,
|
|
CurrentAddress: payload.CurrentAddress,
|
|
EmergencyContact: payload.EmergencyContact,
|
|
EmergencyPhone: payload.EmergencyPhone,
|
|
EmergencyRelation: payload.EmergencyRelation,
|
|
WorkEmail: payload.WorkEmail,
|
|
HireDate: efDatePointer(payload.HireDate),
|
|
RegularDate: efDatePointer(payload.RegularDate),
|
|
LeaveDate: efDatePointer(payload.LeaveDate),
|
|
EmploymentStatus: payload.EmploymentStatus,
|
|
Remark: payload.Remark,
|
|
IsDeleted: 0,
|
|
CreateTime: now,
|
|
UpdateTime: &now,
|
|
}
|
|
if _, err := models.Orm.Insert(item); err != nil {
|
|
log.Printf("员工档案创建失败: tid=%d employee_id=%d err=%v", tid, payload.EmployeeID, err)
|
|
c.efErr(500, 500, "保存失败,请检查员工档案表字段及日期数据")
|
|
return
|
|
}
|
|
|
|
c.efOk(item)
|
|
}
|
|
|
|
// Update POST /backend/oa/employeefile/update/:id
|
|
// 更新档案基本资料;员工归属(employee_id)不允许修改。
|
|
func (c *BackendEmployeeFileController) Update() {
|
|
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
|
|
}
|
|
|
|
payload, ok := c.efParsePayload()
|
|
if !ok {
|
|
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.FileNo = payload.FileNo
|
|
item.IDCard = payload.IDCard
|
|
// 证照字段由 UpdateCertificates 独立维护,基础资料保存不得覆盖已有附件。
|
|
item.PoliticalStatus = payload.PoliticalStatus
|
|
item.MaritalStatus = payload.MaritalStatus
|
|
item.NativePlace = payload.NativePlace
|
|
item.HouseholdAddress = payload.HouseholdAddress
|
|
item.CurrentAddress = payload.CurrentAddress
|
|
item.EmergencyContact = payload.EmergencyContact
|
|
item.EmergencyPhone = payload.EmergencyPhone
|
|
item.EmergencyRelation = payload.EmergencyRelation
|
|
item.WorkEmail = payload.WorkEmail
|
|
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", "PoliticalStatus", "MaritalStatus", "NativePlace",
|
|
"HouseholdAddress", "CurrentAddress", "EmergencyContact", "EmergencyPhone",
|
|
"EmergencyRelation", "WorkEmail", "HireDate", "RegularDate", "LeaveDate",
|
|
"EmploymentStatus", "Remark", "UpdateTime"); err != nil {
|
|
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
|
|
}
|
|
|
|
c.efOk(item)
|
|
}
|
|
|
|
// Delete DELETE /backend/oa/employeefile/delete/:id 软删除档案。
|
|
func (c *BackendEmployeeFileController) Delete() {
|
|
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
|
|
}
|
|
|
|
now := time.Now()
|
|
n, err := c.efQuery(tid).Filter("id", id).
|
|
Update(map[string]interface{}{
|
|
"IsDeleted": int8(1),
|
|
"DeleteTime": now,
|
|
"UpdateTime": now,
|
|
})
|
|
if err != nil || n == 0 {
|
|
c.efErr(404, 404, "档案不存在或已删除")
|
|
return
|
|
}
|
|
|
|
c.efOk(nil)
|
|
}
|
|
|
|
// FiledEmployees GET /backend/oa/employeefile/filedEmployees
|
|
// 返回已建档的员工ID列表,前端用于过滤出可建档(未建档)员工。
|
|
func (c *BackendEmployeeFileController) FiledEmployees() {
|
|
claims, err := c.efClaims()
|
|
if err != nil {
|
|
c.efErr(401, 401, "未登录或无权限")
|
|
return
|
|
}
|
|
tid := claims.TenantId
|
|
|
|
var files []models.BackendEmployeeFile
|
|
if _, err := c.efQuery(tid).All(&files, "EmployeeID"); err != nil {
|
|
c.efErr(500, 500, "查询失败")
|
|
return
|
|
}
|
|
ids := make([]uint64, 0, len(files))
|
|
for _, f := range files {
|
|
ids = append(ids, f.EmployeeID)
|
|
}
|
|
c.efOk(ids)
|
|
}
|
|
|
|
// employeeFileRecordPayload 档案子记录请求体。
|
|
// type: 1-教育经历 2-工作经历 3-合同信息 4-证照附件。
|
|
type employeeFileRecordPayload struct {
|
|
EmployeeID uint64 `json:"employee_id"`
|
|
Type int8 `json:"type"`
|
|
Title string `json:"title"`
|
|
SubTitle string `json:"sub_title"`
|
|
Extra string `json:"extra"`
|
|
StartDate string `json:"start_date"`
|
|
EndDate string `json:"end_date"`
|
|
Description string `json:"description"`
|
|
AttachmentURL string `json:"attachment_url"`
|
|
Sort int `json:"sort"`
|
|
}
|
|
|
|
// efParseRecordPayload 读取并校验子记录请求体;校验失败时直接输出错误响应。
|
|
func (c *BackendEmployeeFileController) efParseRecordPayload() (employeeFileRecordPayload, bool) {
|
|
var payload employeeFileRecordPayload
|
|
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
|
if err != nil || json.Unmarshal(raw, &payload) != nil {
|
|
c.efErr(400, 400, "参数错误")
|
|
return payload, false
|
|
}
|
|
|
|
if payload.EmployeeID == 0 {
|
|
c.efErr(400, 400, "参数缺少员工")
|
|
return payload, false
|
|
}
|
|
if payload.Type < 1 || payload.Type > 4 {
|
|
c.efErr(400, 400, "记录类型无效")
|
|
return payload, false
|
|
}
|
|
payload.Title = strings.TrimSpace(payload.Title)
|
|
if payload.Title == "" {
|
|
c.efErr(400, 400, "请输入名称")
|
|
return payload, false
|
|
}
|
|
if len(payload.Title) > 100 {
|
|
c.efErr(400, 400, "名称不能超过100字")
|
|
return payload, false
|
|
}
|
|
payload.StartDate = strings.TrimSpace(payload.StartDate)
|
|
payload.EndDate = strings.TrimSpace(payload.EndDate)
|
|
if !efIsValidDate(payload.StartDate) || !efIsValidDate(payload.EndDate) {
|
|
c.efErr(400, 400, "日期格式无效")
|
|
return payload, false
|
|
}
|
|
if payload.StartDate != "" && payload.EndDate != "" && payload.EndDate < payload.StartDate {
|
|
c.efErr(400, 400, "结束日期不能早于开始日期")
|
|
return payload, false
|
|
}
|
|
if len(payload.AttachmentURL) > 500 {
|
|
c.efErr(400, 400, "附件地址过长")
|
|
return payload, false
|
|
}
|
|
return payload, true
|
|
}
|
|
|
|
// CreateRecord POST /backend/oa/employeefile/record/create 新增子记录。
|
|
func (c *BackendEmployeeFileController) CreateRecord() {
|
|
claims, err := c.efClaims()
|
|
if err != nil {
|
|
c.efErr(401, 401, "未登录或无权限")
|
|
return
|
|
}
|
|
tid := claims.TenantId
|
|
|
|
payload, ok := c.efParseRecordPayload()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// 档案必须存在(记录挂在员工上,但以建档为前提)
|
|
exists, _ := c.efQuery(tid).Filter("employee_id", payload.EmployeeID).Count()
|
|
if exists == 0 {
|
|
c.efErr(404, 404, "档案不存在,请先建档")
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
item := &models.BackendEmployeeFileRecord{
|
|
Tid: tid,
|
|
EmployeeID: payload.EmployeeID,
|
|
Type: payload.Type,
|
|
Title: payload.Title,
|
|
SubTitle: payload.SubTitle,
|
|
Extra: payload.Extra,
|
|
StartDate: efDatePointer(payload.StartDate),
|
|
EndDate: efDatePointer(payload.EndDate),
|
|
Description: payload.Description,
|
|
AttachmentURL: payload.AttachmentURL,
|
|
Sort: payload.Sort,
|
|
IsDeleted: 0,
|
|
CreateTime: now,
|
|
UpdateTime: &now,
|
|
}
|
|
if _, err := models.Orm.Insert(item); err != nil {
|
|
c.efErr(500, 500, "保存失败")
|
|
return
|
|
}
|
|
|
|
c.efOk(item)
|
|
}
|
|
|
|
// UpdateRecord POST /backend/oa/employeefile/record/update/:id 更新子记录。
|
|
func (c *BackendEmployeeFileController) UpdateRecord() {
|
|
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
|
|
}
|
|
|
|
payload, ok := c.efParseRecordPayload()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var item models.BackendEmployeeFileRecord
|
|
if err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
|
Filter("tid", tid).
|
|
Filter("is_deleted", 0).
|
|
Filter("id", id).
|
|
One(&item); err != nil {
|
|
c.efErr(404, 404, "记录不存在")
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
item.Type = payload.Type
|
|
item.Title = payload.Title
|
|
item.SubTitle = payload.SubTitle
|
|
item.Extra = payload.Extra
|
|
item.StartDate = efDatePointer(payload.StartDate)
|
|
item.EndDate = efDatePointer(payload.EndDate)
|
|
item.Description = payload.Description
|
|
item.AttachmentURL = payload.AttachmentURL
|
|
item.Sort = payload.Sort
|
|
item.UpdateTime = &now
|
|
|
|
if _, err := models.Orm.Update(&item,
|
|
"Type", "Title", "SubTitle", "Extra", "StartDate", "EndDate",
|
|
"Description", "AttachmentURL", "Sort", "UpdateTime"); err != nil {
|
|
c.efErr(500, 500, "保存失败")
|
|
return
|
|
}
|
|
|
|
c.efOk(item)
|
|
}
|
|
|
|
// DeleteRecord DELETE /backend/oa/employeefile/record/delete/:id 软删除子记录。
|
|
func (c *BackendEmployeeFileController) DeleteRecord() {
|
|
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
|
|
}
|
|
|
|
now := time.Now()
|
|
n, err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
|
Filter("tid", tid).
|
|
Filter("is_deleted", 0).
|
|
Filter("id", id).
|
|
Update(map[string]interface{}{
|
|
"IsDeleted": int8(1),
|
|
"DeleteTime": now,
|
|
"UpdateTime": now,
|
|
})
|
|
if err != nil || n == 0 {
|
|
c.efErr(404, 404, "记录不存在或已删除")
|
|
return
|
|
}
|
|
|
|
c.efOk(nil)
|
|
}
|