382 lines
12 KiB
Go
382 lines
12 KiB
Go
package applogic
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"aijianzhan/platform/internal/authx"
|
||
"aijianzhan/platform/internal/invitestore"
|
||
"aijianzhan/platform/internal/types"
|
||
"aijianzhan/platform/internal/userstore"
|
||
)
|
||
|
||
type PlatformLogic struct {
|
||
*AuthLogic
|
||
}
|
||
|
||
type CreateTenantResult struct {
|
||
Tenant *userstore.TenantInfo `json:"tenant"`
|
||
AdminAccount *AdminAccountOut `json:"admin_account,omitempty"`
|
||
AdminInvite *invitestore.Invite `json:"admin_invite,omitempty"`
|
||
}
|
||
|
||
// AdminAccountOut 新建公司时下发的公司管理员凭据(密码仅此一次明文返回)。
|
||
type AdminAccountOut struct {
|
||
UserID int64 `json:"user_id"`
|
||
Username string `json:"username"`
|
||
Password string `json:"password,omitempty"`
|
||
Phone string `json:"phone,omitempty"`
|
||
UsernameLoginDisabled bool `json:"username_login_disabled,omitempty"`
|
||
DisplayName string `json:"display_name"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
type AdminInfo struct {
|
||
UserID int64 `json:"user_id"`
|
||
Username string `json:"username"`
|
||
Phone string `json:"phone"`
|
||
UsernameLoginDisabled bool `json:"username_login_disabled"`
|
||
DisplayName string `json:"display_name"`
|
||
Role string `json:"role"`
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
func NewPlatformLogic(l *AuthLogic) *PlatformLogic {
|
||
return &PlatformLogic{AuthLogic: l}
|
||
}
|
||
|
||
func (l *PlatformLogic) requireSuper() (*userstore.User, error) {
|
||
if !authx.IsPlatformAdmin(authx.Role(l.ctx)) {
|
||
return nil, fmt.Errorf("需要超级管理员")
|
||
}
|
||
u, err := l.svcCtx.Users.GetByID(l.ctx, authx.UserID(l.ctx))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !u.IsPlatformAdmin() {
|
||
return nil, fmt.Errorf("需要超级管理员")
|
||
}
|
||
return u, nil
|
||
}
|
||
|
||
func (l *PlatformLogic) ListTenants() ([]userstore.TenantInfo, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
return l.svcCtx.Users.ListTenants(l.ctx)
|
||
}
|
||
|
||
func (l *PlatformLogic) CreateTenant(name, slug string, withAdminInvite bool, adminPhone string) (*CreateTenantResult, error) {
|
||
super, err := l.requireSuper()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
phone := strings.TrimSpace(adminPhone)
|
||
if l.phoneLoginOnly() && phone == "" {
|
||
return nil, fmt.Errorf("本部署仅手机号登录,请填写管理员手机号")
|
||
}
|
||
t, err := l.svcCtx.Users.CreateTenant(l.ctx, strings.TrimSpace(name), strings.TrimSpace(slug))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if l.svcCtx.Roles != nil {
|
||
_ = l.svcCtx.Roles.EnsureDefaults(l.ctx, t.TenantID)
|
||
}
|
||
if l.svcCtx.TenantPerm != nil {
|
||
_ = l.svcCtx.TenantPerm.EnsureDefault(l.ctx, t.TenantID)
|
||
}
|
||
out := &CreateTenantResult{Tenant: t}
|
||
|
||
acc, aerr := l.createCompanyAdmin(t.TenantID, strings.TrimSpace(name), phone)
|
||
if aerr != nil {
|
||
return nil, fmt.Errorf("创建公司成功,但管理员账号生成失败: %v", aerr)
|
||
}
|
||
out.AdminAccount = acc
|
||
|
||
if withAdminInvite && l.svcCtx.Invites != nil {
|
||
inv, ierr := l.svcCtx.Invites.Create(l.ctx, t.TenantID, super.UserID, invitestore.CreateInput{
|
||
Role: authx.Role管理员,
|
||
MaxUses: 1,
|
||
ExpiresIn: 7 * 24 * time.Hour,
|
||
})
|
||
if ierr == nil {
|
||
out.AdminInvite = inv
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// createCompanyAdmin 生成随机唯一管理员;phone 非空时绑定手机。
|
||
func (l *PlatformLogic) createCompanyAdmin(tenantID int64, companyName, phone string) (*AdminAccountOut, error) {
|
||
uname, err := userstore.AllocUniqueUsername(l.ctx, l.svcCtx.Users.UsernameExists)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
display := strings.TrimSpace(companyName)
|
||
if display == "" {
|
||
display = "公司"
|
||
}
|
||
display += "管理员"
|
||
u, plain, err := l.svcCtx.Users.CreateMember(l.ctx, tenantID, uname, "", display, authx.Role管理员, 0)
|
||
if err != nil {
|
||
if strings.Contains(err.Error(), "already exists") {
|
||
uname2, e2 := userstore.AllocUniqueUsername(l.ctx, l.svcCtx.Users.UsernameExists)
|
||
if e2 != nil {
|
||
return nil, e2
|
||
}
|
||
u, plain, err = l.svcCtx.Users.CreateMember(l.ctx, tenantID, uname2, "", display, authx.Role管理员, 0)
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
out := &AdminAccountOut{
|
||
UserID: u.UserID, Username: u.Username, Password: plain,
|
||
DisplayName: u.DisplayName, Role: u.Role,
|
||
}
|
||
phone = strings.TrimSpace(phone)
|
||
if phone == "" {
|
||
return out, nil
|
||
}
|
||
ns, nerr := userstore.NormalizePhone(phone)
|
||
if nerr != nil {
|
||
return nil, nerr
|
||
}
|
||
nu, berr := l.svcCtx.Users.BindPhone(l.ctx, u.UserID, ns)
|
||
if berr != nil {
|
||
return nil, fmt.Errorf("账号已创建但绑定手机失败: %v", berr)
|
||
}
|
||
out.Phone = nu.Phone
|
||
return out, nil
|
||
}
|
||
|
||
// IssueAdminAccount 为已有公司再发一个管理员账号;有期限控制时手机号可选。
|
||
func (l *PlatformLogic) IssueAdminAccount(tenantID int64, adminPhone string) (*AdminAccountOut, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
if l.phoneLoginOnly() && strings.TrimSpace(adminPhone) == "" {
|
||
return nil, fmt.Errorf("本部署仅手机号登录,请填写管理员手机号")
|
||
}
|
||
t, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return l.createCompanyAdmin(t.TenantID, t.Name, adminPhone)
|
||
}
|
||
|
||
// ListCompanyAdmins 列出该公司人类管理员账号(用户名唯一不可改)。
|
||
func (l *PlatformLogic) ListCompanyAdmins(tenantID int64) ([]AdminInfo, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID); err != nil {
|
||
return nil, err
|
||
}
|
||
members, err := l.svcCtx.Users.ListMembers(l.ctx, tenantID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]AdminInfo, 0)
|
||
for _, u := range members {
|
||
if !authx.IsCompanyAdmin(u.Role) {
|
||
continue
|
||
}
|
||
out = append(out, AdminInfo{
|
||
UserID: u.UserID, Username: u.Username, Phone: u.Phone,
|
||
UsernameLoginDisabled: u.UsernameLoginDisabled,
|
||
DisplayName: u.DisplayName, Role: u.Role, Status: u.Status,
|
||
})
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// UpdateCompanyAdmin 平台超管重置该公司管理员密码 / 绑定手机 / 禁用用户名登录(用户名不变)。
|
||
func (l *PlatformLogic) UpdateCompanyAdmin(tenantID, userID int64, resetPassword bool, password string, phone *string, disableUsernameLogin *bool) (*AdminAccountOut, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID); err != nil {
|
||
return nil, err
|
||
}
|
||
u, err := l.svcCtx.Users.GetByID(l.ctx, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if u.TenantID != tenantID {
|
||
return nil, fmt.Errorf("成员不属于该公司")
|
||
}
|
||
if !authx.IsCompanyAdmin(u.Role) {
|
||
return nil, fmt.Errorf("仅可管理公司管理员账号")
|
||
}
|
||
if authx.IsPlatformAdmin(u.Role) {
|
||
return nil, fmt.Errorf("cannot edit platform admin")
|
||
}
|
||
out := &AdminAccountOut{
|
||
UserID: u.UserID, Username: u.Username, Phone: u.Phone,
|
||
UsernameLoginDisabled: u.UsernameLoginDisabled,
|
||
DisplayName: u.DisplayName, Role: u.Role,
|
||
}
|
||
if resetPassword {
|
||
plain, err := l.svcCtx.Users.SetPassword(l.ctx, userID, password)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out.Password = plain
|
||
}
|
||
if phone != nil {
|
||
if l.phoneLoginOnly() && strings.TrimSpace(*phone) == "" {
|
||
return nil, fmt.Errorf("本部署仅支持手机号登录,不可解绑手机号")
|
||
}
|
||
nu, err := l.svcCtx.Users.BindPhone(l.ctx, userID, *phone)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out.Phone = nu.Phone
|
||
out.DisplayName = nu.DisplayName
|
||
out.UsernameLoginDisabled = nu.UsernameLoginDisabled
|
||
}
|
||
if disableUsernameLogin != nil {
|
||
nu, err := l.svcCtx.Users.SetUsernameLoginDisabled(l.ctx, userID, *disableUsernameLogin)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out.Phone = nu.Phone
|
||
out.UsernameLoginDisabled = nu.UsernameLoginDisabled
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (l *PlatformLogic) UpdateTenant(tenantID int64, name, slug string) (*userstore.TenantInfo, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
return l.svcCtx.Users.UpdateTenant(l.ctx, tenantID, name, slug)
|
||
}
|
||
|
||
func (l *PlatformLogic) GetTenantPerms(tenantID int64) ([]string, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
if l.svcCtx.TenantPerm == nil {
|
||
return authx.CompanyPermCatalog(), nil
|
||
}
|
||
if _, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID); err != nil {
|
||
return nil, err
|
||
}
|
||
return l.svcCtx.TenantPerm.Get(l.ctx, tenantID)
|
||
}
|
||
|
||
func (l *PlatformLogic) SetTenantPerms(tenantID int64, perms []string) ([]string, error) {
|
||
if _, err := l.requireSuper(); err != nil {
|
||
return nil, err
|
||
}
|
||
if l.svcCtx.TenantPerm == nil {
|
||
return nil, fmt.Errorf("tenant perm store unavailable")
|
||
}
|
||
if _, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := l.svcCtx.TenantPerm.Set(l.ctx, tenantID, perms); err != nil {
|
||
return nil, err
|
||
}
|
||
return l.svcCtx.TenantPerm.Get(l.ctx, tenantID)
|
||
}
|
||
|
||
func (l *PlatformLogic) IssueAdminInvite(tenantID int64) (*invitestore.Invite, error) {
|
||
super, err := l.requireSuper()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if l.svcCtx.Invites == nil {
|
||
return nil, fmt.Errorf("invite store unavailable")
|
||
}
|
||
if _, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID); err != nil {
|
||
return nil, err
|
||
}
|
||
return l.svcCtx.Invites.Create(l.ctx, tenantID, super.UserID, invitestore.CreateInput{
|
||
Role: authx.Role管理员,
|
||
MaxUses: 1,
|
||
ExpiresIn: 7 * 24 * time.Hour,
|
||
})
|
||
}
|
||
|
||
// EnterTenant 进入某公司上下文(JWT 仍为超级管理员,但带上 tenant_id)。
|
||
func (l *PlatformLogic) EnterTenant(tenantID int64) (*types.TokenResp, error) {
|
||
super, err := l.requireSuper()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if tenantID <= 0 {
|
||
return nil, fmt.Errorf("tenant_id required")
|
||
}
|
||
t, err := l.svcCtx.Users.GetTenant(l.ctx, tenantID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp, err := l.issue(t.TenantID, super.UserID, authx.Role超级管理员, super.Username, super.DisplayName, 0)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp.Phone = super.Phone
|
||
resp.Status = userstore.StatusActive
|
||
resp.TenantName = t.Name
|
||
resp.Message = fmt.Sprintf("已打开「%s」的管理视图(你仍是平台超管,不是该公司账号)", t.Name)
|
||
return resp, nil
|
||
}
|
||
|
||
// ExitTenant 退出公司管理视图,回到平台工作台(tenant_id=0)。
|
||
func (l *PlatformLogic) ExitTenant() (*types.TokenResp, error) {
|
||
super, err := l.requireSuper()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp, err := l.issue(0, super.UserID, authx.Role超级管理员, super.Username, super.DisplayName, 0)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp.Phone = super.Phone
|
||
resp.Status = userstore.StatusActive
|
||
resp.Message = "已回到平台工作台"
|
||
return resp, nil
|
||
}
|
||
|
||
// CompanyEntitlements 本公司可用权限。
|
||
func (l *AuthLogic) CompanyEntitlements() ([]string, []authx.PermModule, error) {
|
||
tid := authx.TenantID(l.ctx)
|
||
if tid <= 0 {
|
||
return nil, nil, fmt.Errorf("missing tenant")
|
||
}
|
||
var perms []string
|
||
var err error
|
||
// 超管进入公司后看全量权限模块,便于代管(不受该公司额度裁剪展示)
|
||
if authx.IsPlatformAdmin(authx.Role(l.ctx)) {
|
||
perms = authx.CompanyPermCatalog()
|
||
} else if l.svcCtx.TenantPerm != nil {
|
||
perms, err = l.svcCtx.TenantPerm.Get(l.ctx, tid)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
} else {
|
||
perms = authx.CompanyPermCatalog()
|
||
}
|
||
allow := map[string]struct{}{}
|
||
for _, p := range perms {
|
||
allow[p] = struct{}{}
|
||
}
|
||
var modules []authx.PermModule
|
||
for _, m := range authx.PermModules() {
|
||
var items []authx.PermItem
|
||
for _, it := range m.Items {
|
||
if _, ok := allow[it.Perm]; ok {
|
||
items = append(items, it)
|
||
}
|
||
}
|
||
if len(items) > 0 {
|
||
modules = append(modules, authx.PermModule{Title: m.Title, Items: items})
|
||
}
|
||
}
|
||
return perms, modules, nil
|
||
}
|