package orgunitstore import ( "context" "database/sql" "errors" "fmt" "strings" "sync" "time" ) // MaxDepth 组织树最大深度(公司下第 1 级为 depth=1)。 const MaxDepth = 5 type OrgUnit struct { OrgUnitID int64 `json:"org_unit_id"` TenantID int64 `json:"tenant_id"` ParentID int64 `json:"parent_id,omitempty"` Name string `json:"name"` Code string `json:"code,omitempty"` Depth int `json:"depth"` Path string `json:"path"` CreatedAt time.Time `json:"created_at"` } type CreateInput struct { ParentID int64 Name string Code string } type UpdateInput struct { Name *string Code *string } type Store interface { List(ctx context.Context, tenantID int64) ([]OrgUnit, error) Get(ctx context.Context, tenantID, orgUnitID int64) (*OrgUnit, error) Create(ctx context.Context, tenantID int64, in CreateInput) (*OrgUnit, error) Update(ctx context.Context, tenantID, orgUnitID int64, in UpdateInput) (*OrgUnit, error) Delete(ctx context.Context, tenantID, orgUnitID int64) error // DescendantIDs 含自身。 DescendantIDs(ctx context.Context, tenantID, orgUnitID int64) ([]int64, error) } type MemoryStore struct { mu sync.Mutex byID map[int64]*OrgUnit seq int64 } func NewMemoryStore() *MemoryStore { return &MemoryStore{byID: map[int64]*OrgUnit{}} } func (s *MemoryStore) List(_ context.Context, tenantID int64) ([]OrgUnit, error) { s.mu.Lock() defer s.mu.Unlock() out := make([]OrgUnit, 0) for _, o := range s.byID { if o.TenantID == tenantID { out = append(out, *o) } } return out, nil } func (s *MemoryStore) Get(_ context.Context, tenantID, orgUnitID int64) (*OrgUnit, error) { s.mu.Lock() defer s.mu.Unlock() o, ok := s.byID[orgUnitID] if !ok || o.TenantID != tenantID { return nil, fmt.Errorf("org unit not found") } cp := *o return &cp, nil } func (s *MemoryStore) Create(_ context.Context, tenantID int64, in CreateInput) (*OrgUnit, error) { s.mu.Lock() defer s.mu.Unlock() name := strings.TrimSpace(in.Name) if name == "" { return nil, fmt.Errorf("name required") } code := strings.TrimSpace(in.Code) depth := 1 pathPrefix := "" if in.ParentID > 0 { p, ok := s.byID[in.ParentID] if !ok || p.TenantID != tenantID { return nil, fmt.Errorf("parent not found") } if p.Depth >= MaxDepth { return nil, fmt.Errorf("max org depth is %d", MaxDepth) } depth = p.Depth + 1 pathPrefix = p.Path } if code != "" { for _, o := range s.byID { if o.TenantID == tenantID && o.Code == code { return nil, fmt.Errorf("org code already exists") } } } s.seq++ id := s.seq path := fmt.Sprintf("%s/%d", pathPrefix, id) if pathPrefix == "" { path = fmt.Sprintf("/%d", id) } o := &OrgUnit{ OrgUnitID: id, TenantID: tenantID, ParentID: in.ParentID, Name: name, Code: code, Depth: depth, Path: path, CreatedAt: time.Now().UTC(), } s.byID[id] = o cp := *o return &cp, nil } func (s *MemoryStore) Update(_ context.Context, tenantID, orgUnitID int64, in UpdateInput) (*OrgUnit, error) { s.mu.Lock() defer s.mu.Unlock() o, ok := s.byID[orgUnitID] if !ok || o.TenantID != tenantID { return nil, fmt.Errorf("org unit not found") } if in.Name != nil { n := strings.TrimSpace(*in.Name) if n == "" { return nil, fmt.Errorf("name required") } o.Name = n } if in.Code != nil { code := strings.TrimSpace(*in.Code) for _, x := range s.byID { if x.TenantID == tenantID && x.Code == code && x.OrgUnitID != orgUnitID { return nil, fmt.Errorf("org code already exists") } } o.Code = code } cp := *o return &cp, nil } func (s *MemoryStore) Delete(_ context.Context, tenantID, orgUnitID int64) error { s.mu.Lock() defer s.mu.Unlock() o, ok := s.byID[orgUnitID] if !ok || o.TenantID != tenantID { return fmt.Errorf("org unit not found") } for _, x := range s.byID { if x.TenantID == tenantID && x.ParentID == orgUnitID { return fmt.Errorf("org unit has children") } } delete(s.byID, orgUnitID) return nil } func (s *MemoryStore) DescendantIDs(_ context.Context, tenantID, orgUnitID int64) ([]int64, error) { s.mu.Lock() defer s.mu.Unlock() o, ok := s.byID[orgUnitID] if !ok || o.TenantID != tenantID { return nil, fmt.Errorf("org unit not found") } out := []int64{orgUnitID} prefix := o.Path + "/" for _, x := range s.byID { if x.TenantID == tenantID && strings.HasPrefix(x.Path, prefix) { out = append(out, x.OrgUnitID) } } return out, nil } type PostgresStore struct { DB *sql.DB } func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} } func (s *PostgresStore) List(ctx context.Context, tenantID int64) ([]OrgUnit, error) { rows, err := s.DB.QueryContext(ctx, ` SELECT org_unit_id, tenant_id, COALESCE(parent_id,0), name, COALESCE(code,''), depth, path, created_at FROM platform_meta.org_units WHERE tenant_id=$1 ORDER BY path`, tenantID) if err != nil { return nil, err } defer rows.Close() out := make([]OrgUnit, 0) for rows.Next() { var o OrgUnit if err := rows.Scan(&o.OrgUnitID, &o.TenantID, &o.ParentID, &o.Name, &o.Code, &o.Depth, &o.Path, &o.CreatedAt); err != nil { return nil, err } out = append(out, o) } return out, rows.Err() } func (s *PostgresStore) Get(ctx context.Context, tenantID, orgUnitID int64) (*OrgUnit, error) { var o OrgUnit err := s.DB.QueryRowContext(ctx, ` SELECT org_unit_id, tenant_id, COALESCE(parent_id,0), name, COALESCE(code,''), depth, path, created_at FROM platform_meta.org_units WHERE org_unit_id=$1 AND tenant_id=$2`, orgUnitID, tenantID, ).Scan(&o.OrgUnitID, &o.TenantID, &o.ParentID, &o.Name, &o.Code, &o.Depth, &o.Path, &o.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("org unit not found") } return &o, err } func (s *PostgresStore) Create(ctx context.Context, tenantID int64, in CreateInput) (*OrgUnit, error) { name := strings.TrimSpace(in.Name) if name == "" { return nil, fmt.Errorf("name required") } code := strings.TrimSpace(in.Code) depth := 1 parentPath := "" var parentAny any if in.ParentID > 0 { p, err := s.Get(ctx, tenantID, in.ParentID) if err != nil { return nil, fmt.Errorf("parent not found") } if p.Depth >= MaxDepth { return nil, fmt.Errorf("max org depth is %d", MaxDepth) } depth = p.Depth + 1 parentPath = p.Path parentAny = in.ParentID } tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { _ = tx.Rollback() }() var id int64 err = tx.QueryRowContext(ctx, ` INSERT INTO platform_meta.org_units(tenant_id, parent_id, name, code, depth, path) VALUES($1,$2,$3,$4,$5,'') RETURNING org_unit_id`, tenantID, parentAny, name, code, depth).Scan(&id) if err != nil { if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") { return nil, fmt.Errorf("org code already exists") } return nil, err } path := fmt.Sprintf("/%d", id) if parentPath != "" { path = parentPath + "/" + fmt.Sprintf("%d", id) } if _, err := tx.ExecContext(ctx, `UPDATE platform_meta.org_units SET path=$1 WHERE org_unit_id=$2`, path, id); err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return s.Get(ctx, tenantID, id) } func (s *PostgresStore) Update(ctx context.Context, tenantID, orgUnitID int64, in UpdateInput) (*OrgUnit, error) { cur, err := s.Get(ctx, tenantID, orgUnitID) if err != nil { return nil, err } name, code := cur.Name, cur.Code if in.Name != nil { name = strings.TrimSpace(*in.Name) if name == "" { return nil, fmt.Errorf("name required") } } if in.Code != nil { code = strings.TrimSpace(*in.Code) } _, err = s.DB.ExecContext(ctx, ` UPDATE platform_meta.org_units SET name=$1, code=$2 WHERE org_unit_id=$3 AND tenant_id=$4`, name, code, orgUnitID, tenantID) if err != nil { if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") { return nil, fmt.Errorf("org code already exists") } return nil, err } return s.Get(ctx, tenantID, orgUnitID) } func (s *PostgresStore) Delete(ctx context.Context, tenantID, orgUnitID int64) error { var n int if err := s.DB.QueryRowContext(ctx, ` SELECT COUNT(1) FROM platform_meta.org_units WHERE tenant_id=$1 AND parent_id=$2`, tenantID, orgUnitID).Scan(&n); err != nil { return err } if n > 0 { return fmt.Errorf("org unit has children") } res, err := s.DB.ExecContext(ctx, ` DELETE FROM platform_meta.org_units WHERE org_unit_id=$1 AND tenant_id=$2`, orgUnitID, tenantID) if err != nil { return err } aff, _ := res.RowsAffected() if aff == 0 { return fmt.Errorf("org unit not found") } return nil } func (s *PostgresStore) DescendantIDs(ctx context.Context, tenantID, orgUnitID int64) ([]int64, error) { o, err := s.Get(ctx, tenantID, orgUnitID) if err != nil { return nil, err } rows, err := s.DB.QueryContext(ctx, ` SELECT org_unit_id FROM platform_meta.org_units WHERE tenant_id=$1 AND (org_unit_id=$2 OR path LIKE $3) ORDER BY path`, tenantID, orgUnitID, o.Path+"/%") if err != nil { return nil, err } defer rows.Close() out := make([]int64, 0) for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() }