Files
yunzerwebsiteallinone/go/controllers/backend_organization.go
T

1463 lines
40 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "编码")})
}