480 lines
13 KiB
Go
480 lines
13 KiB
Go
package controllers
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"server/models"
|
|
)
|
|
|
|
// 本文件包含组织架构模块的 DTO 组装、树结构与层级校验,以及请求参数解析工具。
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DTO 组装
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// organizationDTOList 批量组装组织 DTO。一次性把父组织名、负责人名、员工数查出来,
|
|
// 避免逐行查询导致的 N+1 问题。
|
|
func (c *BackendOrganizationController) organizationDTOList(tid uint64, rows []models.BackendOrganization) []organizationDTO {
|
|
list := make([]organizationDTO, 0, len(rows))
|
|
if len(rows) == 0 {
|
|
return list
|
|
}
|
|
|
|
nameByID := c.orgNameMap(tid)
|
|
leaderNames := c.employeeNameMap(tid)
|
|
counts := c.employeeCountByDepartment(tid)
|
|
|
|
for _, row := range rows {
|
|
list = append(list, c.assembleOrganizationDTO(row, nameByID, leaderNames, counts))
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (c *BackendOrganizationController) organizationDTO(tid uint64, row models.BackendOrganization) organizationDTO {
|
|
return c.assembleOrganizationDTO(row,
|
|
c.orgNameMap(tid), c.employeeNameMap(tid), c.employeeCountByDepartment(tid))
|
|
}
|
|
|
|
func (c *BackendOrganizationController) assembleOrganizationDTO(
|
|
row models.BackendOrganization,
|
|
nameByID map[uint64]string,
|
|
leaderNames map[uint64]string,
|
|
counts map[string]int64,
|
|
) organizationDTO {
|
|
leaderID := uint64(0)
|
|
if row.LeaderID != nil {
|
|
leaderID = *row.LeaderID
|
|
}
|
|
|
|
return organizationDTO{
|
|
ID: row.ID,
|
|
Tid: row.Tid,
|
|
TenantID: row.Tid,
|
|
OrgName: row.OrgName,
|
|
OrgCode: row.OrgCode,
|
|
ParentID: row.ParentID,
|
|
ParentName: nameByID[row.ParentID],
|
|
LeaderID: leaderID,
|
|
LeaderName: leaderNames[leaderID],
|
|
IsCompany: row.IsCompany,
|
|
Sort: row.Sort,
|
|
Status: row.Status,
|
|
Remark: derefString(row.Remark),
|
|
EmployeeCount: counts[strconv.FormatUint(row.ID, 10)],
|
|
CreateTime: formatDateTime(&row.CreateTime),
|
|
UpdateTime: formatDateTime(&row.UpdateTime),
|
|
}
|
|
}
|
|
|
|
func (c *BackendOrganizationController) employeeDTOList(tid uint64, rows []models.BackendEmployee) []employeeDTO {
|
|
list := make([]employeeDTO, 0, len(rows))
|
|
if len(rows) == 0 {
|
|
return list
|
|
}
|
|
|
|
nameByID := c.orgNameMap(tid)
|
|
for _, row := range rows {
|
|
list = append(list, c.assembleEmployeeDTO(row, nameByID))
|
|
}
|
|
return list
|
|
}
|
|
|
|
func (c *BackendOrganizationController) employeeDTO(tid uint64, row models.BackendEmployee) employeeDTO {
|
|
return c.assembleEmployeeDTO(row, c.orgNameMap(tid))
|
|
}
|
|
|
|
func (c *BackendOrganizationController) assembleEmployeeDTO(
|
|
row models.BackendEmployee,
|
|
nameByID map[uint64]string,
|
|
) employeeDTO {
|
|
tid := 0
|
|
if row.Tid != nil {
|
|
tid = *row.Tid
|
|
}
|
|
|
|
birthday := ""
|
|
if row.Birthday != nil {
|
|
birthday = row.Birthday.Format("2006-01-02")
|
|
}
|
|
|
|
affiliateUnit := derefString(row.AffiliateUnit)
|
|
department := derefString(row.Department)
|
|
|
|
return employeeDTO{
|
|
ID: row.ID,
|
|
Tid: tid,
|
|
TenantID: tid,
|
|
Account: row.Account,
|
|
Name: row.Name,
|
|
Gender: row.Gender,
|
|
Sex: row.Gender,
|
|
Birthday: birthday,
|
|
AffiliateUnit: affiliateUnit,
|
|
AffiliateUnitName: orgNameByIDString(nameByID, affiliateUnit),
|
|
Department: department,
|
|
DepartmentName: orgNameByIDString(nameByID, department),
|
|
Position: derefString(row.Position),
|
|
Education: derefString(row.Education),
|
|
Nation: derefString(row.Nation),
|
|
Phone: derefString(row.Phone),
|
|
Wechat: derefString(row.Wechat),
|
|
Email: derefString(row.Email),
|
|
HomeAddress: derefString(row.HomeAddress),
|
|
AccountStatus: row.AccountStatus,
|
|
Status: row.AccountStatus,
|
|
CreateTime: formatDateTime(&row.CreateTime),
|
|
}
|
|
}
|
|
|
|
func (c *BackendOrganizationController) positionDTO(tid uint64, row models.BackendPosition) positionDTO {
|
|
nameByID := c.orgNameMap(tid)
|
|
return positionDTO{
|
|
ID: row.ID,
|
|
Tid: row.Tid,
|
|
TenantID: row.Tid,
|
|
DepartmentID: row.DepartmentID,
|
|
DepartmentName: nameByID[row.DepartmentID],
|
|
PositionCode: row.PositionCode,
|
|
PositionName: row.PositionName,
|
|
PositionType: row.PositionType,
|
|
Status: row.Status,
|
|
Sort: row.Sort,
|
|
Remark: derefString(row.Remark),
|
|
CreateTime: formatDateTime(&row.CreateTime),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 组织关系查询
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (c *BackendOrganizationController) orgNameMap(tid uint64) map[uint64]string {
|
|
result := map[uint64]string{}
|
|
var rows []models.BackendOrganization
|
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "OrgName"); err != nil {
|
|
return result
|
|
}
|
|
for _, row := range rows {
|
|
result[row.ID] = row.OrgName
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *BackendOrganizationController) employeeNameMap(tid uint64) map[uint64]string {
|
|
result := map[uint64]string{}
|
|
var rows []models.BackendEmployee
|
|
if _, err := c.employeeQuery(tid).All(&rows, "ID", "Name"); err != nil {
|
|
return result
|
|
}
|
|
for _, row := range rows {
|
|
result[uint64(row.ID)] = row.Name
|
|
}
|
|
return result
|
|
}
|
|
|
|
// employeeCountByDepartment 统计各部门(department 存的是组织ID字符串)的员工数。
|
|
func (c *BackendOrganizationController) employeeCountByDepartment(tid uint64) map[string]int64 {
|
|
result := map[string]int64{}
|
|
var rows []models.BackendEmployee
|
|
if _, err := c.employeeQuery(tid).All(&rows, "Department"); err != nil {
|
|
return result
|
|
}
|
|
for _, row := range rows {
|
|
key := strings.TrimSpace(derefString(row.Department))
|
|
if key == "" {
|
|
continue
|
|
}
|
|
result[key]++
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *BackendOrganizationController) orgExists(tid, id uint64) bool {
|
|
if id == 0 {
|
|
return false
|
|
}
|
|
return c.orgQuery(tid).Filter("id", id).Exist()
|
|
}
|
|
|
|
// parentMap 返回 组织ID -> 上级组织ID 的映射,用于层级与环路判断。
|
|
func (c *BackendOrganizationController) parentMap(tid uint64) map[uint64]uint64 {
|
|
result := map[uint64]uint64{}
|
|
var rows []models.BackendOrganization
|
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "ParentID"); err != nil {
|
|
return result
|
|
}
|
|
for _, row := range rows {
|
|
result[row.ID] = row.ParentID
|
|
}
|
|
return result
|
|
}
|
|
|
|
// orgDepth 计算组织所在层级,顶级为 1。
|
|
func (c *BackendOrganizationController) orgDepth(tid, id uint64) (int, error) {
|
|
parents := c.parentMap(tid)
|
|
depth := 0
|
|
current := id
|
|
for current > 0 {
|
|
depth++
|
|
if depth > 64 {
|
|
return depth, errors.New("组织层级数据异常(可能存在环路)")
|
|
}
|
|
next, ok := parents[current]
|
|
if !ok {
|
|
break
|
|
}
|
|
current = next
|
|
}
|
|
return depth, nil
|
|
}
|
|
|
|
// subtreeHeight 计算以 id 为根的子树高度(只有自身时为 1)。
|
|
func (c *BackendOrganizationController) subtreeHeight(tid, id uint64) int {
|
|
childrenOf := map[uint64][]uint64{}
|
|
var rows []models.BackendOrganization
|
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "ParentID"); err != nil {
|
|
return 1
|
|
}
|
|
for _, row := range rows {
|
|
childrenOf[row.ParentID] = append(childrenOf[row.ParentID], row.ID)
|
|
}
|
|
return subtreeHeightFrom(childrenOf, id, 0)
|
|
}
|
|
|
|
func subtreeHeightFrom(childrenOf map[uint64][]uint64, id uint64, depth int) int {
|
|
if depth > 64 {
|
|
return depth
|
|
}
|
|
height := 1
|
|
for _, child := range childrenOf[id] {
|
|
if h := subtreeHeightFrom(childrenOf, child, depth+1) + 1; h > height {
|
|
height = h
|
|
}
|
|
}
|
|
return height
|
|
}
|
|
|
|
// collectOrgIDs 返回 rootID 及其所有下级组织的ID。
|
|
func (c *BackendOrganizationController) collectOrgIDs(tid, rootID uint64) []uint64 {
|
|
childrenOf := map[uint64][]uint64{}
|
|
var rows []models.BackendOrganization
|
|
if _, err := c.orgQuery(tid).All(&rows, "ID", "ParentID"); err != nil {
|
|
return []uint64{rootID}
|
|
}
|
|
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
|
|
}
|
|
|
|
// rootCompanyID 沿上级链向上找到所属的顶层公司ID。
|
|
func (c *BackendOrganizationController) rootCompanyID(tid uint64, org models.BackendOrganization) uint64 {
|
|
if org.IsCompany == 1 {
|
|
return org.ID
|
|
}
|
|
parents := c.parentMap(tid)
|
|
current := org.ParentID
|
|
for i := 0; i < 64 && current > 0; i++ {
|
|
var row models.BackendOrganization
|
|
if err := c.orgQuery(tid).Filter("id", current).One(&row); err != nil {
|
|
return 0
|
|
}
|
|
if row.IsCompany == 1 {
|
|
return row.ID
|
|
}
|
|
next, ok := parents[current]
|
|
if !ok {
|
|
return 0
|
|
}
|
|
current = next
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// validateParentChange 校验把 orgID 挂到 newParentID 下是否合法:
|
|
// 不能挂到自己或自己的后代(形成环路),且移动后总层级不超过设置上限。
|
|
func (c *BackendOrganizationController) validateParentChange(tid, orgID, newParentID uint64, settings orgSettings) error {
|
|
if newParentID == 0 {
|
|
return nil
|
|
}
|
|
if newParentID == orgID {
|
|
return errors.New("上级组织不能是自己")
|
|
}
|
|
if !c.orgExists(tid, newParentID) {
|
|
return errors.New("上级组织不存在")
|
|
}
|
|
|
|
for _, id := range c.collectOrgIDs(tid, orgID) {
|
|
if id == newParentID {
|
|
return errors.New("不能将组织移动到自己的下级组织中")
|
|
}
|
|
}
|
|
|
|
parentDepth, err := c.orgDepth(tid, newParentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if settings.MaxOrgLevels > 0 && parentDepth+c.subtreeHeight(tid, orgID) > settings.MaxOrgLevels {
|
|
return fmt.Errorf("移动后组织层级将超过上限 %d 级", settings.MaxOrgLevels)
|
|
}
|
|
if settings.MaxOrgChildren > 0 {
|
|
count, err := c.orgQuery(tid).Filter("parent_id", newParentID).
|
|
Exclude("id", orgID).Exclude("status", 0).Count()
|
|
if err == nil && int(count) >= settings.MaxOrgChildren {
|
|
return fmt.Errorf("同一上级下最多 %d 个子组织", settings.MaxOrgChildren)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateEmployeeOrg 校验员工的隶属单位与部门必须是当前租户下已存在的组织。
|
|
func (c *BackendOrganizationController) validateEmployeeOrg(tid uint64, affiliateUnit, department string) error {
|
|
for label, raw := range map[string]string{"隶属单位": affiliateUnit, "部门": department} {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
id, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("%s格式不正确", label)
|
|
}
|
|
if !c.orgExists(tid, id) {
|
|
return fmt.Errorf("%s不存在", label)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildOrganizationTree 把扁平的组织 DTO 列表组装成树。
|
|
// 上级不在列表中的节点(如上级被禁用)作为根节点返回,避免数据丢失。
|
|
func buildOrganizationTree(list []organizationDTO) []map[string]interface{} {
|
|
nodeMap := make(map[uint64]map[string]interface{}, len(list))
|
|
order := make([]uint64, 0, len(list))
|
|
|
|
for _, item := range list {
|
|
raw, _ := json.Marshal(item)
|
|
node := map[string]interface{}{}
|
|
_ = json.Unmarshal(raw, &node)
|
|
node["children"] = make([]map[string]interface{}, 0)
|
|
nodeMap[item.ID] = node
|
|
order = append(order, item.ID)
|
|
}
|
|
|
|
tree := make([]map[string]interface{}, 0)
|
|
for _, id := range order {
|
|
node := nodeMap[id]
|
|
parentID := uint64(0)
|
|
if v, ok := node["parent_id"].(float64); ok {
|
|
parentID = uint64(v)
|
|
}
|
|
if parent, exists := nodeMap[parentID]; parentID > 0 && exists {
|
|
parent["children"] = append(parent["children"].([]map[string]interface{}), node)
|
|
continue
|
|
}
|
|
tree = append(tree, node)
|
|
}
|
|
return tree
|
|
}
|
|
|
|
// treeDepth 计算树的最大深度。
|
|
func treeDepth(childrenOf map[uint64][]uint64, rootID uint64, depth int) int {
|
|
if depth > 64 {
|
|
return depth
|
|
}
|
|
maxDepth := depth
|
|
for _, child := range childrenOf[rootID] {
|
|
if d := treeDepth(childrenOf, child, depth+1); d > maxDepth {
|
|
maxDepth = d
|
|
}
|
|
}
|
|
return maxDepth
|
|
}
|
|
|
|
func orgNameByIDString(nameByID map[uint64]string, raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
id, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return nameByID[id]
|
|
}
|
|
|
|
func uniqueMessage(unique bool, subject string) string {
|
|
if unique {
|
|
return subject + "可用"
|
|
}
|
|
return subject + "已存在"
|
|
}
|
|
|
|
func formatDateTime(t *time.Time) string {
|
|
if t == nil || t.IsZero() {
|
|
return ""
|
|
}
|
|
return t.Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
func clampInt(v, min, max int) int {
|
|
if v < min {
|
|
return min
|
|
}
|
|
if v > max {
|
|
return max
|
|
}
|
|
return v
|
|
}
|
|
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// generateCode 生成形如 ORG20260827193012 的编码,长度不足时补时间戳末尾数字。
|
|
func (c *BackendOrganizationController) generateCode(prefix string, length int) string {
|
|
prefix = strings.TrimSpace(prefix)
|
|
stamp := time.Now().Format("20060102150405")
|
|
code := prefix + stamp
|
|
if length > 0 && len(code) > length && len(prefix) < length {
|
|
keep := length - len(prefix)
|
|
code = prefix + stamp[len(stamp)-keep:]
|
|
}
|
|
return code
|
|
}
|
|
|
|
// hashEmployeePassword 适配 password varchar(64),使用 sha256 hex;空密码返回空串。
|
|
func hashEmployeePassword(plain string) string {
|
|
plain = strings.TrimSpace(plain)
|
|
if plain == "" {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256([]byte(plain))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|