Files
yunzerwebsiteallinone/go/controllers/backend_organization_dto.go
T
2026-08-27 21:25:39 +08:00

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[:])
}