From 2dfc6d6aaf17ec2a89ff34c98e0a511c751afa8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com> Date: Fri, 28 Aug 2026 18:01:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=91=98=E5=B7=A5=E6=A1=A3?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/employeeFile.js | 65 ++ backend/src/router/index.js | 6 + .../components/fileDetailDrawer.vue | 547 +++++++++++ .../components/fileEditDialog.vue | 428 +++++++++ .../components/recordEditDialog.vue | 290 ++++++ .../src/views/apps/oa/employeefile/index.vue | 348 +++++++ go/controllers/backend_employee_file.go | 846 ++++++++++++++++++ go/models/backend_employee_file.go | 67 ++ go/models/init.go | 2 + go/routers/backend/backend.go | 12 + sql/alter_employee_file_add_cert_photos.sql | 8 + sql/seed_employee_file_menu.sql | 27 + sql/yz_backend_employee_file.sql | 70 ++ test-output.txt | 24 - 14 files changed, 2716 insertions(+), 24 deletions(-) create mode 100644 backend/src/api/employeeFile.js create mode 100644 backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue create mode 100644 backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue create mode 100644 backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue create mode 100644 backend/src/views/apps/oa/employeefile/index.vue create mode 100644 go/controllers/backend_employee_file.go create mode 100644 go/models/backend_employee_file.go create mode 100644 sql/alter_employee_file_add_cert_photos.sql create mode 100644 sql/seed_employee_file_menu.sql create mode 100644 sql/yz_backend_employee_file.sql delete mode 100644 test-output.txt diff --git a/backend/src/api/employeeFile.js b/backend/src/api/employeeFile.js new file mode 100644 index 0000000..296ac59 --- /dev/null +++ b/backend/src/api/employeeFile.js @@ -0,0 +1,65 @@ +import request from '@/utils/request'; + +/** + * 员工档案接口(OA 人事档案) + * + * 档案与员工(yz_backend_employee)一对一;子记录接口统一管理 + * 教育经历 / 工作经历 / 合同信息 / 证照附件四类记录。 + * 后端统一返回 { code, data, msg } 包装,调用方需自行取 res.data 使用。 + */ + +const BASE = '/backend/oa/employeefile'; + +/* --------------------------------- 档案 --------------------------------- */ + +/** 分页查询档案列表,支持 keyword / status / org_id 筛选 */ +export function getEmployeeFileList(params) { + return request({ url: `${BASE}/list`, method: 'get', params }); +} + +/** 档案数量统计(总数 + 在职状态分布) */ +export function getEmployeeFileStats() { + return request({ url: `${BASE}/stats`, method: 'get' }); +} + +/** 档案详情(含员工展示信息与全部子记录) */ +export function getEmployeeFileDetail(id) { + return request({ url: `${BASE}/detail/${id}`, method: 'get' }); +} + +/** 为员工建档;file_no 留空由后端自动生成 */ +export function createEmployeeFile(data) { + return request({ url: `${BASE}/create`, method: 'post', data }); +} + +/** 更新档案基本资料(不允许修改员工归属) */ +export function updateEmployeeFile(id, data) { + return request({ url: `${BASE}/update/${id}`, method: 'post', data }); +} + +/** 删除档案(软删除) */ +export function deleteEmployeeFile(id) { + return request({ url: `${BASE}/delete/${id}`, method: 'delete' }); +} + +/** 已建档员工ID列表,用于过滤出可建档(未建档)员工 */ +export function getFiledEmployeeIds() { + return request({ url: `${BASE}/filedEmployees`, method: 'get' }); +} + +/* -------------------------------- 子记录 -------------------------------- */ + +/** 新增子记录,type: 1-教育经历 2-工作经历 3-合同信息 4-证照附件 */ +export function createFileRecord(data) { + return request({ url: `${BASE}/record/create`, method: 'post', data }); +} + +/** 更新子记录 */ +export function updateFileRecord(id, data) { + return request({ url: `${BASE}/record/update/${id}`, method: 'post', data }); +} + +/** 删除子记录(软删除) */ +export function deleteFileRecord(id) { + return request({ url: `${BASE}/record/delete/${id}`, method: 'delete' }); +} diff --git a/backend/src/router/index.js b/backend/src/router/index.js index 06535b4..4408225 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -110,6 +110,12 @@ const staticMainChildren = [ 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", diff --git a/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue b/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue new file mode 100644 index 0000000..a8c0019 --- /dev/null +++ b/backend/src/views/apps/oa/employeefile/components/fileDetailDrawer.vue @@ -0,0 +1,547 @@ + + + + + + + + {{ file.employee_name || '-' }} + + {{ statusMeta(file.employment_status).text }} + + {{ file.file_no || '-' }} + + + {{ file.department_name || '-' }} · {{ file.position || '-' }} + · 工号 {{ file.employee_account }} + · {{ file.phone }} + + + + + 编辑资料 + + + + + + + 员工信息 + + {{ file.employee_name || '-' }} + {{ genderText(file) }} + {{ file.birthday || '-' }} + {{ file.education || '-' }} + {{ file.nation || '-' }} + {{ file.phone || '-' }} + {{ file.email || '-' }} + + {{ file.affiliate_unit_name || '-' }} + + {{ file.department_name || '-' }} + + {{ file.home_address || '-' }} + + + + {{ employeeStatusText(file) }} + + + + + 档案信息 + + {{ file.id_card || '-' }} + {{ file.political_status || '-' }} + {{ maritalText(file.marital_status) }} + {{ file.native_place || '-' }} + {{ file.work_email || '-' }} + {{ file.hire_date || '-' }} + {{ file.regular_date || '-' }} + {{ file.leave_date || '-' }} + + + {{ file.emergency_contact }}{{ file.emergency_phone ? `(${file.emergency_phone})` : '' }} + + - + + + {{ file.household_address || '-' }} + + {{ file.current_address || '-' }} + {{ file.remark || '-' }} + + + 证照资料 + + + + + + + + + + + PDF 附件 + + + 未上传 + {{ cert.label }} + + + + + + + + + 新增{{ conf.label }} + + + + + + {{ row.sub_title || '-' }} + + + {{ row.extra || '-' }} + + + {{ dateRangeText(row) }} + + + + {{ contractStatus(row).text }} + + + + {{ row.description || '-' }} + + + + + 编辑 + + 删除 + + + + + + + + + + + + + 上传证照 + + + + + + + + + PDF 附件 + + + {{ item.title }} + {{ item.sub_title || ' ' }} + + 预览 + + 编辑 + + 删除 + + + + + + + + + + + + + + + diff --git a/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue b/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue new file mode 100644 index 0000000..c2f5f0b --- /dev/null +++ b/backend/src/views/apps/oa/employeefile/components/fileEditDialog.vue @@ -0,0 +1,428 @@ + + + + + + + + + + + + + + + 个人信息 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 任职信息 + + + 试用 + 正式 + 离职 + + + + + + + + + + + + + 证照资料 + + + + + + + 上传{{ cert.label }} + + + + 移除 + + + + + 紧急联系人 + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + + + diff --git a/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue b/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue new file mode 100644 index 0000000..91a4883 --- /dev/null +++ b/backend/src/views/apps/oa/employeefile/components/recordEditDialog.vue @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 上传证照 + + + + 移除附件 + + 支持图片 / PDF,大小不超过 10MB + + + + + + 取消 + 保存 + + + + + + + diff --git a/backend/src/views/apps/oa/employeefile/index.vue b/backend/src/views/apps/oa/employeefile/index.vue new file mode 100644 index 0000000..a222a86 --- /dev/null +++ b/backend/src/views/apps/oa/employeefile/index.vue @@ -0,0 +1,348 @@ + + + + + 员工档案 + 一人一档,集中管理人事资料、教育经历、工作经历、合同与证照 + + + 刷新 + 新建档案 + + + + + + + {{ stats[item.key] ?? 0 }} + {{ item.label }} + + + + + + + + + + + + + 重置 + + + + + {{ row.file_no || '-' }} + + + + {{ row.employee_name || '-' }} + + + + {{ genderText(row) }} + + + {{ row.department_name || '-' }} + + + {{ row.position || '-' }} + + + {{ row.phone || '-' }} + + + {{ row.hire_date || '-' }} + + + + + {{ statusMeta(row.employment_status).text }} + + + + + + 查看档案 + 编辑 + 删除 + + + + + + + + + + + + + + editDialogRef?.open(file)" /> + + + + + + diff --git a/go/controllers/backend_employee_file.go b/go/controllers/backend_employee_file.go new file mode 100644 index 0000000..38e13f8 --- /dev/null +++ b/go/controllers/backend_employee_file.go @@ -0,0 +1,846 @@ +package controllers + +import ( + "encoding/json" + "io" + "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 将 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] + } + return s +} + +// 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"` +} + +// 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.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{} + } + 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}) +} + +// 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") + } + + 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: strings.TrimSpace(payload.HireDate), + RegularDate: strings.TrimSpace(payload.RegularDate), + LeaveDate: strings.TrimSpace(payload.LeaveDate), + EmploymentStatus: payload.EmploymentStatus, + Remark: payload.Remark, + IsDeleted: 0, + CreateTime: now, + UpdateTime: &now, + } + if _, err := models.Orm.Insert(item); err != nil { + 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 + item.IDCardFront = payload.IDCardFront + item.IDCardBack = payload.IDCardBack + item.EducationPhoto = payload.EducationPhoto + 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 = strings.TrimSpace(payload.HireDate) + item.RegularDate = strings.TrimSpace(payload.RegularDate) + item.LeaveDate = strings.TrimSpace(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", + "HouseholdAddress", "CurrentAddress", "EmergencyContact", "EmergencyPhone", + "EmergencyRelation", "WorkEmail", "HireDate", "RegularDate", "LeaveDate", + "EmploymentStatus", "Remark", "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: payload.StartDate, + EndDate: 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 = payload.StartDate + item.EndDate = 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) +} diff --git a/go/models/backend_employee_file.go b/go/models/backend_employee_file.go new file mode 100644 index 0000000..c958d63 --- /dev/null +++ b/go/models/backend_employee_file.go @@ -0,0 +1,67 @@ +package models + +import "time" + +// BackendEmployeeFile 员工档案主表: yz_backend_employee_file +// 档案与员工(yz_backend_employee)按 tid + employee_id 一对一, +// 供 OA 员工档案页面管理人事资料。日期字段与 OA 日程一致, +// 以 "YYYY-MM-DD" 字符串存取,规避 parseTime 的时区换算问题。 +type BackendEmployeeFile struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid int `orm:"column(tid)" json:"tid"` + EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"` + FileNo string `orm:"column(file_no);size(50);default()" json:"file_no"` + IDCard string `orm:"column(id_card);size(30);default()" json:"id_card"` + IDCardFront string `orm:"column(id_card_front);size(500);default()" json:"id_card_front"` + IDCardBack string `orm:"column(id_card_back);size(500);default()" json:"id_card_back"` + EducationPhoto string `orm:"column(education_photo);size(500);default()" json:"education_photo"` + PoliticalStatus string `orm:"column(political_status);size(30);default()" json:"political_status"` + MaritalStatus int8 `orm:"column(marital_status);default(0)" json:"marital_status"` + NativePlace string `orm:"column(native_place);size(100);default()" json:"native_place"` + HouseholdAddress string `orm:"column(household_address);size(255);default()" json:"household_address"` + CurrentAddress string `orm:"column(current_address);size(255);default()" json:"current_address"` + EmergencyContact string `orm:"column(emergency_contact);size(50);default()" json:"emergency_contact"` + 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"` + 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"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *BackendEmployeeFile) TableName() string { + return "yz_backend_employee_file" +} + +// BackendEmployeeFileRecord 员工档案子记录表: yz_backend_employee_file_record +// 用 type 区分四类记录:1-教育经历 2-工作经历 3-合同信息 4-证照附件。 +// title/sub_title/extra 的语义随类型变化(见建表脚本注释), +// 避免为每类记录单独建表。 +type BackendEmployeeFileRecord struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid int `orm:"column(tid)" json:"tid"` + EmployeeID uint64 `orm:"column(employee_id)" json:"employee_id"` + Type int8 `orm:"column(type);default(1)" json:"type"` + 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"` + 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"` + 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);auto_now;type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *BackendEmployeeFileRecord) TableName() string { + return "yz_backend_employee_file_record" +} diff --git a/go/models/init.go b/go/models/init.go index a937b21..bda9a68 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -101,6 +101,8 @@ func Init(_ string) { new(BackendScheduleReminderSendLog), new(OaSchedule), + new(BackendEmployeeFile), + new(BackendEmployeeFileRecord), ) // 创建全局 Ormer diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 3d83e67..281ee6c 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -102,6 +102,18 @@ func RegisterAuthRoutes() { registerOrganizationRoutes("erp") registerOrganizationRoutes("oa") + // 员工档案(OA 人事档案:一人一档 + 教育/工作/合同/证照子记录) + beego.Router("/backend/oa/employeefile/list", &controllers.BackendEmployeeFileController{}, "get:List") + beego.Router("/backend/oa/employeefile/stats", &controllers.BackendEmployeeFileController{}, "get:Stats") + 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/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") + beego.Router("/backend/oa/employeefile/record/update/:id", &controllers.BackendEmployeeFileController{}, "post:UpdateRecord") + beego.Router("/backend/oa/employeefile/record/delete/:id", &controllers.BackendEmployeeFileController{}, "delete:DeleteRecord") + // 通讯录管理(与组织架构联动) beego.Router("/backend/erp/contact/list", &controllers.BackendErpContactController{}, "get:List") beego.Router("/backend/erp/contact/detail/:id", &controllers.BackendErpContactController{}, "get:Detail") diff --git a/sql/alter_employee_file_add_cert_photos.sql b/sql/alter_employee_file_add_cert_photos.sql new file mode 100644 index 0000000..6a35875 --- /dev/null +++ b/sql/alter_employee_file_add_cert_photos.sql @@ -0,0 +1,8 @@ +-- 员工档案主表增加证照字段:身份证正反面、学历照片。 +-- 仅在已执行过 yz_backend_employee_file.sql 建表时执行本脚本; +-- 未建表的库直接执行最新版 yz_backend_employee_file.sql 即可,无需本脚本。 + +ALTER TABLE `yz_backend_employee_file` + ADD COLUMN `id_card_front` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证正面(人像面)图片URL' AFTER `id_card`, + ADD COLUMN `id_card_back` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证反面(国徽面)图片URL' AFTER `id_card_front`, + ADD COLUMN `education_photo` varchar(500) NOT NULL DEFAULT '' COMMENT '学历照片URL' AFTER `id_card_back`; diff --git a/sql/seed_employee_file_menu.sql b/sql/seed_employee_file_menu.sql new file mode 100644 index 0000000..75544dd --- /dev/null +++ b/sql/seed_employee_file_menu.sql @@ -0,0 +1,27 @@ +-- 员工档案菜单(租户端):挂在办公自动化模块目录下。 +-- +-- 前置:yz_system_menu 中已存在 path 为 /apps/oa 的模块目录菜单; +-- 若不存在,脚本会自动创建。 +-- views 字段:[2] 租户端。type 字段: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/employeefile', '/apps/oa/employeefile/index.vue', 'Postcard', 4, 1, 1, '[2]', 2, '一人一档的人事档案管理' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/employeefile' +); + +-- 修正已有菜单的组件路径(历史数据可能指向旧组件) +UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/employeefile/index.vue', `type` = 2, `views` = '[2]' + WHERE `path` = '/apps/oa/employeefile'; diff --git a/sql/yz_backend_employee_file.sql b/sql/yz_backend_employee_file.sql new file mode 100644 index 0000000..0ab6b44 --- /dev/null +++ b/sql/yz_backend_employee_file.sql @@ -0,0 +1,70 @@ +-- 员工档案表(租户端 OA 人事档案) +-- 在租户业务库执行以下语句创建表结构。 +-- 数据按租户 tid 隔离;档案与员工(yz_backend_employee)一对一, +-- 子记录表统一存放教育经历 / 工作经历 / 合同信息 / 证照附件。 + +-- --------------------------------------------------------------------------- +-- 1. 员工档案主表 +-- --------------------------------------------------------------------------- + +CREATE TABLE `yz_backend_employee_file` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', + `employee_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '员工ID(yz_backend_employee.id)', + `file_no` varchar(50) NOT NULL DEFAULT '' COMMENT '档案编号(留空自动生成EF+时间戳)', + `id_card` varchar(30) NOT NULL DEFAULT '' COMMENT '身份证号', + `id_card_front` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证正面(人像面)图片URL', + `id_card_back` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证反面(国徽面)图片URL', + `education_photo` varchar(500) NOT NULL DEFAULT '' COMMENT '学历照片URL', + `political_status` varchar(30) NOT NULL DEFAULT '' COMMENT '政治面貌', + `marital_status` tinyint NOT NULL DEFAULT '0' COMMENT '婚姻状况 0-未知 1-未婚 2-已婚 3-离异 4-丧偶', + `native_place` varchar(100) NOT NULL DEFAULT '' COMMENT '籍贯', + `household_address` varchar(255) NOT NULL DEFAULT '' COMMENT '户籍地址', + `current_address` varchar(255) NOT NULL DEFAULT '' COMMENT '现居住址', + `emergency_contact` varchar(50) NOT NULL DEFAULT '' COMMENT '紧急联系人', + `emergency_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '紧急联系电话', + `emergency_relation` varchar(30) NOT NULL DEFAULT '' COMMENT '与本人关系', + `work_email` varchar(100) NOT NULL DEFAULT '' COMMENT '工作邮箱', + `hire_date` date DEFAULT NULL COMMENT '入职日期', + `regular_date` date DEFAULT NULL COMMENT '转正日期', + `leave_date` date DEFAULT NULL COMMENT '离职日期', + `employment_status` tinyint NOT NULL DEFAULT '2' COMMENT '在职状态 1-试用 2-正式 3-离职', + `remark` text NULL COMMENT '备注', + `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_tid_employee` (`tid`,`employee_id`), + KEY `idx_file_no` (`file_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='员工档案主表'; + +-- --------------------------------------------------------------------------- +-- 2. 员工档案子记录表(教育经历/工作经历/合同信息/证照附件) +-- type 字段区分记录类型,title/sub_title/extra 的语义随类型变化: +-- 教育经历(1):title=学校 sub_title=专业 extra=学历 +-- 工作经历(2):title=公司 sub_title=职位 extra=所在部门 +-- 合同信息(3):title=合同名称 sub_title=合同类型 extra=签订主体 +-- 证照附件(4):title=证照名称 sub_title=证件号码 extra='' +-- --------------------------------------------------------------------------- + +CREATE TABLE `yz_backend_employee_file_record` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', + `employee_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '员工ID(yz_backend_employee.id)', + `type` tinyint NOT NULL DEFAULT '1' COMMENT '记录类型 1-教育经历 2-工作经历 3-合同信息 4-证照附件', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '名称(学校/公司/合同/证照)', + `sub_title` varchar(100) NOT NULL DEFAULT '' COMMENT '次级信息(专业/职位/合同类型/证件号码)', + `extra` varchar(100) NOT NULL DEFAULT '' COMMENT '附加信息(学历/部门/签订主体)', + `start_date` date DEFAULT NULL COMMENT '开始日期', + `end_date` date DEFAULT NULL COMMENT '结束日期', + `description` text NULL COMMENT '描述', + `attachment_url` varchar(500) NOT NULL DEFAULT '' COMMENT '附件地址(证照图片等)', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序(越小越靠前)', + `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + KEY `idx_tid_employee_type` (`tid`,`employee_id`,`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='员工档案子记录表'; diff --git a/test-output.txt b/test-output.txt deleted file mode 100644 index 59be8fc..0000000 --- a/test-output.txt +++ /dev/null @@ -1,24 +0,0 @@ -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/controllers [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/middleware [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/models [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/jwtutil [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/tokenprobe [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/api [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/app [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/backend [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/index [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/routers/platform [setup failed] -FAIL _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/services [setup failed] -FAIL backend [setup failed] -FAIL docs [setup failed] -FAIL frontend [setup failed] -FAIL go [setup failed] -FAIL platform [setup failed] -FAIL sql [setup failed] -FAIL uniapp [setup failed] -? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/passwordutil [no test files] -? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/pkg/versionutil [no test files] -? _/E_/Demos/DemoOwns/Go/yunzerwebsiteallinone/go/version [no test files] -FAIL
一人一档,集中管理人事资料、教育经历、工作经历、合同与证照