chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
244
platform/internal/logic/applogic/agents.go
Normal file
244
platform/internal/logic/applogic/agents.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"aijianzhan/platform/internal/agentcap"
|
||||
"aijianzhan/platform/internal/agentstore"
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/tenantperm"
|
||||
"aijianzhan/platform/internal/types"
|
||||
)
|
||||
|
||||
type AgentAdminLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewAgentAdminLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgentAdminLogic {
|
||||
return &AgentAdminLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) store() (agentstore.Store, error) {
|
||||
if l.svcCtx.Agents == nil {
|
||||
return nil, fmt.Errorf("agent store unavailable")
|
||||
}
|
||||
return l.svcCtx.Agents, nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) List() ([]agentstore.Account, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := st.List(l.ctx, authx.TenantID(l.ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range items {
|
||||
l.attachRole(&items[i])
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) Get(agentID int64) (*agentstore.Account, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, err := st.Get(l.ctx, authx.TenantID(l.ctx), agentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.attachRole(acc)
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) attachRole(acc *agentstore.Account) {
|
||||
if acc == nil || acc.RoleID <= 0 || l.svcCtx.Roles == nil {
|
||||
return
|
||||
}
|
||||
role, err := l.svcCtx.Roles.Get(l.ctx, authx.TenantID(l.ctx), acc.RoleID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
acc.RoleCode = role.Code
|
||||
acc.RoleName = role.Name
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) resolvePerms(roleID int64, fallback []string) (int64, []string, error) {
|
||||
if roleID <= 0 {
|
||||
return 0, authx.NormalizePerms(fallback), nil
|
||||
}
|
||||
if l.svcCtx.Roles == nil {
|
||||
return 0, nil, fmt.Errorf("role store unavailable")
|
||||
}
|
||||
role, err := l.svcCtx.Roles.Get(l.ctx, authx.TenantID(l.ctx), roleID)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return role.RoleID, authx.NormalizePerms(append([]string{}, role.Permissions...)), nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) Create(req *types.AgentCreateReq) (*types.AgentCreateResp, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleID, perms, err := l.resolvePerms(req.RoleID, req.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if roleID <= 0 && len(perms) == 0 {
|
||||
return nil, fmt.Errorf("role_id or permissions required")
|
||||
}
|
||||
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, secret, err := st.Create(l.ctx, authx.TenantID(l.ctx), authx.UserID(l.ctx), agentstore.CreateInput{
|
||||
Name: req.Name,
|
||||
Perms: perms,
|
||||
AppSlugs: req.AppSlugs,
|
||||
Status: agentstore.StatusActive,
|
||||
RoleID: roleID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.attachRole(acc)
|
||||
return &types.AgentCreateResp{Account: *acc, ClientSecret: secret}, nil
|
||||
}
|
||||
|
||||
func (l *AuthLogic) SelfRegisterAgent(req *types.AgentSelfRegisterReq) (*types.AgentSelfRegisterResp, error) {
|
||||
if l.svcCtx.Agents == nil {
|
||||
return nil, fmt.Errorf("agent store unavailable")
|
||||
}
|
||||
want := l.svcCtx.Config.Agent.RegisterSecret
|
||||
if want == "" {
|
||||
want = l.svcCtx.Config.Auth.IssueSecret
|
||||
}
|
||||
if want == "" {
|
||||
want = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
if req.RegisterSecret == "" || req.RegisterSecret != want {
|
||||
return nil, fmt.Errorf("invalid register secret")
|
||||
}
|
||||
tenantID := req.TenantID
|
||||
if tenantID <= 0 {
|
||||
tenantID = 1
|
||||
}
|
||||
acc, secret, reused, err := l.svcCtx.Agents.Register(l.ctx, tenantID, req.Name, req.HostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := "已登记为 pending,请管理员在控制台分配角色并启用后再换票"
|
||||
if reused {
|
||||
msg = "已存在 pending 登记,已轮换 client_secret;仍须管理员分配角色并启用"
|
||||
}
|
||||
return &types.AgentSelfRegisterResp{
|
||||
Account: *acc,
|
||||
ClientSecret: secret,
|
||||
Reused: reused,
|
||||
Message: msg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) Update(agentID int64, req *types.AgentUpdateReq) (*agentstore.Account, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in := agentstore.UpdateInput{}
|
||||
if req.Name != nil {
|
||||
in.Name = req.Name
|
||||
}
|
||||
if req.Status != nil {
|
||||
in.Status = req.Status
|
||||
}
|
||||
if req.AppSlugs != nil {
|
||||
in.AppSlugs = req.AppSlugs
|
||||
}
|
||||
if req.RoleID != nil {
|
||||
roleID, perms, err := l.resolvePerms(*req.RoleID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in.RoleID = &roleID
|
||||
in.Perms = &perms
|
||||
} else if req.Permissions != nil {
|
||||
n := authx.NormalizePerms(*req.Permissions)
|
||||
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in.Perms = &n
|
||||
}
|
||||
acc, err := st.Update(l.ctx, authx.TenantID(l.ctx), agentID, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.attachRole(acc)
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) Rotate(agentID int64) (*types.AgentSecretResp, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret, err := st.RotateSecret(l.ctx, authx.TenantID(l.ctx), agentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, err := st.Get(l.ctx, authx.TenantID(l.ctx), agentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.AgentSecretResp{ClientID: acc.ClientID, ClientSecret: secret}, nil
|
||||
}
|
||||
|
||||
func (l *AgentAdminLogic) Delete(agentID int64) error {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.Delete(l.ctx, authx.TenantID(l.ctx), agentID)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) IssueClientCredentials(clientID, clientSecret string) (*types.TokenResp, error) {
|
||||
if l.svcCtx.Agents == nil {
|
||||
return nil, fmt.Errorf("agent store unavailable")
|
||||
}
|
||||
acc, err := l.svcCtx.Agents.Authenticate(l.ctx, strings.TrimSpace(clientID), clientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, exp, err := authx.IssueAgentToken(l.svcCtx.JWT, acc.TenantID, acc.AgentID, acc.Perms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = l.svcCtx.Agents.TouchToken(l.ctx, acc.AgentID)
|
||||
secret := l.svcCtx.Config.Agent.CapsuleSecret
|
||||
if secret == "" {
|
||||
secret = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
return &types.TokenResp{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: exp,
|
||||
TenantID: acc.TenantID,
|
||||
UserID: acc.AgentID,
|
||||
Username: acc.ClientID,
|
||||
DisplayName: acc.Name,
|
||||
Role: authx.RoleAgent,
|
||||
AgentKey: agentcap.PublicAgentKey(secret, acc.TenantID, acc.AgentID),
|
||||
AgentID: acc.AgentID,
|
||||
Permissions: append([]string{}, acc.Perms...),
|
||||
AppSlugs: append([]string{}, acc.AppSlugs...),
|
||||
}, nil
|
||||
}
|
||||
91
platform/internal/logic/applogic/aggregate.go
Normal file
91
platform/internal/logic/applogic/aggregate.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/types"
|
||||
)
|
||||
|
||||
func (l *CrudLogic) Aggregate(slug, resource, groupBy, sumField string) (*types.AggregateResp, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupBy != "" {
|
||||
ok := false
|
||||
if ref.Resource.List != nil {
|
||||
for _, f := range ref.Resource.List.AllowedFilters {
|
||||
if f == groupBy {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range ref.Entity.Fields {
|
||||
if f.Name == groupBy {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("group_by not allowed: %s", groupBy)
|
||||
}
|
||||
}
|
||||
items, total, err := l.svcCtx.CRUD.List(l.ctx, ref, tenantID, 1, 5000, nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &types.AggregateResp{Total: total, GroupBy: groupBy, SumField: sumField}
|
||||
if groupBy == "" {
|
||||
if sumField != "" {
|
||||
for _, it := range items {
|
||||
resp.Sum += toFloat(it[sumField])
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
buckets := map[string]*types.AggregateBucket{}
|
||||
order := []string{}
|
||||
for _, it := range items {
|
||||
key := fmt.Sprint(it[groupBy])
|
||||
if key == "" || key == "<nil>" {
|
||||
key = "(空)"
|
||||
}
|
||||
b, ok := buckets[key]
|
||||
if !ok {
|
||||
b = &types.AggregateBucket{Key: key}
|
||||
buckets[key] = b
|
||||
order = append(order, key)
|
||||
}
|
||||
b.Count++
|
||||
if sumField != "" {
|
||||
b.Sum += toFloat(it[sumField])
|
||||
resp.Sum += toFloat(it[sumField])
|
||||
}
|
||||
}
|
||||
for _, k := range order {
|
||||
resp.Buckets = append(resp.Buckets, *buckets[k])
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func toFloat(v any) float64 {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return t
|
||||
case float32:
|
||||
return float64(t)
|
||||
case int:
|
||||
return float64(t)
|
||||
case int64:
|
||||
return float64(t)
|
||||
case string:
|
||||
n, _ := strconv.ParseFloat(t, 64)
|
||||
return n
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
393
platform/internal/logic/applogic/auth.go
Normal file
393
platform/internal/logic/applogic/auth.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"aijianzhan/platform/internal/agentcap"
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/meta"
|
||||
"aijianzhan/platform/internal/smsstore"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/types"
|
||||
"aijianzhan/platform/internal/userstore"
|
||||
)
|
||||
|
||||
type AuthLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AuthLogic {
|
||||
return &AuthLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *AuthLogic) IssueToken(req *types.TokenReq) (*types.TokenResp, error) {
|
||||
gt := strings.TrimSpace(strings.ToLower(req.GrantType))
|
||||
if gt == "client_credentials" || (req.ClientID != "" && req.ClientSecret != "") {
|
||||
return l.IssueClientCredentials(req.ClientID, req.ClientSecret)
|
||||
}
|
||||
want := l.svcCtx.Config.Auth.IssueSecret
|
||||
if want == "" {
|
||||
want = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
if req.Secret == "" || req.Secret != want {
|
||||
return nil, fmt.Errorf("invalid issue secret")
|
||||
}
|
||||
role := req.Role
|
||||
if role == "" {
|
||||
role = authx.Role管理员
|
||||
}
|
||||
return l.issue(req.TenantID, req.UserID, role, "", "", 0)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) Register(req *types.RegisterReq) (*types.TokenResp, error) {
|
||||
if l.phoneLoginOnly() {
|
||||
return nil, fmt.Errorf("本系统仅支持手机号登录,请联系管理员开通账号并绑定手机")
|
||||
}
|
||||
if l.svcCtx.Users == nil {
|
||||
return nil, fmt.Errorf("user store unavailable")
|
||||
}
|
||||
u, err := l.svcCtx.Users.Register(l.ctx, req.Username, req.Password, req.DisplayName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.issueUser(u)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) phoneLoginOnly() bool {
|
||||
// 有部署期限控制时,不必强制手机号登录
|
||||
if l.svcCtx.Config.License.Enabled {
|
||||
return false
|
||||
}
|
||||
return l.svcCtx.Config.Auth.PhoneLoginOnly
|
||||
}
|
||||
|
||||
// PhoneLoginOnlyPolicy 对外暴露:是否强制仅手机号登录(License 开启时为 false)。
|
||||
func (l *AuthLogic) PhoneLoginOnlyPolicy() bool {
|
||||
return l.phoneLoginOnly()
|
||||
}
|
||||
|
||||
func (l *AuthLogic) Login(req *types.LoginReq) (*types.TokenResp, error) {
|
||||
if l.svcCtx.Users == nil {
|
||||
return nil, fmt.Errorf("user store unavailable")
|
||||
}
|
||||
smsCode := strings.TrimSpace(req.SMSCode)
|
||||
phone := strings.TrimSpace(req.Phone)
|
||||
account := strings.TrimSpace(req.Username)
|
||||
if account == "" {
|
||||
account = phone
|
||||
}
|
||||
|
||||
// 手机号 + 短信验证码
|
||||
if smsCode != "" {
|
||||
if phone == "" {
|
||||
phone = account
|
||||
}
|
||||
ns, err := userstore.NormalizePhone(phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.svcCtx.SMS == nil {
|
||||
return nil, fmt.Errorf("短信服务未启用")
|
||||
}
|
||||
if err := l.svcCtx.SMS.Consume(smsstore.PurposeLogin, ns, smsCode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := l.svcCtx.Users.GetByPhone(l.ctx, ns)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("该手机号未绑定账号")
|
||||
}
|
||||
if u.Status == "disabled" {
|
||||
return nil, fmt.Errorf("账号已停用")
|
||||
}
|
||||
return l.issueUser(u)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Password) == "" {
|
||||
return nil, fmt.Errorf("请填写密码或短信验证码")
|
||||
}
|
||||
|
||||
// 仅手机号登录:必须用手机号+密码
|
||||
if l.phoneLoginOnly() {
|
||||
loginPhone := phone
|
||||
if loginPhone == "" {
|
||||
loginPhone = account
|
||||
}
|
||||
ns, err := userstore.NormalizePhone(loginPhone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请使用手机号登录")
|
||||
}
|
||||
u, err := l.svcCtx.Users.Login(l.ctx, ns, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := l.enforceLoginPolicy(u, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.issueUser(u)
|
||||
}
|
||||
|
||||
// 兼容模式:用户名/手机号 + 密码
|
||||
if account == "" {
|
||||
return nil, fmt.Errorf("请填写用户名或手机号")
|
||||
}
|
||||
u, err := l.svcCtx.Users.Login(l.ctx, account, req.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viaUsername := !userstore.LooksLikePhone(account)
|
||||
if err := l.enforceLoginPolicy(u, viaUsername); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.issueUser(u)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) enforceLoginPolicy(u *userstore.User, viaUsername bool) error {
|
||||
cfg := l.svcCtx.Config.Auth
|
||||
phoneOnly := l.phoneLoginOnly()
|
||||
requirePhone := cfg.RequirePhoneBound || phoneOnly
|
||||
if requirePhone && strings.TrimSpace(u.Phone) == "" {
|
||||
return fmt.Errorf("本部署要求绑定手机号后才能登录,请联系管理员")
|
||||
}
|
||||
if !viaUsername {
|
||||
return nil
|
||||
}
|
||||
if phoneOnly || cfg.DisableUsernameLoginIfPhoneBound || u.UsernameLoginDisabled {
|
||||
if strings.TrimSpace(u.Phone) == "" {
|
||||
return fmt.Errorf("已禁用用户名登录,请联系管理员绑定手机号")
|
||||
}
|
||||
return fmt.Errorf("已禁用用户名登录,请使用手机号+密码或短信验证码")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SendSMSResult struct {
|
||||
ExpiresIn int
|
||||
RetryAfter int
|
||||
DebugCode string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (l *AuthLogic) SendLoginSMS(phone string) (*SendSMSResult, error) {
|
||||
if l.svcCtx.Users == nil || l.svcCtx.SMS == nil {
|
||||
return nil, fmt.Errorf("短信服务未启用")
|
||||
}
|
||||
provider := strings.ToLower(strings.TrimSpace(l.svcCtx.Config.SMS.Provider))
|
||||
if provider == "off" || provider == "disabled" {
|
||||
return nil, fmt.Errorf("短信登录未开启")
|
||||
}
|
||||
ns, err := userstore.NormalizePhone(phone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := l.svcCtx.Users.GetByPhone(l.ctx, ns); err != nil {
|
||||
return nil, fmt.Errorf("该手机号未绑定账号,请联系管理员绑定手机后再登录")
|
||||
}
|
||||
code, expiresIn, retryAfter, err := l.svcCtx.SMS.Issue(smsstore.PurposeLogin, ns)
|
||||
if err != nil {
|
||||
return &SendSMSResult{RetryAfter: retryAfter}, err
|
||||
}
|
||||
out := &SendSMSResult{
|
||||
ExpiresIn: expiresIn,
|
||||
Message: "验证码已发送",
|
||||
}
|
||||
if provider == "" || provider == "dev" {
|
||||
log.Printf("[sms:dev] login code for %s = %s (expires %ds)", ns, code, expiresIn)
|
||||
out.DebugCode = code
|
||||
out.Message = "开发模式:验证码已写入服务日志(并返回 debug_code)"
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (l *AuthLogic) ChangePassword(oldPassword, newPassword string) error {
|
||||
if l.svcCtx.Users == nil {
|
||||
return fmt.Errorf("user store unavailable")
|
||||
}
|
||||
uid := authx.UserID(l.ctx)
|
||||
if uid <= 0 {
|
||||
return fmt.Errorf("未登录")
|
||||
}
|
||||
return l.svcCtx.Users.ChangePassword(l.ctx, uid, oldPassword, newPassword)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) BindPhone(phone string) (*userstore.User, error) {
|
||||
if l.svcCtx.Users == nil {
|
||||
return nil, fmt.Errorf("user store unavailable")
|
||||
}
|
||||
uid := authx.UserID(l.ctx)
|
||||
if uid <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
if l.phoneLoginOnly() && strings.TrimSpace(phone) == "" {
|
||||
return nil, fmt.Errorf("本部署仅支持手机号登录,不可解绑手机号")
|
||||
}
|
||||
return l.svcCtx.Users.BindPhone(l.ctx, uid, phone)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) SetUsernameLoginDisabled(disabled bool) (*userstore.User, error) {
|
||||
if l.svcCtx.Users == nil {
|
||||
return nil, fmt.Errorf("user store unavailable")
|
||||
}
|
||||
uid := authx.UserID(l.ctx)
|
||||
if uid <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
return l.svcCtx.Users.SetUsernameLoginDisabled(l.ctx, uid, disabled)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) Me() (*userstore.User, error) {
|
||||
if l.svcCtx.Users == nil {
|
||||
return nil, fmt.Errorf("user store unavailable")
|
||||
}
|
||||
uid := authx.UserID(l.ctx)
|
||||
if uid <= 0 {
|
||||
return nil, fmt.Errorf("未登录")
|
||||
}
|
||||
return l.svcCtx.Users.GetByID(l.ctx, uid)
|
||||
}
|
||||
|
||||
func (l *AuthLogic) issue(tenantID, userID int64, role, username, displayName string, orgUnitID int64) (*types.TokenResp, error) {
|
||||
role = authx.NormalizeRole(role)
|
||||
token, exp, err := authx.IssueToken(l.svcCtx.JWT, tenantID, userID, role, orgUnitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret := l.svcCtx.Config.Agent.CapsuleSecret
|
||||
if secret == "" {
|
||||
secret = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
return &types.TokenResp{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: exp,
|
||||
TenantID: tenantID,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
Role: role,
|
||||
OrgUnitID: orgUnitID,
|
||||
AgentKey: agentcap.PublicAgentKey(secret, tenantID, userID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CapsuleLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCapsuleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CapsuleLogic {
|
||||
return &CapsuleLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CapsuleLogic) Build(slug string) (*types.CapsuleResp, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
userID := authx.UserID(l.ctx)
|
||||
if authx.Role(l.ctx) == authx.RoleAgent {
|
||||
if l.svcCtx.Agents == nil {
|
||||
return nil, fmt.Errorf("agent store unavailable")
|
||||
}
|
||||
ok, err := l.svcCtx.Agents.HasAppAccess(l.ctx, authx.AgentID(l.ctx), slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("app not granted to agent")
|
||||
}
|
||||
}
|
||||
app, err := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if app.Status != meta.StatusPublished {
|
||||
return nil, fmt.Errorf("app not published")
|
||||
}
|
||||
base := l.svcCtx.Config.PublicBaseURL
|
||||
if base == "" {
|
||||
base = fmt.Sprintf("http://127.0.0.1:%d", l.svcCtx.Config.Port)
|
||||
}
|
||||
desc := &agentcap.Descriptor{
|
||||
Version: "1",
|
||||
BaseURL: strings.TrimRight(base, "/"),
|
||||
AppSlug: slug,
|
||||
TenantHint: fmt.Sprintf("t%d", tenantID),
|
||||
Auth: agentcap.AuthSpec{Type: "bearer_jwt", Header: "Authorization"},
|
||||
Notes: "Decrypt with agent_key from /auth/token. Never expose plaintext API map in UI.",
|
||||
}
|
||||
for _, r := range app.Blueprint.Apis.Resources {
|
||||
path := r.Path
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
full := fmt.Sprintf("/api/v1/apps/%s%s", slug, path)
|
||||
methods := make([]string, 0, len(r.Operations))
|
||||
for _, op := range r.Operations {
|
||||
switch op {
|
||||
case "list", "get", "export":
|
||||
methods = append(methods, "GET")
|
||||
case "create", "import":
|
||||
methods = append(methods, "POST")
|
||||
case "update":
|
||||
methods = append(methods, "PUT")
|
||||
case "delete":
|
||||
methods = append(methods, "DELETE")
|
||||
}
|
||||
}
|
||||
var entityFields []agentcap.FieldSpec
|
||||
pk := "id"
|
||||
for _, e := range app.Blueprint.Entities {
|
||||
if e.Name != r.Entity {
|
||||
continue
|
||||
}
|
||||
pk = e.PrimaryKey
|
||||
for _, f := range e.Fields {
|
||||
entityFields = append(entityFields, agentcap.FieldSpec{Name: f.Name, Type: f.Type})
|
||||
}
|
||||
}
|
||||
filters, sorts := []string{}, []string{}
|
||||
if r.List != nil {
|
||||
filters = r.List.AllowedFilters
|
||||
sorts = r.List.AllowedSorts
|
||||
}
|
||||
name := strings.TrimPrefix(path, "/")
|
||||
desc.Resources = append(desc.Resources, agentcap.ResourceSpec{
|
||||
Name: name,
|
||||
Path: full,
|
||||
Methods: uniqStrings(methods),
|
||||
Filters: filters,
|
||||
Sorts: sorts,
|
||||
Fields: entityFields,
|
||||
PrimaryKey: pk,
|
||||
})
|
||||
}
|
||||
|
||||
secret := l.svcCtx.Config.Agent.CapsuleSecret
|
||||
if secret == "" {
|
||||
secret = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
key := agentcap.DeriveKey(secret, tenantID, userID)
|
||||
capsule, err := agentcap.Encrypt(key, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.CapsuleResp{
|
||||
Capsule: capsule,
|
||||
Format: agentcap.Prefix,
|
||||
Hint: "仅智能体使用 agent_key 解密;前端只展示密文",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func uniqStrings(in []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
if _, ok := seen[s]; ok {
|
||||
continue
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
123
platform/internal/logic/applogic/crud.go
Normal file
123
platform/internal/logic/applogic/crud.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/crud"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/types"
|
||||
)
|
||||
|
||||
type CrudLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCrudLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CrudLogic {
|
||||
return &CrudLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CrudLogic) withOrgScope() context.Context {
|
||||
ctx := l.ctx
|
||||
role := authx.Role(ctx)
|
||||
orgID := authx.OrgUnitID(ctx)
|
||||
scope := crud.RowScope{WriteOrgUnit: orgID}
|
||||
// owner / agent 不按组织裁剪;其它角色若绑定了组织则只看本组织及下级
|
||||
if !authx.IsCompanyAdmin(role) && role != authx.Role智能体 && orgID > 0 && l.svcCtx.OrgUnits != nil {
|
||||
ids, err := l.svcCtx.OrgUnits.DescendantIDs(ctx, authx.TenantID(ctx), orgID)
|
||||
if err == nil {
|
||||
scope.OrgUnitIDs = ids
|
||||
} else {
|
||||
scope.OrgUnitIDs = []int64{orgID}
|
||||
}
|
||||
}
|
||||
return crud.WithRowScope(ctx, scope)
|
||||
}
|
||||
|
||||
func (l *CrudLogic) List(slug, resource string, page, pageSize int, filters map[string]string, sortBy string) (*types.PageResult, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, total, err := l.svcCtx.CRUD.List(ctx, ref, tenantID, page, pageSize, filters, sortBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
return &types.PageResult{Items: items, Page: page, PageSize: pageSize, Total: total}, nil
|
||||
}
|
||||
|
||||
func (l *CrudLogic) Get(slug, resource, id string) (map[string]any, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.svcCtx.CRUD.Get(ctx, ref, tenantID, id)
|
||||
}
|
||||
|
||||
func (l *CrudLogic) Create(slug, resource string, body map[string]any) (map[string]any, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
userID := authx.UserID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.svcCtx.CRUD.Create(ctx, ref, tenantID, userID, body)
|
||||
}
|
||||
|
||||
func (l *CrudLogic) Update(slug, resource, id string, body map[string]any) (map[string]any, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.svcCtx.CRUD.Update(ctx, ref, tenantID, id, body)
|
||||
}
|
||||
|
||||
func (l *CrudLogic) Delete(slug, resource, id string) error {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.svcCtx.CRUD.Delete(ctx, ref, tenantID, id)
|
||||
}
|
||||
|
||||
func (l *CrudLogic) GetBlueprint(slug string) (any, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
app, err := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if app.Blueprint == nil {
|
||||
return nil, fmt.Errorf("blueprint missing")
|
||||
}
|
||||
return app.Blueprint, nil
|
||||
}
|
||||
|
||||
func ParsePage(pageStr, sizeStr string) (int, int) {
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
size, _ := strconv.Atoi(sizeStr)
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
return page, size
|
||||
}
|
||||
319
platform/internal/logic/applogic/impex.go
Normal file
319
platform/internal/logic/applogic/impex.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/meta"
|
||||
"aijianzhan/platform/internal/types"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// ImportRows accepts .xlsx (preferred) or .csv.
|
||||
func (l *CrudLogic) ImportRows(slug, resource, filename string, r io.Reader) (*types.ImportResp, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
userID := authx.UserID(l.ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasOp(ref, "import") {
|
||||
return nil, fmt.Errorf("operation import not allowed")
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
raw, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil, fmt.Errorf("empty file")
|
||||
}
|
||||
|
||||
var rows [][]string
|
||||
switch {
|
||||
case ext == ".xlsx" || ext == ".xlsm" || isZipOOXML(raw):
|
||||
rows, err = readExcelRows(raw)
|
||||
case ext == ".csv" || ext == ".txt" || looksLikeCSV(raw):
|
||||
rows, err = readCSVRows(raw)
|
||||
default:
|
||||
// try excel then csv
|
||||
rows, err = readExcelRows(raw)
|
||||
if err != nil {
|
||||
rows, err = readCSVRows(raw)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse file: %w", err)
|
||||
}
|
||||
if len(rows) < 1 {
|
||||
return nil, fmt.Errorf("no header row")
|
||||
}
|
||||
|
||||
headers := make([]string, len(rows[0]))
|
||||
for i, h := range rows[0] {
|
||||
headers[i] = strings.TrimSpace(h)
|
||||
}
|
||||
|
||||
resp := &types.ImportResp{Errors: []string{}}
|
||||
for rowNum := 1; rowNum < len(rows); rowNum++ {
|
||||
rec := rows[rowNum]
|
||||
if rowEmpty(rec) {
|
||||
continue
|
||||
}
|
||||
body := map[string]any{}
|
||||
for i, h := range headers {
|
||||
if i >= len(rec) || h == "" {
|
||||
continue
|
||||
}
|
||||
fname := mapHeaderToField(ref, h)
|
||||
if fname == "" || fname == ref.Entity.PrimaryKey {
|
||||
continue
|
||||
}
|
||||
body[fname] = coerceValue(ref, fname, strings.TrimSpace(rec[i]))
|
||||
}
|
||||
if len(body) == 0 {
|
||||
resp.Skipped++
|
||||
continue
|
||||
}
|
||||
if _, err := l.svcCtx.CRUD.Create(l.ctx, ref, tenantID, userID, body); err != nil {
|
||||
resp.Skipped++
|
||||
if len(resp.Errors) < 100 {
|
||||
resp.Errors = append(resp.Errors, fmt.Sprintf("row %d: %v", rowNum+1, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
resp.Inserted++
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ImportCSV kept for callers; prefers CSV parsing.
|
||||
func (l *CrudLogic) ImportCSV(slug, resource string, r io.Reader) (*types.ImportResp, error) {
|
||||
return l.ImportRows(slug, resource, "import.csv", r)
|
||||
}
|
||||
|
||||
// ExportExcel writes .xlsx workbook.
|
||||
func (l *CrudLogic) ExportExcel(slug, resource string) ([]byte, string, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !hasOp(ref, "export") {
|
||||
return nil, "", fmt.Errorf("operation export not allowed")
|
||||
}
|
||||
items, _, err := l.svcCtx.CRUD.List(l.ctx, ref, tenantID, 1, 5000, nil, "")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
headers := make([]string, 0, len(ref.Entity.Fields))
|
||||
for _, f := range ref.Entity.Fields {
|
||||
headers = append(headers, f.Name)
|
||||
}
|
||||
|
||||
f := excelize.NewFile()
|
||||
sheet := f.GetSheetName(0)
|
||||
for i, h := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
_ = f.SetCellValue(sheet, cell, h)
|
||||
}
|
||||
for ri, item := range items {
|
||||
for ci, h := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(ci+1, ri+2)
|
||||
if v, ok := item[h]; ok && v != nil {
|
||||
_ = f.SetCellValue(sheet, cell, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), resource + ".xlsx", nil
|
||||
}
|
||||
|
||||
// ExportCSV kept for format=csv.
|
||||
func (l *CrudLogic) ExportCSV(slug, resource string) ([]byte, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(l.ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasOp(ref, "export") {
|
||||
return nil, fmt.Errorf("operation export not allowed")
|
||||
}
|
||||
items, _, err := l.svcCtx.CRUD.List(l.ctx, ref, tenantID, 1, 5000, nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := make([]string, 0, len(ref.Entity.Fields))
|
||||
for _, f := range ref.Entity.Fields {
|
||||
headers = append(headers, f.Name)
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
w := csv.NewWriter(buf)
|
||||
_ = w.Write(headers)
|
||||
for _, item := range items {
|
||||
row := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
if v, ok := item[h]; ok && v != nil {
|
||||
row[i] = fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
_ = w.Write(row)
|
||||
}
|
||||
w.Flush()
|
||||
return buf.Bytes(), w.Error()
|
||||
}
|
||||
|
||||
func readExcelRows(raw []byte) ([][]string, error) {
|
||||
f, err := excelize.OpenReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) == 0 {
|
||||
return nil, fmt.Errorf("excel has no sheets")
|
||||
}
|
||||
// 优先业务数据页,跳过说明/超限汇总
|
||||
prefer := ""
|
||||
for _, name := range sheets {
|
||||
n := strings.ToLower(name)
|
||||
if strings.Contains(n, "settlement") || (strings.Contains(name, "测点") && !strings.Contains(name, "超限") && !strings.Contains(name, "说明")) {
|
||||
prefer = name
|
||||
break
|
||||
}
|
||||
}
|
||||
if prefer == "" {
|
||||
for _, name := range sheets {
|
||||
if !strings.Contains(name, "说明") && !strings.Contains(name, "超限") {
|
||||
prefer = name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if prefer == "" {
|
||||
prefer = sheets[0]
|
||||
}
|
||||
return f.GetRows(prefer)
|
||||
}
|
||||
|
||||
func readCSVRows(raw []byte) ([][]string, error) {
|
||||
cr := csv.NewReader(bytes.NewReader(raw))
|
||||
cr.FieldsPerRecord = -1
|
||||
return cr.ReadAll()
|
||||
}
|
||||
|
||||
func isZipOOXML(raw []byte) bool {
|
||||
// xlsx is a zip archive
|
||||
return len(raw) >= 4 && raw[0] == 'P' && raw[1] == 'K' && raw[2] == 3 && raw[3] == 4
|
||||
}
|
||||
|
||||
func looksLikeCSV(raw []byte) bool {
|
||||
sample := raw
|
||||
if len(sample) > 512 {
|
||||
sample = sample[:512]
|
||||
}
|
||||
s := string(sample)
|
||||
return strings.Contains(s, ",") || strings.Contains(s, "\t") || strings.Contains(s, ";")
|
||||
}
|
||||
|
||||
func rowEmpty(rec []string) bool {
|
||||
for _, c := range rec {
|
||||
if strings.TrimSpace(c) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func hasOp(ref *meta.ResourceRef, op string) bool {
|
||||
for _, o := range ref.Resource.Operations {
|
||||
if o == op {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mapHeaderToField(ref *meta.ResourceRef, header string) string {
|
||||
h := strings.TrimSpace(header)
|
||||
if h == "" {
|
||||
return ""
|
||||
}
|
||||
norm := normalizeHeader(h)
|
||||
aliases := map[string]string{
|
||||
"编号dk": "dkilo", "测点dk": "dkilo", "dkilo": "dkilo",
|
||||
"里程": "chainage", "chainage": "chainage",
|
||||
"测点编号": "point_code", "测点": "point_code", "point_code": "point_code",
|
||||
"断面类型": "section_type", "section_type": "section_type",
|
||||
"工点": "worksite", "worksite": "worksite",
|
||||
"cjl数值": "cjl_value", "观测值": "cjl_value", "观测值mm": "cjl_value", "cjl_value": "cjl_value",
|
||||
"cjl颜色": "cjl_color", "观测色": "cjl_color", "cjl_color": "cjl_color",
|
||||
"设计沉降": "design_settlement_mm", "设计总沉降量mm": "design_settlement_mm", "design_settlement_mm": "design_settlement_mm",
|
||||
"累积沉降": "cum_settlement_mm", "累积沉降量mm": "cum_settlement_mm", "cum_settlement_mm": "cum_settlement_mm",
|
||||
"预测沉降": "pred_settlement_mm", "预测沉降mm": "pred_settlement_mm", "pred_settlement_mm": "pred_settlement_mm",
|
||||
"超限量": "exceed_mm", "超限量mm": "exceed_mm", "exceed_mm": "exceed_mm",
|
||||
"超限累计天数": "exceed_days", "累计天数天": "exceed_days", "exceed_days": "exceed_days",
|
||||
"监督天": "supervise_days", "监督周期天": "supervise_days", "supervise_days": "supervise_days",
|
||||
"超期天数": "overdue_days", "overdue_days": "overdue_days",
|
||||
"前一日占比": "before_day", "before_day": "before_day", "beforeday": "before_day",
|
||||
"后一日占比": "next_day", "next_day": "next_day", "nextday": "next_day",
|
||||
"频率": "frequency", "frequency": "frequency",
|
||||
"cljd标识": "workinfo_kilo", "workinfo_kilo": "workinfo_kilo",
|
||||
"状态": "status", "status": "status",
|
||||
"里程米": "mileage_m", "mileage_m": "mileage_m",
|
||||
}
|
||||
if a, ok := aliases[norm]; ok {
|
||||
h = a
|
||||
norm = a
|
||||
}
|
||||
for _, f := range ref.Entity.Fields {
|
||||
if f.Name == h || f.Label == header || normalizeHeader(f.Label) == norm || f.Name == norm {
|
||||
return f.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeHeader(s string) string {
|
||||
s = strings.TrimSpace(strings.ToLower(s))
|
||||
repl := strings.NewReplacer(" ", "", "_", "", "-", "", "(", "", ")", "", "(", "", ")", "", ".", "")
|
||||
return repl.Replace(s)
|
||||
}
|
||||
|
||||
func coerceValue(ref *meta.ResourceRef, fieldName, raw string) any {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
for _, f := range ref.Entity.Fields {
|
||||
if f.Name != fieldName {
|
||||
continue
|
||||
}
|
||||
switch f.Type {
|
||||
case "int", "bigint":
|
||||
n, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err == nil {
|
||||
return n
|
||||
}
|
||||
case "decimal":
|
||||
n, err := strconv.ParseFloat(raw, 64)
|
||||
if err == nil {
|
||||
return n
|
||||
}
|
||||
case "boolean":
|
||||
return raw == "1" || strings.EqualFold(raw, "true") || raw == "是"
|
||||
}
|
||||
return raw
|
||||
}
|
||||
return raw
|
||||
}
|
||||
66
platform/internal/logic/applogic/members.go
Normal file
66
platform/internal/logic/applogic/members.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/userstore"
|
||||
)
|
||||
|
||||
type MemberLogic struct {
|
||||
*AuthLogic
|
||||
}
|
||||
|
||||
func NewMemberLogic(l *AuthLogic) *MemberLogic {
|
||||
return &MemberLogic{AuthLogic: l}
|
||||
}
|
||||
|
||||
func (l *MemberLogic) List() ([]userstore.User, error) {
|
||||
tid := authx.TenantID(l.ctx)
|
||||
if tid <= 0 {
|
||||
return nil, fmt.Errorf("missing tenant")
|
||||
}
|
||||
return l.svcCtx.Users.ListMembers(l.ctx, tid)
|
||||
}
|
||||
|
||||
// Create 创建成员:用户名始终随机唯一;password 为空则随机初始密码(用户登录后可自行修改)。
|
||||
func (l *MemberLogic) Create(password, displayName, role string, orgUnitID int64) (*userstore.User, string, error) {
|
||||
tid := authx.TenantID(l.ctx)
|
||||
if tid <= 0 {
|
||||
return nil, "", fmt.Errorf("missing tenant")
|
||||
}
|
||||
if role == "" {
|
||||
role = authx.Role编辑
|
||||
}
|
||||
uname, err := userstore.AllocUniqueUsername(l.ctx, l.svcCtx.Users.UsernameExists)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
u, plain, err := l.svcCtx.Users.CreateMember(l.ctx, tid, uname, password, displayName, role, orgUnitID)
|
||||
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
|
||||
}
|
||||
return l.svcCtx.Users.CreateMember(l.ctx, tid, uname2, password, displayName, role, orgUnitID)
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
return u, plain, nil
|
||||
}
|
||||
|
||||
func (l *MemberLogic) Update(userID int64, role string, orgUnitID int64, status string) (*userstore.User, error) {
|
||||
tid := authx.TenantID(l.ctx)
|
||||
if tid <= 0 {
|
||||
return nil, fmt.Errorf("missing tenant")
|
||||
}
|
||||
if userID == authx.UserID(l.ctx) && role != "" && authx.NormalizeRole(role) != authx.Role管理员 {
|
||||
if authx.IsCompanyAdmin(authx.Role(l.ctx)) {
|
||||
return nil, fmt.Errorf("不能修改自己的角色,请由其他管理员操作")
|
||||
}
|
||||
}
|
||||
return l.svcCtx.Users.UpdateMember(l.ctx, tid, userID, role, orgUnitID, status)
|
||||
}
|
||||
55
platform/internal/logic/applogic/orgunits.go
Normal file
55
platform/internal/logic/applogic/orgunits.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/orgunitstore"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/types"
|
||||
)
|
||||
|
||||
type OrgUnitLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewOrgUnitLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrgUnitLogic {
|
||||
return &OrgUnitLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *OrgUnitLogic) List() ([]orgunitstore.OrgUnit, error) {
|
||||
if l.svcCtx.OrgUnits == nil {
|
||||
return nil, fmt.Errorf("org unit store unavailable")
|
||||
}
|
||||
return l.svcCtx.OrgUnits.List(l.ctx, authx.TenantID(l.ctx))
|
||||
}
|
||||
|
||||
func (l *OrgUnitLogic) Create(req *types.OrgUnitCreateReq) (*orgunitstore.OrgUnit, error) {
|
||||
if l.svcCtx.OrgUnits == nil {
|
||||
return nil, fmt.Errorf("org unit store unavailable")
|
||||
}
|
||||
return l.svcCtx.OrgUnits.Create(l.ctx, authx.TenantID(l.ctx), orgunitstore.CreateInput{
|
||||
ParentID: req.ParentID,
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
})
|
||||
}
|
||||
|
||||
func (l *OrgUnitLogic) Update(id int64, req *types.OrgUnitUpdateReq) (*orgunitstore.OrgUnit, error) {
|
||||
if l.svcCtx.OrgUnits == nil {
|
||||
return nil, fmt.Errorf("org unit store unavailable")
|
||||
}
|
||||
return l.svcCtx.OrgUnits.Update(l.ctx, authx.TenantID(l.ctx), id, orgunitstore.UpdateInput{
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
})
|
||||
}
|
||||
|
||||
func (l *OrgUnitLogic) Delete(id int64) error {
|
||||
if l.svcCtx.OrgUnits == nil {
|
||||
return fmt.Errorf("org unit store unavailable")
|
||||
}
|
||||
return l.svcCtx.OrgUnits.Delete(l.ctx, authx.TenantID(l.ctx), id)
|
||||
}
|
||||
381
platform/internal/logic/applogic/platform.go
Normal file
381
platform/internal/logic/applogic/platform.go
Normal file
@@ -0,0 +1,381 @@
|
||||
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
|
||||
}
|
||||
470
platform/internal/logic/applogic/publish.go
Normal file
470
platform/internal/logic/applogic/publish.go
Normal file
@@ -0,0 +1,470 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aijianzhan/platform/internal/audit"
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/agentcap"
|
||||
"aijianzhan/platform/internal/blueprint"
|
||||
"aijianzhan/platform/internal/meta"
|
||||
"aijianzhan/platform/internal/schema"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PublishLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewPublishLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishLogic {
|
||||
return &PublishLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *PublishLogic) ListApps() (*types.AppListResp, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
items, err := l.svcCtx.Meta.ListByTenant(l.ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowed := map[string]struct{}{}
|
||||
filter := false
|
||||
scope := "all"
|
||||
// 管理账号:本租户全部模块(含在建)
|
||||
// 智能体:app_slugs 为空或含 * → 不限制;否则仅白名单
|
||||
if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil {
|
||||
acc, err := l.svcCtx.Agents.Get(l.ctx, tenantID, authx.AgentID(l.ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
open := len(acc.AppSlugs) == 0
|
||||
for _, s := range acc.AppSlugs {
|
||||
if s == "*" {
|
||||
open = true
|
||||
}
|
||||
allowed[s] = struct{}{}
|
||||
}
|
||||
if open {
|
||||
scope = "open"
|
||||
filter = false
|
||||
} else {
|
||||
scope = "granted"
|
||||
filter = true
|
||||
}
|
||||
}
|
||||
out := make([]types.AppListItem, 0, len(items))
|
||||
for _, it := range items {
|
||||
if filter {
|
||||
if _, ok := allowed[it.Slug]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
st := it.Status
|
||||
out = append(out, types.AppListItem{
|
||||
AppID: it.AppID,
|
||||
Slug: it.Slug,
|
||||
Name: it.Name,
|
||||
Status: string(st),
|
||||
StatusLabel: meta.StatusLabelCN(st),
|
||||
Building: meta.IsBuilding(st),
|
||||
SchemaName: it.SchemaName,
|
||||
PageCount: it.PageCount,
|
||||
EntityCount: it.EntityCount,
|
||||
UpdatedAt: it.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
CreatedAt: it.CreatedAt.UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return &types.AppListResp{Items: out, Scope: scope}, nil
|
||||
}
|
||||
|
||||
// SaveDraft 登记/更新「在建」模块蓝图(不跑 DDL)。管理账号可用来看到生成中尚未发布的模块。
|
||||
func (l *PublishLogic) SaveDraft(slug string, req *types.DraftReq) (*types.AppListItem, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
bp, err := blueprint.Parse(req.Blueprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slug = blueprint.NormalizeIdent(slug)
|
||||
bp.Meta.Slug = blueprint.NormalizeIdent(bp.Meta.Slug)
|
||||
if bp.Meta.Slug == "" {
|
||||
bp.Meta.Slug = slug
|
||||
}
|
||||
if slug == "" {
|
||||
slug = bp.Meta.Slug
|
||||
}
|
||||
bp.Meta.Slug = slug
|
||||
if bp.Apis.BasePath == "" || strings.Contains(bp.Apis.BasePath, "-") {
|
||||
bp.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
}
|
||||
if bp.Meta.Name == "" {
|
||||
bp.Meta.Name = slug
|
||||
}
|
||||
if bp.Version == "" {
|
||||
bp.Version = "1.0"
|
||||
}
|
||||
if bp.Storage.Mode == "" {
|
||||
bp.Storage.Mode = "schema_per_app"
|
||||
}
|
||||
if bp.Storage.Engine == "" {
|
||||
bp.Storage.Engine = "postgres"
|
||||
}
|
||||
// 草稿允许尚未完全合法;尽量校验,失败则仍以宽松方式保存关键字段
|
||||
_ = bp.Validate(slug)
|
||||
|
||||
now := time.Now().UTC()
|
||||
appID := uuid.NewString()
|
||||
schemaName := bp.AssignSchemaName(tenantID)
|
||||
if existing, err := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug); err == nil && existing != nil {
|
||||
if existing.Status == meta.StatusPublished {
|
||||
return nil, fmt.Errorf("module already published: %s (use publish to update pages)", slug)
|
||||
}
|
||||
appID = existing.AppID
|
||||
now = existing.CreatedAt
|
||||
if existing.SchemaName != "" {
|
||||
schemaName = existing.SchemaName
|
||||
bp.Storage.SchemaName = existing.SchemaName
|
||||
}
|
||||
}
|
||||
rec := &meta.AppRecord{
|
||||
AppID: appID,
|
||||
TenantID: tenantID,
|
||||
Slug: slug,
|
||||
Name: bp.Meta.Name,
|
||||
SchemaName: schemaName,
|
||||
Engine: bp.Storage.Engine,
|
||||
Status: meta.StatusDraft,
|
||||
Blueprint: bp,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = l.audit("draft.save", audit.DetailJSON(map[string]any{"slug": slug, "user_id": authx.UserID(l.ctx)}))
|
||||
return &types.AppListItem{
|
||||
AppID: rec.AppID,
|
||||
Slug: slug,
|
||||
Name: rec.Name,
|
||||
Status: string(meta.StatusDraft),
|
||||
StatusLabel: meta.StatusLabelCN(meta.StatusDraft),
|
||||
Building: true,
|
||||
SchemaName: schemaName,
|
||||
PageCount: len(bp.Pages),
|
||||
EntityCount: len(bp.Entities),
|
||||
UpdatedAt: rec.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
CreatedAt: rec.CreatedAt.UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.PublishResp, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
userID := authx.UserID(l.ctx)
|
||||
|
||||
incoming, err := blueprint.Parse(req.Blueprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slug = blueprint.NormalizeIdent(slug)
|
||||
incoming.Meta.Slug = blueprint.NormalizeIdent(incoming.Meta.Slug)
|
||||
if incoming.Meta.Slug == "" {
|
||||
incoming.Meta.Slug = slug
|
||||
}
|
||||
if slug == "" {
|
||||
slug = incoming.Meta.Slug
|
||||
}
|
||||
// 路径与蓝图不一致时以路径 slug 为准(先选应用再发布)
|
||||
if slug != "" && slug != incoming.Meta.Slug {
|
||||
incoming.Meta.Slug = slug
|
||||
}
|
||||
if incoming.Apis.BasePath == "" || strings.Contains(incoming.Apis.BasePath, "-") {
|
||||
incoming.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
}
|
||||
|
||||
mode := strings.ToLower(strings.TrimSpace(req.Mode))
|
||||
if mode == "" {
|
||||
mode = "auto"
|
||||
}
|
||||
if mode == "merge" {
|
||||
mode = "add_pages" // 兼容旧别名
|
||||
}
|
||||
switch mode {
|
||||
case "auto", "add_pages", "create", "replace":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported publish mode: %s (use auto|add_pages|create|replace)", mode)
|
||||
}
|
||||
|
||||
existing, existErr := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug)
|
||||
exists := existErr == nil && existing != nil
|
||||
|
||||
publishMode := "created"
|
||||
var mergeRes *blueprint.MergeResult
|
||||
var bp *blueprint.Blueprint
|
||||
|
||||
switch {
|
||||
case mode == "create":
|
||||
if exists {
|
||||
return nil, fmt.Errorf("app already exists: %s (select it and publish with mode=add_pages to add newly generated pages)", slug)
|
||||
}
|
||||
bp = incoming
|
||||
publishMode = "created"
|
||||
case mode == "add_pages":
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("app not found: %s (create it first, or use mode=create/auto)", slug)
|
||||
}
|
||||
if existing.Blueprint == nil {
|
||||
return nil, fmt.Errorf("existing app has no blueprint")
|
||||
}
|
||||
bp = existing.Blueprint
|
||||
bp.Meta.Slug = slug
|
||||
bp.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
mergeRes, err = blueprint.MergeInto(bp, incoming)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publishMode = "pages_added"
|
||||
case mode == "replace":
|
||||
bp = incoming
|
||||
if exists {
|
||||
publishMode = "replaced"
|
||||
} else {
|
||||
publishMode = "created"
|
||||
}
|
||||
default: // auto
|
||||
if exists {
|
||||
if existing.Blueprint == nil {
|
||||
return nil, fmt.Errorf("existing app has no blueprint")
|
||||
}
|
||||
bp = existing.Blueprint
|
||||
bp.Meta.Slug = slug
|
||||
bp.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
mergeRes, err = blueprint.MergeInto(bp, incoming)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publishMode = "pages_added"
|
||||
} else {
|
||||
bp = incoming
|
||||
publishMode = "created"
|
||||
}
|
||||
}
|
||||
|
||||
if err := bp.Validate(slug); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schemaName := bp.AssignSchemaName(tenantID)
|
||||
dbName := bp.AssignDatabaseName(tenantID)
|
||||
ddl, err := schema.BuildPostgresDDL(bp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appID := uuid.NewString()
|
||||
now := time.Now().UTC()
|
||||
republish := false
|
||||
if exists {
|
||||
appID = existing.AppID
|
||||
now = existing.CreatedAt
|
||||
republish = existing.Status == meta.StatusPublished
|
||||
if existing.SchemaName != "" {
|
||||
schemaName = existing.SchemaName
|
||||
bp.Storage.SchemaName = existing.SchemaName
|
||||
}
|
||||
if existing.DatabaseName != "" {
|
||||
dbName = existing.DatabaseName
|
||||
}
|
||||
ddl, err = schema.BuildPostgresDDL(bp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
rec := &meta.AppRecord{
|
||||
AppID: appID,
|
||||
TenantID: tenantID,
|
||||
Slug: slug,
|
||||
Name: bp.Meta.Name,
|
||||
SchemaName: schemaName,
|
||||
DatabaseName: dbName,
|
||||
Engine: bp.Storage.Engine,
|
||||
Status: meta.StatusProvisioning,
|
||||
Blueprint: bp,
|
||||
DDL: ddl,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
|
||||
return nil, fmt.Errorf("meta save: %w", err)
|
||||
}
|
||||
|
||||
runner := l.svcCtx.Schema
|
||||
var appDB *sql.DB
|
||||
if dbName != "" {
|
||||
if err := runner.EnsureDatabase(l.ctx, dbName); err != nil {
|
||||
rec.Status = meta.StatusFailed
|
||||
rec.Error = err.Error()
|
||||
_ = l.svcCtx.Meta.Save(l.ctx, rec)
|
||||
_ = l.audit("publish.failed", audit.DetailJSON(map[string]any{"slug": slug, "error": err.Error()}))
|
||||
return nil, fmt.Errorf("ensure database: %w", err)
|
||||
}
|
||||
if l.svcCtx.Config.DataSource != "" {
|
||||
dsn, err := schema.DSNForDatabase(l.svcCtx.Config.DataSource, dbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appDB, err = sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer appDB.Close()
|
||||
runner = &schema.PostgresRunner{DB: appDB}
|
||||
if l.svcCtx.DBPool != nil {
|
||||
_, _ = l.svcCtx.DBPool.ForApp(rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := runner.ExecDDL(l.ctx, ddl); err != nil {
|
||||
rec.Status = meta.StatusFailed
|
||||
rec.Error = err.Error()
|
||||
rec.UpdatedAt = time.Now().UTC()
|
||||
_ = l.svcCtx.Meta.Save(l.ctx, rec)
|
||||
_ = l.audit("publish.failed", audit.DetailJSON(map[string]any{"slug": slug, "error": err.Error()}))
|
||||
return nil, fmt.Errorf("provision failed: %w", err)
|
||||
}
|
||||
|
||||
endpoints := buildEndpoints(bp)
|
||||
rec.Status = meta.StatusPublished
|
||||
rec.Endpoints = endpoints
|
||||
rec.Error = ""
|
||||
rec.UpdatedAt = time.Now().UTC()
|
||||
if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
action := "publish.success"
|
||||
if republish {
|
||||
action = "publish.republish"
|
||||
}
|
||||
detail := map[string]any{
|
||||
"slug": slug, "schema": schemaName, "database": dbName, "user_id": userID,
|
||||
"republish": republish, "publish_mode": publishMode,
|
||||
}
|
||||
if mergeRes != nil {
|
||||
detail["added_pages"] = mergeRes.AddedPages
|
||||
detail["added_entities"] = mergeRes.AddedEntities
|
||||
}
|
||||
_ = l.audit(action, audit.DetailJSON(detail))
|
||||
|
||||
// 智能体新建发布:自动把该 slug 写入可访问模块,后续不必再去控制台授权
|
||||
if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil && slug != "" {
|
||||
if aid := authx.AgentID(l.ctx); aid > 0 {
|
||||
_ = l.svcCtx.Agents.GrantAppSlug(l.ctx, aid, slug)
|
||||
}
|
||||
}
|
||||
|
||||
ownerID := authx.AgentID(l.ctx)
|
||||
if ownerID <= 0 {
|
||||
ownerID = userID
|
||||
}
|
||||
secret := l.svcCtx.Config.Agent.CapsuleSecret
|
||||
if secret == "" {
|
||||
secret = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
accessPath := ""
|
||||
accessURL := ""
|
||||
if secret != "" && ownerID > 0 {
|
||||
_, filePath, err := agentcap.SealModulePath(secret, tenantID, ownerID, slug)
|
||||
if err == nil {
|
||||
accessPath = filePath
|
||||
base := strings.TrimRight(l.svcCtx.Config.PublicBaseURL, "/")
|
||||
if req.HostMeta != nil && strings.TrimSpace(req.HostMeta.HostBaseURL) != "" {
|
||||
accessURL = strings.TrimRight(strings.TrimSpace(req.HostMeta.HostBaseURL), "/")
|
||||
} else if base != "" {
|
||||
accessURL = base + "/api/v1/public/" + filePath + "/blueprint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moduleName := bp.Meta.Name
|
||||
publishStyle := ""
|
||||
if req.HostMeta != nil {
|
||||
if n := strings.TrimSpace(req.HostMeta.ModuleName); n != "" {
|
||||
moduleName = n
|
||||
}
|
||||
publishStyle = strings.TrimSpace(req.HostMeta.PublishStyle)
|
||||
}
|
||||
if publishStyle == "" {
|
||||
publishStyle = "immediate"
|
||||
}
|
||||
|
||||
resp := &types.PublishResp{
|
||||
AppID: rec.AppID,
|
||||
Slug: slug,
|
||||
SchemaName: schemaName,
|
||||
DatabaseName: dbName,
|
||||
Status: string(meta.StatusPublished),
|
||||
Endpoints: endpoints,
|
||||
DDL: ddl,
|
||||
MemoryMode: l.svcCtx.MemoryMode,
|
||||
PublishMode: publishMode,
|
||||
ModuleName: moduleName,
|
||||
PublishStyle: publishStyle,
|
||||
AccessPath: accessPath,
|
||||
AccessURL: accessURL,
|
||||
PublishedAt: rec.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
OwnerID: ownerID,
|
||||
}
|
||||
if mergeRes != nil {
|
||||
resp.AddedPages = mergeRes.AddedPages
|
||||
resp.AddedEntities = mergeRes.AddedEntities
|
||||
resp.AddedResources = mergeRes.AddedResources
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (l *PublishLogic) audit(action, detail string) error {
|
||||
if l.svcCtx.Audit == nil {
|
||||
return nil
|
||||
}
|
||||
return l.svcCtx.Audit.Log(l.ctx, authx.TenantID(l.ctx), authx.UserID(l.ctx), action, detail)
|
||||
}
|
||||
|
||||
func buildEndpoints(bp *blueprint.Blueprint) []string {
|
||||
base := strings.TrimRight(bp.Apis.BasePath, "/")
|
||||
if base == "" {
|
||||
base = "/api/v1/apps/" + bp.Meta.Slug
|
||||
}
|
||||
out := []string{"GET " + base + "/blueprint"}
|
||||
for _, r := range bp.Apis.Resources {
|
||||
path := r.Path
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
full := base + path
|
||||
for _, op := range r.Operations {
|
||||
switch op {
|
||||
case "list":
|
||||
out = append(out, "GET "+full)
|
||||
case "create":
|
||||
out = append(out, "POST "+full)
|
||||
case "get":
|
||||
out = append(out, "GET "+full+"/{id}")
|
||||
case "update":
|
||||
out = append(out, "PUT "+full+"/{id}")
|
||||
case "delete":
|
||||
out = append(out, "DELETE "+full+"/{id}")
|
||||
case "import":
|
||||
out = append(out, "POST "+full+"/import")
|
||||
case "export":
|
||||
out = append(out, "GET "+full+"/export")
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
93
platform/internal/logic/applogic/roles.go
Normal file
93
platform/internal/logic/applogic/roles.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/rolestore"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/tenantperm"
|
||||
"aijianzhan/platform/internal/types"
|
||||
)
|
||||
|
||||
type RoleAdminLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewRoleAdminLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RoleAdminLogic {
|
||||
return &RoleAdminLogic{ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *RoleAdminLogic) store() (rolestore.Store, error) {
|
||||
if l.svcCtx.Roles == nil {
|
||||
return nil, fmt.Errorf("role store unavailable")
|
||||
}
|
||||
return l.svcCtx.Roles, nil
|
||||
}
|
||||
|
||||
func (l *RoleAdminLogic) List() ([]rolestore.Role, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tid := authx.TenantID(l.ctx)
|
||||
_ = st.EnsureDefaults(l.ctx, tid)
|
||||
return st.List(l.ctx, tid)
|
||||
}
|
||||
|
||||
func (l *RoleAdminLogic) Get(roleID int64) (*rolestore.Role, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.Get(l.ctx, authx.TenantID(l.ctx), roleID)
|
||||
}
|
||||
|
||||
func (l *RoleAdminLogic) Create(req *types.RoleCreateReq) (*rolestore.Role, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms := authx.NormalizePerms(req.Permissions)
|
||||
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.Create(l.ctx, authx.TenantID(l.ctx), rolestore.CreateInput{
|
||||
Code: req.Code,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Permissions: perms,
|
||||
})
|
||||
}
|
||||
|
||||
func (l *RoleAdminLogic) Update(roleID int64, req *types.RoleUpdateReq) (*rolestore.Role, error) {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in := rolestore.UpdateInput{}
|
||||
if req.Name != nil {
|
||||
in.Name = req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
in.Description = req.Description
|
||||
}
|
||||
if req.Permissions != nil {
|
||||
perms := authx.NormalizePerms(*req.Permissions)
|
||||
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, authx.TenantID(l.ctx), perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in.Permissions = &perms
|
||||
}
|
||||
return st.Update(l.ctx, authx.TenantID(l.ctx), roleID, in)
|
||||
}
|
||||
|
||||
func (l *RoleAdminLogic) Delete(roleID int64) error {
|
||||
st, err := l.store()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.Delete(l.ctx, authx.TenantID(l.ctx), roleID)
|
||||
}
|
||||
154
platform/internal/logic/applogic/tenant_invite.go
Normal file
154
platform/internal/logic/applogic/tenant_invite.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package applogic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/invitestore"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/types"
|
||||
"aijianzhan/platform/internal/userstore"
|
||||
)
|
||||
|
||||
func (l *AuthLogic) issueUser(u *userstore.User) (*types.TokenResp, error) {
|
||||
resp, err := l.issue(u.TenantID, u.UserID, u.Role, u.Username, u.DisplayName, u.OrgUnitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Phone = u.Phone
|
||||
resp.UsernameLoginDisabled = u.UsernameLoginDisabled
|
||||
resp.Status = u.Status
|
||||
if resp.Status == "" {
|
||||
if u.HasTenant() || u.IsPlatformAdmin() {
|
||||
resp.Status = userstore.StatusActive
|
||||
} else {
|
||||
resp.Status = userstore.StatusPending
|
||||
}
|
||||
}
|
||||
if u.IsPlatformAdmin() {
|
||||
resp.Message = "平台超级管理员工作台:管理全部公司;打开某公司可查看其内部功能"
|
||||
return resp, nil
|
||||
}
|
||||
if !u.HasTenant() {
|
||||
resp.Message = "账号待加入租户:请使用邀请码加入公司,或创建自己的公司"
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (l *AuthLogic) AcceptInvite(req *types.InviteAcceptReq) (*types.TokenResp, error) {
|
||||
if l.svcCtx.Users == nil || l.svcCtx.Invites == nil {
|
||||
return nil, fmt.Errorf("invite store unavailable")
|
||||
}
|
||||
code := strings.TrimSpace(req.Code)
|
||||
if code == "" {
|
||||
return nil, fmt.Errorf("invite code required")
|
||||
}
|
||||
userID := authx.UserID(l.ctx)
|
||||
cur, err := l.svcCtx.Users.GetByID(l.ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cur.HasTenant() {
|
||||
return nil, fmt.Errorf("already joined a tenant")
|
||||
}
|
||||
inv, err := l.svcCtx.Invites.GetByCode(l.ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid invite code")
|
||||
}
|
||||
if err := invitestore.ValidateUsable(inv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inv.OrgUnitID > 0 && l.svcCtx.OrgUnits != nil {
|
||||
if _, err := l.svcCtx.OrgUnits.Get(l.ctx, inv.TenantID, inv.OrgUnitID); err != nil {
|
||||
return nil, fmt.Errorf("invite org unit invalid")
|
||||
}
|
||||
}
|
||||
if err := l.svcCtx.Invites.Consume(l.ctx, inv.InviteID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := l.svcCtx.Users.JoinTenant(l.ctx, userID, inv.TenantID, inv.Role, inv.OrgUnitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.svcCtx.Roles != nil {
|
||||
_ = l.svcCtx.Roles.EnsureDefaults(l.ctx, u.TenantID)
|
||||
}
|
||||
resp, err := l.issueUser(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Message = "已加入租户"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (l *AuthLogic) CreateTenant(req *types.TenantCreateReq) (*types.TokenResp, error) {
|
||||
if l.svcCtx.Users == nil {
|
||||
return nil, fmt.Errorf("user store unavailable")
|
||||
}
|
||||
userID := authx.UserID(l.ctx)
|
||||
u, err := l.svcCtx.Users.CreateTenantAsOwner(l.ctx, userID, req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.svcCtx.Roles != nil {
|
||||
_ = l.svcCtx.Roles.EnsureDefaults(l.ctx, u.TenantID)
|
||||
}
|
||||
if l.svcCtx.TenantPerm != nil {
|
||||
_ = l.svcCtx.TenantPerm.EnsureDefault(l.ctx, u.TenantID)
|
||||
}
|
||||
resp, err := l.issueUser(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Message = "已创建公司并成为管理员"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type InviteAdminLogic struct {
|
||||
AuthLogic
|
||||
}
|
||||
|
||||
func NewInviteAdminLogic(ctx context.Context, svcCtx *svc.ServiceContext) *InviteAdminLogic {
|
||||
return &InviteAdminLogic{AuthLogic: AuthLogic{ctx: ctx, svcCtx: svcCtx}}
|
||||
}
|
||||
|
||||
func (l *InviteAdminLogic) List() ([]invitestore.Invite, error) {
|
||||
if l.svcCtx.Invites == nil {
|
||||
return nil, fmt.Errorf("invite store unavailable")
|
||||
}
|
||||
return l.svcCtx.Invites.List(l.ctx, authx.TenantID(l.ctx))
|
||||
}
|
||||
|
||||
func (l *InviteAdminLogic) Create(req *types.InviteCreateReq) (*invitestore.Invite, error) {
|
||||
if l.svcCtx.Invites == nil {
|
||||
return nil, fmt.Errorf("invite store unavailable")
|
||||
}
|
||||
tid := authx.TenantID(l.ctx)
|
||||
if req.OrgUnitID > 0 {
|
||||
if l.svcCtx.OrgUnits == nil {
|
||||
return nil, fmt.Errorf("org unit store unavailable")
|
||||
}
|
||||
if _, err := l.svcCtx.OrgUnits.Get(l.ctx, tid, req.OrgUnitID); err != nil {
|
||||
return nil, fmt.Errorf("invalid org_unit_id")
|
||||
}
|
||||
}
|
||||
in := invitestore.CreateInput{
|
||||
Role: req.Role,
|
||||
OrgUnitID: req.OrgUnitID,
|
||||
MaxUses: req.MaxUses,
|
||||
}
|
||||
if req.ExpiresInHours > 0 {
|
||||
in.ExpiresIn = time.Duration(req.ExpiresInHours) * time.Hour
|
||||
}
|
||||
return l.svcCtx.Invites.Create(l.ctx, tid, authx.UserID(l.ctx), in)
|
||||
}
|
||||
|
||||
func (l *InviteAdminLogic) Revoke(inviteID int64) error {
|
||||
if l.svcCtx.Invites == nil {
|
||||
return fmt.Errorf("invite store unavailable")
|
||||
}
|
||||
return l.svcCtx.Invites.Revoke(l.ctx, authx.TenantID(l.ctx), inviteID)
|
||||
}
|
||||
Reference in New Issue
Block a user