package userstore import ( "context" "database/sql" "errors" "fmt" "strings" "sync" "time" "golang.org/x/crypto/bcrypt" "aijianzhan/platform/internal/authx" ) const ( StatusPending = "pending" StatusActive = "active" RolePending = authx.Role待加入 ) type User struct { UserID int64 TenantID int64 // 0 = 未加入任何租户 OrgUnitID int64 // 0 = 未绑定组织(可见租户全量,由角色决定) Username string Phone string // 绑定手机号,可作登录账号 UsernameLoginDisabled bool // 已绑手机时可禁用「用户名+密码」登录 DisplayName string Role string Status string CreatedAt time.Time } func (u *User) HasTenant() bool { return u != nil && u.TenantID > 0 && u.Status == StatusActive && u.Role != RolePending } func (u *User) IsPlatformAdmin() bool { return u != nil && authx.IsPlatformAdmin(u.Role) } type TenantInfo struct { TenantID int64 `json:"tenant_id"` Name string `json:"name"` Slug string `json:"slug"` // 全局唯一路径前缀,如 aaa → /aaa/ CreatedAt time.Time `json:"created_at"` UserCount int64 `json:"user_count"` AppCount int64 `json:"app_count"` } type Store interface { Register(ctx context.Context, username, password, displayName string) (*User, error) EnsureBootstrapOwner(ctx context.Context, username, password, displayName, companyName string) (*User, error) EnsurePlatformAdmin(ctx context.Context, username, password, displayName string) (*User, error) Login(ctx context.Context, username, password string) (*User, error) GetByID(ctx context.Context, userID int64) (*User, error) GetByPhone(ctx context.Context, phone string) (*User, error) BindPhone(ctx context.Context, userID int64, phone string) (*User, error) SetUsernameLoginDisabled(ctx context.Context, userID int64, disabled bool) (*User, error) PhoneExists(ctx context.Context, phone string, excludeUserID int64) (bool, error) JoinTenant(ctx context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) CreateTenantAsOwner(ctx context.Context, userID int64, tenantName string) (*User, error) SetOrgUnit(ctx context.Context, userID, tenantID, orgUnitID int64) (*User, error) ListTenants(ctx context.Context) ([]TenantInfo, error) CreateTenant(ctx context.Context, name, slug string) (*TenantInfo, error) GetTenant(ctx context.Context, tenantID int64) (*TenantInfo, error) GetTenantBySlug(ctx context.Context, slug string) (*TenantInfo, error) UpdateTenant(ctx context.Context, tenantID int64, name, slug string) (*TenantInfo, error) RenameTenantIf(ctx context.Context, oldName, newName string) error EnsureTenantSlugs(ctx context.Context) error ListMembers(ctx context.Context, tenantID int64) ([]User, error) UpdateMember(ctx context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) // CreateMember 在指定公司直接创建登录账号;password 为空则随机生成。返回明文密码(仅此一次)。 CreateMember(ctx context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) UsernameExists(ctx context.Context, username string) (bool, error) // ChangePassword 校验旧密码后设置新密码(用户自行改密)。 ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error // SetPassword 管理员强制设密;password 为空则随机生成。返回明文密码。 SetPassword(ctx context.Context, userID int64, password string) (string, error) } type MemoryStore struct { mu sync.Mutex users map[string]*memUser byID map[int64]*memUser tenants map[int64]*TenantInfo seqUser int64 seqTen int64 } type memUser struct { User Hash string } func NewMemoryStore() *MemoryStore { return &MemoryStore{ users: map[string]*memUser{}, byID: map[int64]*memUser{}, tenants: map[int64]*TenantInfo{}, } } func (s *MemoryStore) Register(_ context.Context, username, password, displayName string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.users[username]; ok { return nil, fmt.Errorf("username already exists") } if len(username) < 3 || len(password) < 6 { return nil, fmt.Errorf("username>=3 and password>=6 required") } hash, err := hashPassword(password) if err != nil { return nil, err } name := displayName if name == "" { name = username } s.seqUser++ u := &memUser{ User: User{ UserID: s.seqUser, TenantID: 0, Username: username, DisplayName: name, Role: RolePending, Status: StatusPending, CreatedAt: time.Now().UTC(), }, Hash: hash, } s.users[username] = u s.byID[u.UserID] = u cp := u.User return &cp, nil } func (s *MemoryStore) EnsureBootstrapOwner(_ context.Context, username, password, displayName, companyName string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() if existing, ok := s.users[username]; ok { cp := existing.User return &cp, nil } hash, err := hashPassword(password) if err != nil { return nil, err } name := displayName if name == "" { name = username } tenName := strings.TrimSpace(companyName) if tenName == "" { tenName = name } s.seqTen++ s.seqUser++ u := &memUser{ User: User{ UserID: s.seqUser, TenantID: s.seqTen, Username: username, DisplayName: name, Role: authx.Role管理员, Status: StatusActive, CreatedAt: time.Now().UTC(), }, Hash: hash, } s.users[username] = u s.byID[u.UserID] = u s.tenants[s.seqTen] = &TenantInfo{TenantID: s.seqTen, Name: tenName, Slug: "demo", CreatedAt: time.Now().UTC()} cp := u.User return &cp, nil } func (s *MemoryStore) EnsurePlatformAdmin(_ context.Context, username, password, displayName string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() hash, err := hashPassword(password) if err != nil { return nil, err } name := displayName if name == "" { name = username } if existing, ok := s.users[username]; ok { existing.Hash = hash existing.Role = authx.Role超级管理员 existing.Status = StatusActive existing.TenantID = 0 existing.OrgUnitID = 0 if name != "" { existing.DisplayName = name } cp := existing.User return &cp, nil } s.seqUser++ u := &memUser{ User: User{ UserID: s.seqUser, TenantID: 0, Username: username, DisplayName: name, Role: authx.Role超级管理员, Status: StatusActive, CreatedAt: time.Now().UTC(), }, Hash: hash, } s.users[username] = u s.byID[u.UserID] = u cp := u.User return &cp, nil } func (s *MemoryStore) ListTenants(_ context.Context) ([]TenantInfo, error) { s.mu.Lock() defer s.mu.Unlock() out := make([]TenantInfo, 0, len(s.tenants)) for _, t := range s.tenants { info := *t var uc int64 for _, u := range s.users { if u.TenantID == t.TenantID && u.Status == StatusActive { uc++ } } info.UserCount = uc out = append(out, info) } return out, nil } func (s *MemoryStore) GetTenant(_ context.Context, tenantID int64) (*TenantInfo, error) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.tenants[tenantID] if !ok { return nil, fmt.Errorf("tenant not found") } cp := *t return &cp, nil } func (s *MemoryStore) CreateTenant(_ context.Context, name, slug string) (*TenantInfo, error) { s.mu.Lock() defer s.mu.Unlock() name = strings.TrimSpace(name) if name == "" { return nil, fmt.Errorf("name required") } if strings.TrimSpace(slug) == "" { slug = SuggestTenantSlug(name) } ns, err := NormalizeTenantSlug(slug) if err != nil { return nil, err } for _, t := range s.tenants { if t.Slug == ns { return nil, fmt.Errorf("slug already exists: %s", ns) } } s.seqTen++ t := &TenantInfo{TenantID: s.seqTen, Name: name, Slug: ns, CreatedAt: time.Now().UTC()} s.tenants[s.seqTen] = t cp := *t return &cp, nil } func (s *MemoryStore) UpdateTenant(_ context.Context, tenantID int64, name, slug string) (*TenantInfo, error) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.tenants[tenantID] if !ok { return nil, fmt.Errorf("tenant not found") } name = strings.TrimSpace(name) if name == "" { return nil, fmt.Errorf("name required") } t.Name = name if strings.TrimSpace(slug) != "" { ns, err := NormalizeTenantSlug(slug) if err != nil { return nil, err } for id, other := range s.tenants { if id != tenantID && other.Slug == ns { return nil, fmt.Errorf("slug already exists: %s", ns) } } t.Slug = ns } cp := *t return &cp, nil } func (s *MemoryStore) GetTenantBySlug(_ context.Context, slug string) (*TenantInfo, error) { s.mu.Lock() defer s.mu.Unlock() ns, err := NormalizeTenantSlug(slug) if err != nil { return nil, err } for _, t := range s.tenants { if t.Slug == ns { cp := *t return &cp, nil } } return nil, fmt.Errorf("tenant not found") } func (s *MemoryStore) EnsureTenantSlugs(_ context.Context) error { s.mu.Lock() defer s.mu.Unlock() used := map[string]struct{}{} for _, t := range s.tenants { if t.Slug != "" { used[t.Slug] = struct{}{} } } for _, t := range s.tenants { if t.Slug != "" { continue } base := SuggestTenantSlug(t.Name) if base == "" { base = fmt.Sprintf("t%d", t.TenantID) } cand := base for i := 2; ; i++ { if _, ok := used[cand]; !ok { if _, err := NormalizeTenantSlug(cand); err == nil { break } } cand = fmt.Sprintf("%s-%d", base, i) if i > 100 { cand = fmt.Sprintf("t%d", t.TenantID) break } } ns, err := NormalizeTenantSlug(cand) if err != nil { ns = fmt.Sprintf("t%d", t.TenantID) } t.Slug = ns used[ns] = struct{}{} } return nil } func (s *MemoryStore) ListMembers(_ context.Context, tenantID int64) ([]User, error) { s.mu.Lock() defer s.mu.Unlock() out := make([]User, 0) for _, u := range s.users { if u.TenantID == tenantID && !authx.IsPlatformAdmin(u.Role) { cp := u.User cp.Role = authx.NormalizeRole(cp.Role) out = append(out, cp) } } return out, nil } func (s *MemoryStore) UsernameExists(_ context.Context, username string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() _, ok := s.users[strings.TrimSpace(username)] return ok, nil } func (s *MemoryStore) CreateMember(_ context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) { s.mu.Lock() defer s.mu.Unlock() if tenantID <= 0 { return nil, "", fmt.Errorf("tenant required") } if _, ok := s.tenants[tenantID]; !ok { return nil, "", fmt.Errorf("tenant not found") } username = strings.TrimSpace(username) if len(username) < 3 { return nil, "", fmt.Errorf("username>=3 required") } if _, ok := s.users[username]; ok { return nil, "", fmt.Errorf("username already exists") } plain := strings.TrimSpace(password) var err error if plain == "" { plain, err = RandomPassword(12) if err != nil { return nil, "", err } } if len(plain) < 6 { return nil, "", fmt.Errorf("password>=6 required") } if !authx.ValidPlatformRole(role) { return nil, "", fmt.Errorf("invalid role") } role = authx.NormalizeRole(role) hash, err := hashPassword(plain) if err != nil { return nil, "", err } name := strings.TrimSpace(displayName) if name == "" { name = username } s.seqUser++ u := &memUser{ User: User{ UserID: s.seqUser, TenantID: tenantID, OrgUnitID: orgUnitID, Username: username, DisplayName: name, Role: role, Status: StatusActive, CreatedAt: time.Now().UTC(), }, Hash: hash, } s.users[username] = u s.byID[u.UserID] = u if t := s.tenants[tenantID]; t != nil { t.UserCount++ } cp := u.User return &cp, plain, nil } func (s *MemoryStore) ChangePassword(_ context.Context, userID int64, oldPassword, newPassword string) error { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return fmt.Errorf("user not found") } if bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(oldPassword)) != nil { return fmt.Errorf("旧密码不正确") } newPassword = strings.TrimSpace(newPassword) if len(newPassword) < 6 { return fmt.Errorf("password>=6 required") } hash, err := hashPassword(newPassword) if err != nil { return err } u.Hash = hash return nil } func (s *MemoryStore) SetPassword(_ context.Context, userID int64, password string) (string, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return "", fmt.Errorf("user not found") } plain := strings.TrimSpace(password) var err error if plain == "" { plain, err = RandomPassword(12) if err != nil { return "", err } } if len(plain) < 6 { return "", fmt.Errorf("password>=6 required") } hash, err := hashPassword(plain) if err != nil { return "", err } u.Hash = hash return plain, nil } func (s *MemoryStore) UpdateMember(_ context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok || u.TenantID != tenantID { return nil, fmt.Errorf("member not found") } if authx.IsPlatformAdmin(u.Role) { return nil, fmt.Errorf("cannot edit platform admin") } if role != "" { if !authx.ValidPlatformRole(role) { return nil, fmt.Errorf("invalid role") } u.Role = authx.NormalizeRole(role) } if status == StatusActive || status == StatusPending || status == "disabled" { u.Status = status } u.OrgUnitID = orgUnitID cp := u.User return &cp, nil } func (s *MemoryStore) Login(_ context.Context, account, password string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() account = strings.TrimSpace(account) u, ok := s.users[account] if !ok { if phone, err := NormalizePhone(account); err == nil { for _, cand := range s.users { if cand.Phone == phone { u, ok = cand, true break } } } } if !ok || bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password)) != nil { return nil, fmt.Errorf("invalid username or password") } cp := u.User cp.Role = authx.NormalizeRole(cp.Role) if cp.Status == "disabled" { return nil, fmt.Errorf("账号已停用") } return &cp, nil } func (s *MemoryStore) GetByID(_ context.Context, userID int64) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return nil, fmt.Errorf("user not found") } cp := u.User cp.Role = authx.NormalizeRole(cp.Role) return &cp, nil } func (s *MemoryStore) GetByPhone(_ context.Context, phone string) (*User, error) { ns, err := NormalizePhone(phone) if err != nil { return nil, err } s.mu.Lock() defer s.mu.Unlock() for _, u := range s.users { if u.Phone == ns { cp := u.User cp.Role = authx.NormalizeRole(cp.Role) return &cp, nil } } return nil, fmt.Errorf("user not found") } func (s *MemoryStore) PhoneExists(_ context.Context, phone string, excludeUserID int64) (bool, error) { s.mu.Lock() defer s.mu.Unlock() phone = strings.TrimSpace(phone) if phone == "" { return false, nil } for _, u := range s.users { if u.Phone == phone && u.UserID != excludeUserID { return true, nil } } return false, nil } func (s *MemoryStore) BindPhone(_ context.Context, userID int64, phone string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return nil, fmt.Errorf("user not found") } phone = strings.TrimSpace(phone) if phone == "" { u.Phone = "" u.UsernameLoginDisabled = false cp := u.User return &cp, nil } ns, err := NormalizePhone(phone) if err != nil { return nil, err } for _, o := range s.users { if o.Phone == ns && o.UserID != userID { return nil, fmt.Errorf("该手机号已被绑定") } } u.Phone = ns cp := u.User return &cp, nil } func (s *MemoryStore) SetUsernameLoginDisabled(_ context.Context, userID int64, disabled bool) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return nil, fmt.Errorf("user not found") } if disabled && strings.TrimSpace(u.Phone) == "" { return nil, fmt.Errorf("请先绑定手机号,再禁用用户名登录") } u.UsernameLoginDisabled = disabled cp := u.User return &cp, nil } func (s *MemoryStore) JoinTenant(_ context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return nil, fmt.Errorf("user not found") } if u.TenantID > 0 && u.Status == StatusActive { return nil, fmt.Errorf("user already joined a tenant") } if tenantID <= 0 { return nil, fmt.Errorf("invalid tenant") } role = normalizePlatformRole(role) u.TenantID = tenantID u.OrgUnitID = orgUnitID u.Role = role u.Status = StatusActive cp := u.User return &cp, nil } func (s *MemoryStore) SetOrgUnit(_ context.Context, userID, tenantID, orgUnitID int64) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok || u.TenantID != tenantID { return nil, fmt.Errorf("user not found") } u.OrgUnitID = orgUnitID cp := u.User return &cp, nil } func (s *MemoryStore) CreateTenantAsOwner(_ context.Context, userID int64, tenantName string) (*User, error) { s.mu.Lock() defer s.mu.Unlock() u, ok := s.byID[userID] if !ok { return nil, fmt.Errorf("user not found") } if u.TenantID > 0 && u.Status == StatusActive { return nil, fmt.Errorf("user already joined a tenant") } name := strings.TrimSpace(tenantName) if name == "" { name = u.DisplayName } if name == "" { name = u.Username } s.seqTen++ u.TenantID = s.seqTen u.Role = authx.Role管理员 u.Status = StatusActive slug := SuggestTenantSlug(name) if slug == "" { slug = fmt.Sprintf("t%d", s.seqTen) } base := slug for i := 2; ; i++ { clash := false for _, t := range s.tenants { if t.Slug == slug { clash = true break } } if !clash { if _, err := NormalizeTenantSlug(slug); err == nil { break } } slug = fmt.Sprintf("%s-%d", base, i) if i > 50 { slug = fmt.Sprintf("t%d", s.seqTen) break } } ns, _ := NormalizeTenantSlug(slug) if ns == "" { ns = fmt.Sprintf("t%d", s.seqTen) } s.tenants[s.seqTen] = &TenantInfo{TenantID: s.seqTen, Name: name, Slug: ns, CreatedAt: time.Now().UTC()} cp := u.User return &cp, nil } type PostgresStore struct { DB *sql.DB } func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} } func (s *PostgresStore) Register(ctx context.Context, username, password, displayName string) (*User, error) { if len(username) < 3 || len(password) < 6 { return nil, fmt.Errorf("username>=3 and password>=6 required") } hash, err := hashPassword(password) if err != nil { return nil, err } name := displayName if name == "" { name = username } u := &User{TenantID: 0, Username: username, DisplayName: name, Role: RolePending, Status: StatusPending} err = s.DB.QueryRowContext(ctx, ` INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status) VALUES(NULL,$1,$2,$3,'pending','pending') RETURNING user_id, created_at`, username, hash, name, ).Scan(&u.UserID, &u.CreatedAt) if err != nil { if isUnique(err) { return nil, fmt.Errorf("username already exists") } return nil, err } return u, nil } func (s *PostgresStore) EnsureBootstrapOwner(ctx context.Context, username, password, displayName, companyName string) (*User, error) { if u, err := s.Login(ctx, username, password); err == nil { return u, nil } hash, err := hashPassword(password) if err != nil { return nil, err } tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { _ = tx.Rollback() }() name := displayName if name == "" { name = username } tenName := strings.TrimSpace(companyName) if tenName == "" { tenName = name } tenSlug := "demo" if tenName != "演示公司" && tenName != "演示账号" { tenSlug = SuggestTenantSlug(tenName) if tenSlug == "" { tenSlug = "demo" } } var tenantID int64 if err := tx.QueryRowContext(ctx, `INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id`, tenName, tenSlug, ).Scan(&tenantID); err != nil { return nil, err } u := &User{TenantID: tenantID, Username: username, DisplayName: name, Role: authx.Role管理员, Status: StatusActive} err = tx.QueryRowContext(ctx, ` INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status) VALUES($1,$2,$3,$4,$5,'active') RETURNING user_id, created_at`, tenantID, username, hash, name, authx.Role管理员, ).Scan(&u.UserID, &u.CreatedAt) if err != nil { if isUnique(err) { _ = tx.Rollback() return s.Login(ctx, username, password) } return nil, err } if err := tx.Commit(); err != nil { return nil, err } return u, nil } func (s *PostgresStore) EnsurePlatformAdmin(ctx context.Context, username, password, displayName string) (*User, error) { hash, err := hashPassword(password) if err != nil { return nil, err } name := displayName if name == "" { name = username } var userID int64 err = s.DB.QueryRowContext(ctx, `SELECT user_id FROM platform_meta.users WHERE username=$1`, username).Scan(&userID) if err == nil { _, err = s.DB.ExecContext(ctx, ` UPDATE platform_meta.users SET password_hash=$1, role=$2, status='active', tenant_id=NULL, org_unit_id=NULL, display_name=CASE WHEN $3='' THEN display_name ELSE $3 END WHERE user_id=$4`, hash, authx.Role超级管理员, name, userID) if err != nil { return nil, err } return s.GetByID(ctx, userID) } if !errors.Is(err, sql.ErrNoRows) { return nil, err } u := &User{TenantID: 0, Username: username, DisplayName: name, Role: authx.Role超级管理员, Status: StatusActive} err = s.DB.QueryRowContext(ctx, ` INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status) VALUES(NULL,$1,$2,$3,$4,'active') RETURNING user_id, created_at`, username, hash, name, authx.Role超级管理员, ).Scan(&u.UserID, &u.CreatedAt) if err != nil { if isUnique(err) { // 并发创建:再走一次 upsert return s.EnsurePlatformAdmin(ctx, username, password, displayName) } return nil, err } return u, nil } func (s *PostgresStore) ListTenants(ctx context.Context) ([]TenantInfo, error) { rows, err := s.DB.QueryContext(ctx, ` SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at, (SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active') AS user_count, (SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id) AS app_count FROM platform_meta.tenants t ORDER BY t.tenant_id`) if err != nil { return nil, err } defer rows.Close() var out []TenantInfo for rows.Next() { var t TenantInfo if err := rows.Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount); err != nil { return nil, err } out = append(out, t) } return out, rows.Err() } func (s *PostgresStore) GetTenant(ctx context.Context, tenantID int64) (*TenantInfo, error) { var t TenantInfo err := s.DB.QueryRowContext(ctx, ` SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at, (SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active'), (SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id) FROM platform_meta.tenants t WHERE t.tenant_id=$1`, tenantID, ).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("tenant not found") } if err != nil { return nil, err } return &t, nil } func (s *PostgresStore) GetTenantBySlug(ctx context.Context, slug string) (*TenantInfo, error) { ns, err := NormalizeTenantSlug(slug) if err != nil { return nil, err } var t TenantInfo err = s.DB.QueryRowContext(ctx, ` SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at, (SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active'), (SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id) FROM platform_meta.tenants t WHERE t.slug=$1`, ns, ).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("tenant not found") } if err != nil { return nil, err } return &t, nil } func (s *PostgresStore) CreateTenant(ctx context.Context, name, slug string) (*TenantInfo, error) { name = strings.TrimSpace(name) if name == "" { return nil, fmt.Errorf("name required") } if strings.TrimSpace(slug) == "" { slug = SuggestTenantSlug(name) } ns, err := NormalizeTenantSlug(slug) if err != nil { return nil, err } var t TenantInfo err = s.DB.QueryRowContext(ctx, `INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id, name, slug, created_at`, name, ns, ).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt) if err != nil { if isUnique(err) { return nil, fmt.Errorf("slug already exists: %s", ns) } return nil, err } return &t, nil } func (s *PostgresStore) UpdateTenant(ctx context.Context, tenantID int64, name, slug string) (*TenantInfo, error) { name = strings.TrimSpace(name) if name == "" { return nil, fmt.Errorf("name required") } if strings.TrimSpace(slug) == "" { res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1 WHERE tenant_id=$2`, name, tenantID) if err != nil { return nil, err } n, _ := res.RowsAffected() if n == 0 { return nil, fmt.Errorf("tenant not found") } return s.GetTenant(ctx, tenantID) } ns, err := NormalizeTenantSlug(slug) if err != nil { return nil, err } res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1, slug=$2 WHERE tenant_id=$3`, name, ns, tenantID) if err != nil { if isUnique(err) { return nil, fmt.Errorf("slug already exists: %s", ns) } return nil, err } n, _ := res.RowsAffected() if n == 0 { return nil, fmt.Errorf("tenant not found") } return s.GetTenant(ctx, tenantID) } func (s *PostgresStore) EnsureTenantSlugs(ctx context.Context) error { rows, err := s.DB.QueryContext(ctx, `SELECT tenant_id, name FROM platform_meta.tenants WHERE COALESCE(slug,'')='' ORDER BY tenant_id`) if err != nil { return err } defer rows.Close() type row struct { id int64 name string } var need []row for rows.Next() { var r row if err := rows.Scan(&r.id, &r.name); err != nil { return err } need = append(need, r) } if err := rows.Err(); err != nil { return err } for _, r := range need { base := SuggestTenantSlug(r.name) if base == "" { base = fmt.Sprintf("t%d", r.id) } cand := base for i := 2; i < 100; i++ { ns, nerr := NormalizeTenantSlug(cand) if nerr != nil { cand = fmt.Sprintf("t%d", r.id) ns = cand } _, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET slug=$1 WHERE tenant_id=$2 AND COALESCE(slug,'')=''`, ns, r.id) if err == nil { break } if isUnique(err) { cand = fmt.Sprintf("%s-%d", base, i) continue } return err } } // 演示公司固定 slug=demo(若仍空或为自动生成) _, _ = s.DB.ExecContext(ctx, ` UPDATE platform_meta.tenants SET slug='demo' WHERE name IN ('演示公司','演示账号') AND (COALESCE(slug,'')='' OR slug LIKE 't%' OR slug='yan-shi') AND NOT EXISTS (SELECT 1 FROM platform_meta.tenants x WHERE x.slug='demo' AND x.name NOT IN ('演示公司','演示账号'))`) return nil } func (s *PostgresStore) RenameTenantIf(ctx context.Context, oldName, newName string) error { oldName = strings.TrimSpace(oldName) newName = strings.TrimSpace(newName) if oldName == "" || newName == "" || oldName == newName { return nil } _, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1 WHERE name=$2`, newName, oldName) return err } func (s *MemoryStore) RenameTenantIf(_ context.Context, oldName, newName string) error { s.mu.Lock() defer s.mu.Unlock() oldName = strings.TrimSpace(oldName) newName = strings.TrimSpace(newName) if oldName == "" || newName == "" { return nil } for _, t := range s.tenants { if t.Name == oldName { t.Name = newName } } return nil } func (s *PostgresStore) ListMembers(ctx context.Context, tenantID int64) ([]User, error) { rows, err := s.DB.QueryContext(ctx, ` SELECT user_id, COALESCE(tenant_id,0), COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at FROM platform_meta.users WHERE tenant_id=$1 ORDER BY user_id`, tenantID) if err != nil { return nil, err } defer rows.Close() var out []User for rows.Next() { var u User if err := rows.Scan(&u.UserID, &u.TenantID, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt); err != nil { return nil, err } u.Role = authx.NormalizeRole(u.Role) if authx.IsPlatformAdmin(u.Role) { continue } out = append(out, u) } return out, rows.Err() } func (s *PostgresStore) UsernameExists(ctx context.Context, username string) (bool, error) { var n int err := s.DB.QueryRowContext(ctx, `SELECT 1 FROM platform_meta.users WHERE username=$1 LIMIT 1`, strings.TrimSpace(username)).Scan(&n) if errors.Is(err, sql.ErrNoRows) { return false, nil } if err != nil { return false, err } return true, nil } func (s *PostgresStore) CreateMember(ctx context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) { if tenantID <= 0 { return nil, "", fmt.Errorf("tenant required") } if _, err := s.GetTenant(ctx, tenantID); err != nil { return nil, "", err } username = strings.TrimSpace(username) if len(username) < 3 { return nil, "", fmt.Errorf("username>=3 required") } plain := strings.TrimSpace(password) var err error if plain == "" { plain, err = RandomPassword(12) if err != nil { return nil, "", err } } if len(plain) < 6 { return nil, "", fmt.Errorf("password>=6 required") } if !authx.ValidPlatformRole(role) { return nil, "", fmt.Errorf("invalid role") } role = authx.NormalizeRole(role) hash, err := hashPassword(plain) if err != nil { return nil, "", err } name := strings.TrimSpace(displayName) if name == "" { name = username } u := &User{TenantID: tenantID, OrgUnitID: orgUnitID, Username: username, DisplayName: name, Role: role, Status: StatusActive} err = s.DB.QueryRowContext(ctx, ` INSERT INTO platform_meta.users(tenant_id, org_unit_id, username, password_hash, display_name, role, status) VALUES($1,NULLIF($2,0),$3,$4,$5,$6,'active') RETURNING user_id, created_at`, tenantID, orgUnitID, username, hash, name, role, ).Scan(&u.UserID, &u.CreatedAt) if err != nil { if isUnique(err) { return nil, "", fmt.Errorf("username already exists") } return nil, "", err } return u, plain, nil } func (s *PostgresStore) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error { var hash string err := s.DB.QueryRowContext(ctx, `SELECT password_hash FROM platform_meta.users WHERE user_id=$1`, userID).Scan(&hash) if errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("user not found") } if err != nil { return err } if bcrypt.CompareHashAndPassword([]byte(hash), []byte(oldPassword)) != nil { return fmt.Errorf("旧密码不正确") } newPassword = strings.TrimSpace(newPassword) if len(newPassword) < 6 { return fmt.Errorf("password>=6 required") } nh, err := hashPassword(newPassword) if err != nil { return err } _, err = s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET password_hash=$1 WHERE user_id=$2`, nh, userID) return err } func (s *PostgresStore) SetPassword(ctx context.Context, userID int64, password string) (string, error) { plain := strings.TrimSpace(password) var err error if plain == "" { plain, err = RandomPassword(12) if err != nil { return "", err } } if len(plain) < 6 { return "", fmt.Errorf("password>=6 required") } nh, err := hashPassword(plain) if err != nil { return "", err } res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET password_hash=$1 WHERE user_id=$2`, nh, userID) if err != nil { return "", err } n, _ := res.RowsAffected() if n == 0 { return "", fmt.Errorf("user not found") } return plain, nil } func (s *PostgresStore) UpdateMember(ctx context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) { cur, err := s.GetByID(ctx, userID) if err != nil { return nil, err } if cur.TenantID != tenantID { return nil, fmt.Errorf("member not found") } if authx.IsPlatformAdmin(cur.Role) { return nil, fmt.Errorf("cannot edit platform admin") } if role != "" { if !authx.ValidPlatformRole(role) { return nil, fmt.Errorf("invalid role") } role = authx.NormalizeRole(role) } else { role = cur.Role } if status == "" { status = cur.Status } if status != StatusActive && status != StatusPending && status != "disabled" { return nil, fmt.Errorf("invalid status") } _, err = s.DB.ExecContext(ctx, ` UPDATE platform_meta.users SET role=$1, org_unit_id=NULLIF($2,0), status=$3 WHERE user_id=$4 AND tenant_id=$5`, role, orgUnitID, status, userID, tenantID) if err != nil { return nil, err } return s.GetByID(ctx, userID) } func (s *PostgresStore) Login(ctx context.Context, account, password string) (*User, error) { account = strings.TrimSpace(account) u, hash, err := s.loadAuthByUsername(ctx, account) if errors.Is(err, sql.ErrNoRows) { if phone, nerr := NormalizePhone(account); nerr == nil { u, hash, err = s.loadAuthByPhone(ctx, phone) } } if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("invalid username or password") } if err != nil { return nil, err } if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil { return nil, fmt.Errorf("invalid username or password") } u.Role = authx.NormalizeRole(u.Role) if u.Status == "disabled" { return nil, fmt.Errorf("账号已停用") } return u, nil } func (s *PostgresStore) loadAuthByUsername(ctx context.Context, username string) (*User, string, error) { var u User var hash string var tenant sql.NullInt64 err := s.DB.QueryRowContext(ctx, ` SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), password_hash, display_name, role, COALESCE(status,'active'), created_at FROM platform_meta.users WHERE username=$1`, username, ).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &hash, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt) if err != nil { return nil, "", err } if tenant.Valid { u.TenantID = tenant.Int64 } return &u, hash, nil } func (s *PostgresStore) loadAuthByPhone(ctx context.Context, phone string) (*User, string, error) { var u User var hash string var tenant sql.NullInt64 err := s.DB.QueryRowContext(ctx, ` SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), password_hash, display_name, role, COALESCE(status,'active'), created_at FROM platform_meta.users WHERE phone=$1`, phone, ).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &hash, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt) if err != nil { return nil, "", err } if tenant.Valid { u.TenantID = tenant.Int64 } return &u, hash, nil } func (s *PostgresStore) GetByID(ctx context.Context, userID int64) (*User, error) { var u User var tenant sql.NullInt64 err := s.DB.QueryRowContext(ctx, ` SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at FROM platform_meta.users WHERE user_id=$1`, userID, ).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("user not found") } if err != nil { return nil, err } if tenant.Valid { u.TenantID = tenant.Int64 } u.Role = authx.NormalizeRole(u.Role) return &u, nil } func (s *PostgresStore) GetByPhone(ctx context.Context, phone string) (*User, error) { ns, err := NormalizePhone(phone) if err != nil { return nil, err } var u User var tenant sql.NullInt64 err = s.DB.QueryRowContext(ctx, ` SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at FROM platform_meta.users WHERE phone=$1`, ns, ).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("user not found") } if err != nil { return nil, err } if tenant.Valid { u.TenantID = tenant.Int64 } u.Role = authx.NormalizeRole(u.Role) return &u, nil } func (s *PostgresStore) PhoneExists(ctx context.Context, phone string, excludeUserID int64) (bool, error) { phone = strings.TrimSpace(phone) if phone == "" { return false, nil } var n int err := s.DB.QueryRowContext(ctx, ` SELECT 1 FROM platform_meta.users WHERE phone=$1 AND user_id<>$2 LIMIT 1`, phone, excludeUserID).Scan(&n) if errors.Is(err, sql.ErrNoRows) { return false, nil } if err != nil { return false, err } return true, nil } func (s *PostgresStore) BindPhone(ctx context.Context, userID int64, phone string) (*User, error) { phone = strings.TrimSpace(phone) if phone == "" { _, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET phone='', username_login_disabled=false WHERE user_id=$1`, userID) if err != nil { return nil, err } return s.GetByID(ctx, userID) } ns, err := NormalizePhone(phone) if err != nil { return nil, err } exists, err := s.PhoneExists(ctx, ns, userID) if err != nil { return nil, err } if exists { return nil, fmt.Errorf("该手机号已被绑定") } res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET phone=$1 WHERE user_id=$2`, ns, userID) if err != nil { if isUnique(err) { return nil, fmt.Errorf("该手机号已被绑定") } return nil, err } n, _ := res.RowsAffected() if n == 0 { return nil, fmt.Errorf("user not found") } return s.GetByID(ctx, userID) } func (s *PostgresStore) SetUsernameLoginDisabled(ctx context.Context, userID int64, disabled bool) (*User, error) { u, err := s.GetByID(ctx, userID) if err != nil { return nil, err } if disabled && strings.TrimSpace(u.Phone) == "" { return nil, fmt.Errorf("请先绑定手机号,再禁用用户名登录") } _, err = s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET username_login_disabled=$1 WHERE user_id=$2`, disabled, userID) if err != nil { return nil, err } return s.GetByID(ctx, userID) } func (s *PostgresStore) JoinTenant(ctx context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) { if tenantID <= 0 { return nil, fmt.Errorf("invalid tenant") } role = normalizePlatformRole(role) res, err := s.DB.ExecContext(ctx, ` UPDATE platform_meta.users SET tenant_id=$1, role=$2, status='active', org_unit_id=NULLIF($3,0) WHERE user_id=$4 AND (tenant_id IS NULL OR status='pending' OR role='pending' OR role='待加入')`, tenantID, role, orgUnitID, userID) if err != nil { return nil, err } n, _ := res.RowsAffected() if n == 0 { u, gerr := s.GetByID(ctx, userID) if gerr != nil { return nil, fmt.Errorf("user not found") } if u.HasTenant() { return nil, fmt.Errorf("user already joined a tenant") } return nil, fmt.Errorf("join tenant failed") } return s.GetByID(ctx, userID) } func (s *PostgresStore) SetOrgUnit(ctx context.Context, userID, tenantID, orgUnitID int64) (*User, error) { res, err := s.DB.ExecContext(ctx, ` UPDATE platform_meta.users SET org_unit_id=NULLIF($1,0) WHERE user_id=$2 AND tenant_id=$3`, orgUnitID, userID, tenantID) if err != nil { return nil, err } n, _ := res.RowsAffected() if n == 0 { return nil, fmt.Errorf("user not found") } return s.GetByID(ctx, userID) } func (s *PostgresStore) CreateTenantAsOwner(ctx context.Context, userID int64, tenantName string) (*User, error) { u, err := s.GetByID(ctx, userID) if err != nil { return nil, err } if u.HasTenant() { return nil, fmt.Errorf("user already joined a tenant") } name := strings.TrimSpace(tenantName) if name == "" { name = u.DisplayName } if name == "" { name = u.Username } tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return nil, err } defer func() { _ = tx.Rollback() }() var tenantID int64 slug := SuggestTenantSlug(name) if slug == "" { slug = fmt.Sprintf("co%d", time.Now().Unix()%100000) } ns, nerr := NormalizeTenantSlug(slug) if nerr != nil { ns = fmt.Sprintf("t%d", time.Now().Unix()%1000000) } for i := 0; i < 20; i++ { try := ns if i > 0 { try = fmt.Sprintf("%s-%d", ns, i+1) } err = tx.QueryRowContext(ctx, `INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id`, name, try, ).Scan(&tenantID) if err == nil { break } if !isUnique(err) { return nil, err } } if err != nil { return nil, fmt.Errorf("create tenant slug conflict") } res, err := tx.ExecContext(ctx, ` UPDATE platform_meta.users SET tenant_id=$1, role='owner', status='active' WHERE user_id=$2 AND (tenant_id IS NULL OR status='pending' OR role='pending')`, tenantID, userID) if err != nil { return nil, err } n, _ := res.RowsAffected() if n == 0 { return nil, fmt.Errorf("user already joined a tenant") } if err := tx.Commit(); err != nil { return nil, err } return s.GetByID(ctx, userID) } // EnsureDemoUser 开发态确保 demo/demo123 为「演示公司」的公司管理员(不是超管自己)。 func EnsureDemoUser(ctx context.Context, store Store) { if store == nil { return } if _, err := store.Login(ctx, "demo", "demo123"); err != nil { _, _ = store.EnsureBootstrapOwner(ctx, "demo", "demo123", "演示用户", "演示公司") } _ = store.RenameTenantIf(ctx, "演示账号", "演示公司") _ = store.EnsureTenantSlugs(ctx) ensureDevPhone(ctx, store, "demo", "13800000001") } // EnsurePlatformAdminUser 确保平台超级管理员 ljk_admin / ljk_admin。 func EnsurePlatformAdminUser(ctx context.Context, store Store) { if store == nil { return } if _, err := store.EnsurePlatformAdmin(ctx, "ljk_admin", "ljk_admin", "平台超级管理员"); err != nil { fmt.Printf("ensure platform admin ljk_admin: %v\n", err) } ensureDevPhone(ctx, store, "ljk_admin", "13531041945") } func ensureDevPhone(ctx context.Context, store Store, username, phone string) { var user *User var err error switch username { case "demo": user, err = store.Login(ctx, "demo", "demo123") case "ljk_admin": user, err = store.Login(ctx, "ljk_admin", "ljk_admin") default: return } if err != nil || user == nil { return } want, nerr := NormalizePhone(phone) if nerr != nil { fmt.Printf("ensure phone for %s: %v\n", username, nerr) return } cur := strings.TrimSpace(user.Phone) if cur != want { if _, err := store.BindPhone(ctx, user.UserID, want); err != nil { fmt.Printf("ensure phone for %s: %v\n", username, err) return } } _, _ = store.SetUsernameLoginDisabled(ctx, user.UserID, true) } func hashPassword(pw string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) return string(b), err } func isUnique(err error) bool { return err != nil && (strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique")) } func normalizePlatformRole(role string) string { n := authx.NormalizeRole(role) if authx.ValidPlatformRole(n) { return n } return authx.Role编辑 }