chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
352
platform/internal/orgunitstore/store.go
Normal file
352
platform/internal/orgunitstore/store.go
Normal file
@@ -0,0 +1,352 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user