增加组织架构
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
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[:])
|
||||
}
|
||||
Reference in New Issue
Block a user