package controllers import ( "fmt" "strconv" "strings" "server/models" "server/pkg/jwtutil" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // BackendOrganizationController 组织架构(组织、员工、职位)接口。 // // 组织架构是租户端的通用基础数据:进销存(ERP)与办公自动化(OA)两个模块各自有独立界面, // 但读写的是同一份数据。所有查询与写入都强制带上 JWT 中的租户ID(tid),实现租户间隔离, // 前端传入的 tid / tenant_id 一律忽略,避免越权读取其它租户的数据。 type BackendOrganizationController struct { beego.Controller } const orgSettingsCodePrefix = "backend_org_settings" type orgSettings struct { OrgCodePrefix string `json:"org_code_prefix"` EmployeeCodePrefix string `json:"employee_code_prefix"` PositionCodePrefix string `json:"position_code_prefix"` AutoGenerateCodes bool `json:"auto_generate_codes"` CodeLength int `json:"code_length"` DefaultOrgType int `json:"default_org_type"` DefaultSort int `json:"default_sort"` DefaultStatus int `json:"default_status"` MaxOrgLevels int `json:"max_org_levels"` MaxOrgChildren int `json:"max_org_children"` AllowDuplicateCode bool `json:"allow_duplicate_codes"` BatchOperations bool `json:"batch_operations"` ExportEnabled bool `json:"export_enabled"` ImportEnabled bool `json:"import_enabled"` } func defaultOrgSettings() orgSettings { return orgSettings{ OrgCodePrefix: "ORG", EmployeeCodePrefix: "EMP", PositionCodePrefix: "POS", AutoGenerateCodes: true, CodeLength: 8, DefaultOrgType: 0, DefaultSort: 0, DefaultStatus: 1, MaxOrgLevels: 10, MaxOrgChildren: 50, AllowDuplicateCode: false, BatchOperations: true, ExportEnabled: true, ImportEnabled: true, } } type organizationDTO struct { ID uint64 `json:"id"` Tid uint64 `json:"tid"` TenantID uint64 `json:"tenant_id"` OrgName string `json:"org_name"` OrgCode string `json:"org_code"` ParentID uint64 `json:"parent_id"` ParentName string `json:"parent_name"` LeaderID uint64 `json:"leader_id"` LeaderName string `json:"leader_name"` IsCompany int `json:"is_company"` Sort uint `json:"sort"` Status int8 `json:"status"` Remark string `json:"remark"` CreateTime string `json:"create_time"` UpdateTime string `json:"update_time"` } type employeeDTO struct { ID uint `json:"id"` Tid int `json:"tid"` TenantID int `json:"tenant_id"` Account string `json:"account"` Name string `json:"name"` Gender int8 `json:"gender"` Sex int8 `json:"sex"` 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"` Education string `json:"education"` Nation string `json:"nation"` Phone string `json:"phone"` Wechat string `json:"wechat"` Email string `json:"email"` HomeAddress string `json:"home_address"` AccountStatus int8 `json:"account_status"` Status int8 `json:"status"` CreateTime string `json:"create_time"` } type positionDTO struct { ID uint64 `json:"id"` Tid uint64 `json:"tid"` TenantID uint64 `json:"tenant_id"` DepartmentID uint64 `json:"department_id"` DepartmentName string `json:"department_name"` PositionCode string `json:"position_code"` PositionName string `json:"position_name"` PositionType int8 `json:"position_type"` Status int8 `json:"status"` Sort uint `json:"sort"` Remark string `json:"remark"` CreateTime string `json:"create_time"` } // --------------------------------------------------------------------------- // 认证与响应 // --------------------------------------------------------------------------- // claims 解析 Authorization 头中的 JWT,要求是租户端(backend)用户且带有效租户ID。 func (c *BackendOrganizationController) claims() (*jwtutil.Claims, bool) { auth := strings.TrimSpace(c.Ctx.Request.Header.Get("Authorization")) parts := strings.SplitN(auth, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, false } claims, err := jwtutil.ParseToken(parts[1]) if err != nil || claims.UserType != "backend" || claims.TenantId <= 0 { return nil, false } return claims, true } // tenantID 取当前登录租户ID,未通过认证时直接输出 401 并返回 false。 func (c *BackendOrganizationController) tenantID() (uint64, bool) { claims, ok := c.claims() if !ok { c.jsonError(401, "未登录或登录已过期") return 0, false } return uint64(claims.TenantId), true } func (c *BackendOrganizationController) jsonOK(data interface{}) { resp := map[string]interface{}{"code": 200, "msg": "success"} if data != nil { resp["data"] = data } c.Data["json"] = resp _ = c.ServeJSON() } func (c *BackendOrganizationController) jsonError(code int, msg string) { if code == 401 { c.Ctx.Output.SetStatus(401) } c.Data["json"] = map[string]interface{}{"code": code, "msg": msg} _ = c.ServeJSON() } // --------------------------------------------------------------------------- // 查询基座:所有 QuerySeter 都强制附加 tid 过滤 // --------------------------------------------------------------------------- func (c *BackendOrganizationController) orgQuery(tid uint64) orm.QuerySeter { return models.Orm.QueryTable(new(models.BackendOrganization)). Filter("tid", tid). Filter("delete_time__isnull", true) } func (c *BackendOrganizationController) employeeQuery(tid uint64) orm.QuerySeter { return models.Orm.QueryTable(new(models.BackendEmployee)). Filter("tid", tid). Filter("delete_time__isnull", true) } func (c *BackendOrganizationController) positionQuery(tid uint64) orm.QuerySeter { return models.Orm.QueryTable(new(models.BackendPosition)). Filter("tid", tid). Filter("delete_time__isnull", true) } // --------------------------------------------------------------------------- // 组织机构 // --------------------------------------------------------------------------- // GetOrganization 获取组织机构列表(扁平)。 // GET /backend/{erp|oa}/getOrganization func (c *BackendOrganizationController) GetOrganization() { tid, ok := c.tenantID() if !ok { return } qs := c.orgQuery(tid).Exclude("status", 0) if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" { qs = qs.Filter("org_name__icontains", keyword) } var rows []models.BackendOrganization if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil { c.jsonError(500, "查询组织机构失败: "+err.Error()) return } c.jsonOK(c.organizationDTOList(tid, rows)) } // GetOrganizationDetail 获取组织机构详情。 // GET /backend/{erp|oa}/getOrganizationDetail/:id func (c *BackendOrganizationController) GetOrganizationDetail() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint64(":id") if !valid { c.jsonError(400, "无效ID") return } var row models.BackendOrganization if err := c.orgQuery(tid).Filter("id", id).Exclude("status", 0).One(&row); err != nil { c.jsonError(404, "组织机构不存在") return } c.jsonOK(c.organizationDTO(tid, row)) } // CreateOrganization 创建组织机构。 // POST /backend/{erp|oa}/createOrganization func (c *BackendOrganizationController) CreateOrganization() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() settings := c.loadOrgSettings(tid) orgName, _ := c.getStringValue(body, "org_name", "name") orgName = strings.TrimSpace(orgName) if orgName == "" { c.jsonError(400, "组织名称不能为空") return } parentID, _ := c.getUint64Value(body, "parent_id") if parentID > 0 { if !c.orgExists(tid, parentID) { c.jsonError(400, "上级组织不存在") return } depth, err := c.orgDepth(tid, parentID) if err != nil { c.jsonError(500, "校验组织层级失败: "+err.Error()) return } if settings.MaxOrgLevels > 0 && depth+1 > settings.MaxOrgLevels { c.jsonError(400, fmt.Sprintf("组织层级最多 %d 级", settings.MaxOrgLevels)) return } if settings.MaxOrgChildren > 0 { count, err := c.orgQuery(tid).Filter("parent_id", parentID).Exclude("status", 0).Count() if err == nil && int(count) >= settings.MaxOrgChildren { c.jsonError(400, fmt.Sprintf("同一上级下最多 %d 个子组织", settings.MaxOrgChildren)) return } } } orgCode, _ := c.getStringValue(body, "org_code", "code") orgCode = strings.TrimSpace(orgCode) if orgCode == "" { if !settings.AutoGenerateCodes { c.jsonError(400, "组织编码不能为空") return } orgCode = c.generateCode(settings.OrgCodePrefix, settings.CodeLength) } if !settings.AllowDuplicateCode { count, err := c.orgQuery(tid).Filter("org_code", orgCode).Count() if err != nil { c.jsonError(500, "校验组织编码失败: "+err.Error()) return } if count > 0 { c.jsonError(400, "组织编码已存在") return } } leaderID, hasLeader := c.getUint64Value(body, "leader_id") sortVal, hasSort := c.getUintValue(body, "sort") isCompany, hasCompany := c.getIntValue(body, "is_company") status, hasStatus := c.getIntValue(body, "status") remark, _ := c.getStringValue(body, "remark") row := models.BackendOrganization{ Tid: tid, OrgName: orgName, OrgCode: orgCode, ParentID: parentID, Sort: uint(settings.DefaultSort), IsCompany: boolInt(parentID == 0), Status: int8(settings.DefaultStatus), Remark: strPtrIfNotEmpty(remark), } if hasSort { row.Sort = sortVal } if hasLeader && leaderID > 0 { row.LeaderID = &leaderID } if hasCompany { row.IsCompany = isCompany } if hasStatus { row.Status = int8(status) } id, err := models.Orm.Insert(&row) if err != nil { c.jsonError(500, "创建组织机构失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{"id": id, "org_code": orgCode}) } // EditOrganization 更新组织机构。 // POST /backend/{erp|oa}/editOrganization/:id func (c *BackendOrganizationController) EditOrganization() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint64(":id") if !valid { c.jsonError(400, "无效ID") return } if !c.orgExists(tid, id) { c.jsonError(404, "组织机构不存在") return } body := c.parseJSONBody() settings := c.loadOrgSettings(tid) update := orm.Params{} if v, has := c.getStringValue(body, "org_name", "name"); has { v = strings.TrimSpace(v) if v == "" { c.jsonError(400, "组织名称不能为空") return } update["org_name"] = v } if v, has := c.getStringValue(body, "org_code", "code"); has { v = strings.TrimSpace(v) if v == "" { c.jsonError(400, "组织编码不能为空") return } if !settings.AllowDuplicateCode { count, err := c.orgQuery(tid).Filter("org_code", v).Exclude("id", id).Count() if err != nil { c.jsonError(500, "校验组织编码失败: "+err.Error()) return } if count > 0 { c.jsonError(400, "组织编码已存在") return } } update["org_code"] = v } if v, has := c.getUint64Value(body, "parent_id"); has { if err := c.validateParentChange(tid, id, v, settings); err != nil { c.jsonError(400, err.Error()) return } update["parent_id"] = v } if v, has := c.getUint64Value(body, "leader_id"); has { update["leader_id"] = nullableUint64(v) } if v, has := c.getUintValue(body, "sort"); has { update["sort"] = v } if v, has := c.getIntValue(body, "is_company"); has { update["is_company"] = v } if v, has := c.getIntValue(body, "status"); has { update["status"] = int8(v) } if v, has := c.getStringValue(body, "remark"); has { update["remark"] = nullableString(v) } if len(update) == 0 { c.jsonError(400, "无更新字段") return } if _, err := c.orgQuery(tid).Filter("id", id).Update(update); err != nil { c.jsonError(500, "更新组织机构失败: "+err.Error()) return } c.jsonOK(nil) } // DeleteOrganization 删除组织机构(软删除)。 // DELETE /backend/{erp|oa}/deleteOrganization/:id func (c *BackendOrganizationController) DeleteOrganization() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint64(":id") if !valid { c.jsonError(400, "无效ID") return } childCount, err := c.orgQuery(tid).Filter("parent_id", id).Exclude("status", 0).Count() if err != nil { c.jsonError(500, "检查子组织失败: "+err.Error()) return } if childCount > 0 { c.jsonError(400, "请先删除下级组织") return } employeeCount, err := c.employeeQuery(tid).Filter("department", strconv.FormatUint(id, 10)).Count() if err != nil { c.jsonError(500, "检查组织员工失败: "+err.Error()) return } if employeeCount > 0 { c.jsonError(400, "该组织下还有员工,请先调岗或删除员工") return } num, err := c.orgQuery(tid).Filter("id", id). Update(orm.Params{"delete_time": c.nowString(), "status": int8(0)}) if err != nil { c.jsonError(500, "删除组织机构失败: "+err.Error()) return } if num == 0 { c.jsonError(404, "组织机构不存在") return } c.jsonOK(nil) } // GetCompanys 获取企业单位列表。 // GET /backend/{erp|oa}/getCompanys func (c *BackendOrganizationController) GetCompanys() { tid, ok := c.tenantID() if !ok { return } var rows []models.BackendOrganization _, err := c.orgQuery(tid).Exclude("status", 0).Filter("is_company", 1). OrderBy("sort", "id").All(&rows) if err != nil { c.jsonError(500, "查询企业单位失败: "+err.Error()) return } c.jsonOK(c.organizationDTOList(tid, rows)) } // GetDepartments 获取部门列表。 // GET /backend/{erp|oa}/getDepartments?parent_id=1 func (c *BackendOrganizationController) GetDepartments() { tid, ok := c.tenantID() if !ok { return } parentID, _ := c.GetUint64("parent_id") qs := c.orgQuery(tid).Exclude("status", 0) if parentID > 0 { qs = qs.Filter("parent_id", parentID) } else { qs = qs.Filter("is_company", 0) } var rows []models.BackendOrganization if _, err := qs.OrderBy("sort", "id").All(&rows); err != nil { c.jsonError(500, "查询部门失败: "+err.Error()) return } c.jsonOK(c.organizationDTOList(tid, rows)) } // GetOrganizationTree 获取组织架构树。 // GET /backend/{erp|oa}/getOrganizationTree func (c *BackendOrganizationController) GetOrganizationTree() { tid, ok := c.tenantID() if !ok { return } var rows []models.BackendOrganization _, err := c.orgQuery(tid).Exclude("status", 0).OrderBy("sort", "id").All(&rows) if err != nil { c.jsonError(500, "查询组织机构失败: "+err.Error()) return } c.jsonOK(buildOrganizationTree(c.organizationDTOList(tid, rows))) } // SearchOrganizations 按名称/编码搜索组织机构。 // GET /backend/{erp|oa}/searchOrganizations?keyword=xx func (c *BackendOrganizationController) SearchOrganizations() { tid, ok := c.tenantID() if !ok { return } keyword := strings.TrimSpace(c.GetString("keyword")) cond := orm.NewCondition(). And("tid", tid). And("delete_time__isnull", true). AndNot("status", 0) if keyword != "" { cond = cond.AndCond(orm.NewCondition(). Or("org_name__icontains", keyword). Or("org_code__icontains", keyword)) } var rows []models.BackendOrganization _, err := models.Orm.QueryTable(new(models.BackendOrganization)). SetCond(cond).OrderBy("sort", "id").All(&rows) if err != nil { c.jsonError(500, "查询组织机构失败: "+err.Error()) return } c.jsonOK(c.organizationDTOList(tid, rows)) } // GetOrganizationHierarchy 获取指定组织的上级链与直接下级。 // GET /backend/{erp|oa}/getOrganizationHierarchy/:org_id func (c *BackendOrganizationController) GetOrganizationHierarchy() { tid, ok := c.tenantID() if !ok { return } orgID, valid := c.pathUint64(":org_id") if !valid { c.jsonError(400, "无效组织ID") return } var org models.BackendOrganization if err := c.orgQuery(tid).Filter("id", orgID).One(&org); err != nil { c.jsonError(404, "组织不存在") return } var children []models.BackendOrganization if _, err := c.orgQuery(tid).Filter("parent_id", orgID).Exclude("status", 0). OrderBy("sort", "id").All(&children); err != nil { c.jsonError(500, "查询子组织失败: "+err.Error()) return } // 自底向上收集上级链,最多回溯 64 层,防止脏数据造成死循环 ancestors := make([]organizationDTO, 0) parentID := org.ParentID for i := 0; i < 64 && parentID > 0; i++ { var parent models.BackendOrganization if err := c.orgQuery(tid).Filter("id", parentID).One(&parent); err != nil { break } ancestors = append([]organizationDTO{c.organizationDTO(tid, parent)}, ancestors...) parentID = parent.ParentID } c.jsonOK(map[string]interface{}{ "current": c.organizationDTO(tid, org), "ancestors": ancestors, "children": c.organizationDTOList(tid, children), }) } // GetOrganizationStats 组织架构统计。 // GET /backend/{erp|oa}/getOrganizationStats func (c *BackendOrganizationController) GetOrganizationStats() { tid, ok := c.tenantID() if !ok { return } var rows []models.BackendOrganization if _, err := c.orgQuery(tid).All(&rows); err != nil { c.jsonError(500, "统计组织架构失败: "+err.Error()) return } childrenOf := map[uint64][]uint64{} for _, row := range rows { childrenOf[row.ParentID] = append(childrenOf[row.ParentID], row.ID) } companyCount, departmentCount, enabledCount := 0, 0, 0 for _, row := range rows { if row.IsCompany == 1 { companyCount++ } else { departmentCount++ } if row.Status == 1 { enabledCount++ } } employeeTotal, _ := c.employeeQuery(tid).Count() employeeActive, _ := c.employeeQuery(tid).Filter("account_status", 1).Count() positionTotal, _ := c.positionQuery(tid).Count() c.jsonOK(map[string]interface{}{ "org_total": len(rows), "company_count": companyCount, "department_count": departmentCount, "enabled_count": enabledCount, "disabled_count": len(rows) - enabledCount, "max_depth": treeDepth(childrenOf, 0, 0), "employee_total": employeeTotal, "employee_active": employeeActive, "position_total": positionTotal, }) } // MoveOrganization 移动组织节点到新的上级(拖拽调整层级)。 // POST /backend/{erp|oa}/moveOrganization func (c *BackendOrganizationController) MoveOrganization() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() orgID, hasOrg := c.getUint64Value(body, "org_id", "id") if !hasOrg || orgID == 0 { c.jsonError(400, "组织ID不能为空") return } parentID, _ := c.getUint64Value(body, "parent_id") if !c.orgExists(tid, orgID) { c.jsonError(404, "组织不存在") return } settings := c.loadOrgSettings(tid) if err := c.validateParentChange(tid, orgID, parentID, settings); err != nil { c.jsonError(400, err.Error()) return } update := orm.Params{"parent_id": parentID, "is_company": boolInt(parentID == 0)} if v, has := c.getUintValue(body, "sort"); has { update["sort"] = v } if _, err := c.orgQuery(tid).Filter("id", orgID).Update(update); err != nil { c.jsonError(500, "移动组织失败: "+err.Error()) return } c.jsonOK(nil) } // BatchOrganizeOrganizations 批量启用/禁用/删除组织。 // POST /backend/{erp|oa}/batchOrganizeOrganizations func (c *BackendOrganizationController) BatchOrganizeOrganizations() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() ids := c.getUint64Slice(body, "ids") if len(ids) == 0 { c.jsonError(400, "请选择要操作的组织") return } action, _ := c.getStringValue(body, "action") action = strings.ToLower(strings.TrimSpace(action)) var update orm.Params switch action { case "enable": update = orm.Params{"status": int8(1)} case "disable": update = orm.Params{"status": int8(0)} case "delete": // 有下级或有员工的组织不允许批量删除,避免产生孤儿数据 for _, id := range ids { childCount, _ := c.orgQuery(tid).Filter("parent_id", id).Exclude("status", 0).Count() if childCount > 0 { c.jsonError(400, "选中组织存在下级组织,无法批量删除") return } employeeCount, _ := c.employeeQuery(tid).Filter("department", strconv.FormatUint(id, 10)).Count() if employeeCount > 0 { c.jsonError(400, "选中组织下仍有员工,无法批量删除") return } } update = orm.Params{"delete_time": c.nowString(), "status": int8(0)} default: c.jsonError(400, "不支持的操作类型") return } num, err := c.orgQuery(tid).Filter("id__in", ids).Update(update) if err != nil { c.jsonError(500, "批量操作失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{"affected": num}) } // CheckOrgCodeUnique 检查组织编码唯一性。 // GET /backend/{erp|oa}/checkOrgCodeUnique?org_code=xx&exclude_id=1 func (c *BackendOrganizationController) CheckOrgCodeUnique() { tid, ok := c.tenantID() if !ok { return } orgCode := strings.TrimSpace(c.GetString("org_code")) if orgCode == "" { c.jsonError(400, "组织编码不能为空") return } excludeID, _ := c.GetUint64("exclude_id") if excludeID == 0 { excludeID, _ = c.GetUint64("org_id") } qs := c.orgQuery(tid).Filter("org_code", orgCode) if excludeID > 0 { qs = qs.Exclude("id", excludeID) } count, err := qs.Count() if err != nil { c.jsonError(500, "检查编码唯一性失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{"unique": count == 0, "message": uniqueMessage(count == 0, "编码")}) } // --------------------------------------------------------------------------- // 员工 // --------------------------------------------------------------------------- // GetEmployee 获取员工列表,支持按组织、关键词、状态过滤。 // GET /backend/{erp|oa}/getEmployee?org_id=1&keyword=xx&status=1 func (c *BackendOrganizationController) GetEmployee() { tid, ok := c.tenantID() if !ok { return } cond := orm.NewCondition().And("tid", tid).And("delete_time__isnull", true) orgID, _ := c.GetUint64("org_id") if orgID == 0 { orgID, _ = c.GetUint64("department_id") } if orgID > 0 { // 含下级组织:把整棵子树的组织ID都算进去 orgIDs := c.collectOrgIDs(tid, orgID) values := make([]string, 0, len(orgIDs)) for _, id := range orgIDs { values = append(values, strconv.FormatUint(id, 10)) } cond = cond.And("department__in", values) } if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" { cond = cond.AndCond(orm.NewCondition(). Or("name__icontains", keyword). Or("account__icontains", keyword). Or("phone__icontains", keyword)) } if raw := strings.TrimSpace(c.GetString("status")); raw != "" { if status, err := strconv.Atoi(raw); err == nil { cond = cond.And("account_status", int8(status)) } } var rows []models.BackendEmployee _, err := models.Orm.QueryTable(new(models.BackendEmployee)). SetCond(cond).OrderBy("-id").All(&rows) if err != nil { c.jsonError(500, "查询员工失败: "+err.Error()) return } c.jsonOK(c.employeeDTOList(tid, rows)) } // GetOrganizationEmployees 获取指定组织(含下级)的员工列表。 // GET /backend/{erp|oa}/getOrganizationEmployees/:org_id func (c *BackendOrganizationController) GetOrganizationEmployees() { tid, ok := c.tenantID() if !ok { return } orgID, valid := c.pathUint64(":org_id") if !valid { c.jsonError(400, "无效组织ID") return } orgIDs := c.collectOrgIDs(tid, orgID) values := make([]string, 0, len(orgIDs)) for _, id := range orgIDs { values = append(values, strconv.FormatUint(id, 10)) } var rows []models.BackendEmployee _, err := c.employeeQuery(tid).Filter("department__in", values).OrderBy("-id").All(&rows) if err != nil { c.jsonError(500, "查询员工失败: "+err.Error()) return } c.jsonOK(c.employeeDTOList(tid, rows)) } // GetEmployeeDetail 获取员工详情。 // GET /backend/{erp|oa}/getEmployeeDetail/:id func (c *BackendOrganizationController) GetEmployeeDetail() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint(":id") if !valid { c.jsonError(400, "无效ID") return } var row models.BackendEmployee if err := c.employeeQuery(tid).Filter("id", id).One(&row); err != nil { c.jsonError(404, "员工不存在") return } c.jsonOK(c.employeeDTO(tid, row)) } // CreateEmployee 创建员工。 // POST /backend/{erp|oa}/createEmployee func (c *BackendOrganizationController) CreateEmployee() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() settings := c.loadOrgSettings(tid) name, _ := c.getStringValue(body, "name") name = strings.TrimSpace(name) if name == "" { c.jsonError(400, "姓名不能为空") return } account, _ := c.getStringValue(body, "account") account = strings.TrimSpace(account) if account == "" { if !settings.AutoGenerateCodes { c.jsonError(400, "账号不能为空") return } account = c.generateCode(settings.EmployeeCodePrefix, settings.CodeLength) } count, err := c.employeeQuery(tid).Filter("account", account).Count() if err != nil { c.jsonError(500, "校验账号失败: "+err.Error()) return } if count > 0 { c.jsonError(400, "账号已存在") return } gender, hasGender := c.getIntValue(body, "gender", "sex") status, hasStatus := c.getIntValue(body, "account_status", "status") password, _ := c.getStringValue(body, "password") birthday, _ := c.getStringValue(body, "birthday") affiliateUnit, _ := c.getStringValue(body, "affiliate_unit") department, _ := c.getStringValue(body, "department") position, _ := c.getStringValue(body, "position") education, _ := c.getStringValue(body, "education") nation, _ := c.getStringValue(body, "nation") phone, _ := c.getStringValue(body, "phone") wechat, _ := c.getStringValue(body, "wechat") email, _ := c.getStringValue(body, "email") homeAddress, _ := c.getStringValue(body, "home_address") if err := c.validateEmployeeOrg(tid, affiliateUnit, department); err != nil { c.jsonError(400, err.Error()) return } tidInt := int(tid) row := models.BackendEmployee{ Tid: &tidInt, Account: account, Password: hashEmployeePassword(password), Name: name, Gender: 0, Birthday: parseDatePtr(birthday), AffiliateUnit: strPtrIfNotEmpty(affiliateUnit), Department: strPtrIfNotEmpty(department), Position: strPtrIfNotEmpty(position), Education: strPtrIfNotEmpty(education), Nation: strPtrIfNotEmpty(nation), Phone: strPtrIfNotEmpty(phone), Wechat: strPtrIfNotEmpty(wechat), Email: strPtrIfNotEmpty(email), HomeAddress: strPtrIfNotEmpty(homeAddress), AccountStatus: 1, } if hasGender { row.Gender = int8(gender) } if hasStatus { row.AccountStatus = int8(status) } id, err := models.Orm.Insert(&row) if err != nil { c.jsonError(500, "创建员工失败: "+err.Error()) return } // 同步到通讯录(员工为基础数据,已建档时档案字段优先) SyncContactOnEmployeeCreate(tid, uint64(id), &row) c.jsonOK(map[string]interface{}{"id": id, "account": account}) } // EditEmployee 更新员工。 // POST /backend/{erp|oa}/editEmployee/:id func (c *BackendOrganizationController) EditEmployee() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint(":id") if !valid { c.jsonError(400, "无效ID") return } if !c.employeeQuery(tid).Filter("id", id).Exist() { c.jsonError(404, "员工不存在") return } body := c.parseJSONBody() update := orm.Params{} if v, has := c.getStringValue(body, "account"); has && strings.TrimSpace(v) != "" { v = strings.TrimSpace(v) count, err := c.employeeQuery(tid).Filter("account", v).Exclude("id", id).Count() if err != nil { c.jsonError(500, "校验账号失败: "+err.Error()) return } if count > 0 { c.jsonError(400, "账号已存在") return } update["account"] = v } if v, has := c.getStringValue(body, "name"); has { v = strings.TrimSpace(v) if v == "" { c.jsonError(400, "姓名不能为空") return } update["name"] = v } if v, has := c.getIntValue(body, "gender", "sex"); has { update["gender"] = int8(v) } if v, has := c.getStringValue(body, "birthday"); has { update["birthday"] = parseDatePtr(v) } affiliateUnit, hasAffiliate := c.getStringValue(body, "affiliate_unit") department, hasDepartment := c.getStringValue(body, "department") if hasAffiliate || hasDepartment { if err := c.validateEmployeeOrg(tid, affiliateUnit, department); err != nil { c.jsonError(400, err.Error()) return } } if hasAffiliate { update["affiliate_unit"] = nullableString(affiliateUnit) } if hasDepartment { update["department"] = nullableString(department) } if v, has := c.getStringValue(body, "position"); has { update["position"] = nullableString(v) } if v, has := c.getStringValue(body, "education"); has { update["education"] = nullableString(v) } if v, has := c.getStringValue(body, "nation"); has { update["nation"] = nullableString(v) } if v, has := c.getStringValue(body, "phone"); has { update["phone"] = nullableString(v) } if v, has := c.getStringValue(body, "wechat"); has { update["wechat"] = nullableString(v) } if v, has := c.getStringValue(body, "email"); has { update["email"] = nullableString(v) } if v, has := c.getStringValue(body, "home_address"); has { update["home_address"] = nullableString(v) } if v, has := c.getIntValue(body, "account_status", "status"); has { update["account_status"] = int8(v) } if v, has := c.getStringValue(body, "password"); has && strings.TrimSpace(v) != "" { update["password"] = hashEmployeePassword(v) } if len(update) == 0 { c.jsonError(400, "无更新字段") return } if _, err := c.employeeQuery(tid).Filter("id", id).Update(update); err != nil { c.jsonError(500, "更新员工失败: "+err.Error()) return } // 同步到通讯录(字段优先级:人事档案 > 员工) SyncContactOnEmployeeRefresh(tid, uint64(id)) c.jsonOK(nil) } // DeleteEmployee 员工离职/禁用:员工不做删除,只改账号状态。 // // 说明:员工属于企业数据,人事档案(yz_backend_employee_file)与通讯录 //(yz_backend_erp_contact)都以 employee_id 关联员工。一旦把员工软删除(写 delete_time), // 档案页与通讯录就查不到对应的人,等于连带删掉了企业数据。 // 因此这里只把账号状态置为"离职"(2),不写 delete_time,也不触碰档案与通讯录记录。 // 启用/禁用请走 editEmployee 改 account_status(1 启用 / 0 禁用 / 2 离职)。 // DELETE /backend/{erp|oa}/deleteEmployee/:id func (c *BackendOrganizationController) DeleteEmployee() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint(":id") if !valid { c.jsonError(400, "无效ID") return } if !c.employeeQuery(tid).Filter("id", id).Exist() { c.jsonError(404, "员工不存在") return } // 员工不允许删除,仅置为离职状态;档案与通讯录原样保留 if _, err := c.employeeQuery(tid).Filter("id", id). Update(orm.Params{"account_status": int8(2)}); err != nil { c.jsonError(500, "离职操作失败: "+err.Error()) return } c.jsonOK(nil) } // MoveEmployeeToOrg 员工调岗:批量或单个移动到指定组织。 // POST /backend/{erp|oa}/moveEmployeeToOrg func (c *BackendOrganizationController) MoveEmployeeToOrg() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() orgID, hasOrg := c.getUint64Value(body, "org_id", "department_id") if !hasOrg || orgID == 0 { c.jsonError(400, "组织ID不能为空") return } ids := c.getUintSlice(body, "employee_ids", "ids") if single, has := c.getUintValue(body, "employee_id"); has && single > 0 { ids = append(ids, single) } if len(ids) == 0 { c.jsonError(400, "请选择要调岗的员工") return } var org models.BackendOrganization if err := c.orgQuery(tid).Filter("id", orgID).One(&org); err != nil { c.jsonError(400, "目标组织不存在") return } update := orm.Params{"department": strconv.FormatUint(orgID, 10)} // 部门变更时同步隶属单位:取该组织所在的顶层公司 if companyID := c.rootCompanyID(tid, org); companyID > 0 { update["affiliate_unit"] = strconv.FormatUint(companyID, 10) } if v, has := c.getStringValue(body, "position"); has { update["position"] = nullableString(v) } num, err := c.employeeQuery(tid).Filter("id__in", ids).Update(update) if err != nil { c.jsonError(500, "调岗失败: "+err.Error()) return } if num == 0 { c.jsonError(404, "员工不存在") return } c.jsonOK(map[string]interface{}{"affected": num}) } // CheckEmployeeAccountUnique 检查员工账号唯一性。 // GET /backend/{erp|oa}/checkEmployeeAccountUnique?account=xx&exclude_id=1 func (c *BackendOrganizationController) CheckEmployeeAccountUnique() { tid, ok := c.tenantID() if !ok { return } account := strings.TrimSpace(c.GetString("account")) if account == "" { c.jsonError(400, "员工账号不能为空") return } excludeID, _ := c.GetUint64("exclude_id") if excludeID == 0 { excludeID, _ = c.GetUint64("employee_id") } qs := c.employeeQuery(tid).Filter("account", account) if excludeID > 0 { qs = qs.Exclude("id", excludeID) } count, err := qs.Count() if err != nil { c.jsonError(500, "检查账号唯一性失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{"unique": count == 0, "message": uniqueMessage(count == 0, "账号")}) } // --------------------------------------------------------------------------- // 职位 // --------------------------------------------------------------------------- // GetPosition 获取职位列表。 // GET /backend/{erp|oa}/getPosition?department_id=1&keyword=xx func (c *BackendOrganizationController) GetPosition() { tid, ok := c.tenantID() if !ok { return } cond := orm.NewCondition().And("tid", tid).And("delete_time__isnull", true) if departmentID, _ := c.GetUint64("department_id"); departmentID > 0 { cond = cond.And("department_id", departmentID) } if keyword := strings.TrimSpace(c.GetString("keyword")); keyword != "" { cond = cond.AndCond(orm.NewCondition(). Or("position_name__icontains", keyword). Or("position_code__icontains", keyword)) } if raw := strings.TrimSpace(c.GetString("status")); raw != "" { if status, err := strconv.Atoi(raw); err == nil { cond = cond.And("status", int8(status)) } } var rows []models.BackendPosition _, err := models.Orm.QueryTable(new(models.BackendPosition)). SetCond(cond).OrderBy("sort", "id").All(&rows) if err != nil { c.jsonError(500, "查询职位失败: "+err.Error()) return } list := make([]positionDTO, 0, len(rows)) for _, row := range rows { list = append(list, c.positionDTO(tid, row)) } c.jsonOK(list) } // GetPositionDetail 获取职位详情。 // GET /backend/{erp|oa}/getPositionDetail/:id func (c *BackendOrganizationController) GetPositionDetail() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint64(":id") if !valid { c.jsonError(400, "无效ID") return } var row models.BackendPosition if err := c.positionQuery(tid).Filter("id", id).One(&row); err != nil { c.jsonError(404, "职位不存在") return } c.jsonOK(c.positionDTO(tid, row)) } // CreatePosition 创建职位。 // POST /backend/{erp|oa}/createPosition func (c *BackendOrganizationController) CreatePosition() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() settings := c.loadOrgSettings(tid) positionName, _ := c.getStringValue(body, "position_name", "name") positionName = strings.TrimSpace(positionName) if positionName == "" { c.jsonError(400, "职位名称不能为空") return } departmentID, _ := c.getUint64Value(body, "department_id") if departmentID > 0 && !c.orgExists(tid, departmentID) { c.jsonError(400, "所属部门不存在") return } positionCode, _ := c.getStringValue(body, "position_code", "code") positionCode = strings.TrimSpace(positionCode) if positionCode == "" { if !settings.AutoGenerateCodes { c.jsonError(400, "职位编码不能为空") return } positionCode = c.generateCode(settings.PositionCodePrefix, settings.CodeLength) } if !settings.AllowDuplicateCode { count, err := c.positionQuery(tid).Filter("position_code", positionCode).Count() if err != nil { c.jsonError(500, "校验职位编码失败: "+err.Error()) return } if count > 0 { c.jsonError(400, "职位编码已存在") return } } positionType, _ := c.getIntValue(body, "position_type") status, hasStatus := c.getIntValue(body, "status") sortVal, _ := c.getUintValue(body, "sort") remark, _ := c.getStringValue(body, "remark") row := models.BackendPosition{ Tid: tid, DepartmentID: departmentID, PositionCode: positionCode, PositionName: positionName, PositionType: int8(positionType), Status: 1, Sort: sortVal, Remark: strPtrIfNotEmpty(remark), } if hasStatus { row.Status = int8(status) } id, err := models.Orm.Insert(&row) if err != nil { c.jsonError(500, "创建职位失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{"id": id, "position_code": positionCode}) } // EditPosition 更新职位。 // POST /backend/{erp|oa}/editPosition/:id func (c *BackendOrganizationController) EditPosition() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint64(":id") if !valid { c.jsonError(400, "无效ID") return } if !c.positionQuery(tid).Filter("id", id).Exist() { c.jsonError(404, "职位不存在") return } body := c.parseJSONBody() settings := c.loadOrgSettings(tid) update := orm.Params{} if v, has := c.getUint64Value(body, "department_id"); has { if v > 0 && !c.orgExists(tid, v) { c.jsonError(400, "所属部门不存在") return } update["department_id"] = v } if v, has := c.getStringValue(body, "position_code", "code"); has { v = strings.TrimSpace(v) if v == "" { c.jsonError(400, "职位编码不能为空") return } if !settings.AllowDuplicateCode { count, err := c.positionQuery(tid).Filter("position_code", v).Exclude("id", id).Count() if err != nil { c.jsonError(500, "校验职位编码失败: "+err.Error()) return } if count > 0 { c.jsonError(400, "职位编码已存在") return } } update["position_code"] = v } if v, has := c.getStringValue(body, "position_name", "name"); has { v = strings.TrimSpace(v) if v == "" { c.jsonError(400, "职位名称不能为空") return } update["position_name"] = v } if v, has := c.getIntValue(body, "position_type"); has { update["position_type"] = int8(v) } if v, has := c.getIntValue(body, "status"); has { update["status"] = int8(v) } if v, has := c.getUintValue(body, "sort"); has { update["sort"] = v } if v, has := c.getStringValue(body, "remark"); has { update["remark"] = nullableString(v) } if len(update) == 0 { c.jsonError(400, "无更新字段") return } if _, err := c.positionQuery(tid).Filter("id", id).Update(update); err != nil { c.jsonError(500, "更新职位失败: "+err.Error()) return } c.jsonOK(nil) } // DeletePosition 删除职位(软删除)。 // DELETE /backend/{erp|oa}/deletePosition/:id func (c *BackendOrganizationController) DeletePosition() { tid, ok := c.tenantID() if !ok { return } id, valid := c.pathUint64(":id") if !valid { c.jsonError(400, "无效ID") return } num, err := c.positionQuery(tid).Filter("id", id). Update(orm.Params{"delete_time": c.nowString(), "status": int8(0)}) if err != nil { c.jsonError(500, "删除职位失败: "+err.Error()) return } if num == 0 { c.jsonError(404, "职位不存在") return } c.jsonOK(nil) } // CheckPositionCodeUnique 检查职位编码唯一性。 // GET /backend/{erp|oa}/checkPositionCodeUnique?position_code=xx&exclude_id=1 func (c *BackendOrganizationController) CheckPositionCodeUnique() { tid, ok := c.tenantID() if !ok { return } code := strings.TrimSpace(c.GetString("position_code")) if code == "" { c.jsonError(400, "职位编码不能为空") return } excludeID, _ := c.GetUint64("exclude_id") qs := c.positionQuery(tid).Filter("position_code", code) if excludeID > 0 { qs = qs.Exclude("id", excludeID) } count, err := qs.Count() if err != nil { c.jsonError(500, "检查编码唯一性失败: "+err.Error()) return } c.jsonOK(map[string]interface{}{"unique": count == 0, "message": uniqueMessage(count == 0, "编码")}) }