chore: initial commit of ai site platform

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 10:19:22 +08:00
commit 6366859bb3
222 changed files with 47313 additions and 0 deletions

View 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
}

View 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
}
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View 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
}

View 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
}

View 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)
}

View 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)
}