diff --git a/AGENTS.md b/AGENTS.md index 7121789..16d3135 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ - **Backend rule of law**: all backend development MUST follow `backend/llm.txt` (HTTP/module layout, generated-file rules, GORM-Gen usage, transactions, comments, and route conventions). - Go: run `gofmt` on changed files; keep HTTP handlers thin (bind → `services.*` → return). +- Backend layering: controllers must not call `models.*`, `models.Q.*`, or raw `*gorm.DB` CRUD directly; all DAO/DB operations must live in `backend/app/services/*` (controller only does bind/validate → `services.*` → return). - HTTP module directories are `snake_case`; path params are `camelCase` and prefer typed IDs like `:orderID` to avoid route conflicts. - Avoid editing generated files (e.g. `backend/app/http/**/routes.gen.go`, `backend/docs/docs.go`); regenerate via `atomctl` instead. diff --git a/backend/app/http/web/tenant_apply.go b/backend/app/http/web/tenant_apply.go index 11b0380..92787b4 100644 --- a/backend/app/http/web/tenant_apply.go +++ b/backend/app/http/web/tenant_apply.go @@ -8,12 +8,10 @@ import ( "quyun/v2/app/errorx" "quyun/v2/app/http/web/dto" "quyun/v2/app/services" - "quyun/v2/database/models" "quyun/v2/pkg/consts" "quyun/v2/providers/jwt" "github.com/gofiber/fiber/v3" - "go.ipao.vip/gen/types" "gorm.io/gorm" ) @@ -100,34 +98,7 @@ func (ctl *tenantApply) apply(ctx fiber.Ctx, form *dto.TenantApplyForm) (*dto.Te return nil, errorx.Wrap(err).WithMsg("申请校验失败,请稍后再试") } - tenant := &models.Tenant{ - UserID: claims.UserID, - Code: code, - UUID: types.NewUUIDv4(), - Name: name, - Status: consts.TenantStatusPendingVerify, - Config: types.JSON([]byte(`{}`)), - } - - // NOTE: 使用全新 Session,避免复用 gen.DO 内部带 Model/Statement 的 *gorm.DB 导致 schema 与 dest 不一致而触发 GORM 反射 panic。 - db := models.Q.Tenant.WithContext(ctx).UnderlyingDB().Session(&gorm.Session{}) - err = db.Transaction(func(tx *gorm.DB) error { - if err := tx.Omit("Users").Create(tenant).Error; err != nil { - return err - } - - tenantUser := &models.TenantUser{ - TenantID: tenant.ID, - UserID: claims.UserID, - Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleTenantAdmin}), - Status: consts.UserStatusVerified, - } - if err := tx.Create(tenantUser).Error; err != nil { - return err - } - - return tx.First(tenant, tenant.ID).Error - }) + tenant, err := services.Tenant.ApplyOwnedTenant(ctx, claims.UserID, code, name) if err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, errorx.ErrRecordDuplicated.WithMsg("租户 ID 已被占用,请换一个试试") diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index 58429d7..fe12ae4 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -855,6 +855,68 @@ func (t *tenant) FindOwnedByUserID(ctx context.Context, userID int64) (*models.T return m, nil } +// ApplyOwnedTenant 申请创作者(创建租户申请)。 +// 业务约束: +// - 一个用户仅可申请一个租户:若已存在 owned tenant,则直接返回该租户(幂等)。 +// - 租户创建后默认处于 pending_verify,等待后台审核通过后才算“创作者”。 +func (t *tenant) ApplyOwnedTenant(ctx context.Context, userID int64, code, name string) (*models.Tenant, error) { + if userID <= 0 { + return nil, errors.New("user_id must be > 0") + } + code = strings.ToLower(strings.TrimSpace(code)) + name = strings.TrimSpace(name) + if code == "" { + return nil, errors.New("code is empty") + } + if name == "" { + return nil, errors.New("name is empty") + } + + // 幂等:一个用户仅允许拥有一个租户;若已存在则直接返回。 + existing, err := t.FindOwnedByUserID(ctx, userID) + if err == nil && existing != nil && existing.ID > 0 { + return existing, nil + } + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(err, "check owned tenant failed, user_id: %d", userID) + } + + tenant := &models.Tenant{ + UserID: userID, + Code: code, + UUID: types.NewUUIDv4(), + Name: name, + Status: consts.TenantStatusPendingVerify, + Config: types.JSON([]byte(`{}`)), + } + + // 事务边界:创建租户 + 写入 tenant_users(租户管理员角色)。 + // 使用全新 Session,避免复用带 Model/Statement 的 DB 句柄引发 GORM 反射 panic。 + db := _db.WithContext(ctx).Session(&gorm.Session{}) + err = db.Transaction(func(tx *gorm.DB) error { + if err := tx.Omit("Users").Create(tenant).Error; err != nil { + return err + } + + tenantUser := &models.TenantUser{ + TenantID: tenant.ID, + UserID: userID, + Role: types.NewArray([]consts.TenantUserRole{consts.TenantUserRoleTenantAdmin}), + Status: consts.UserStatusVerified, + } + if err := tx.Create(tenantUser).Error; err != nil { + return err + } + + return tx.First(tenant, tenant.ID).Error + }) + if err != nil { + return nil, err + } + + return tenant, nil +} + func (t *tenant) FindTenantUser(ctx context.Context, tenantID, userID int64) (*models.TenantUser, error) { logrus.WithField("tenant_id", tenantID).WithField("user_id", userID).Info("find tenant user") tbl, query := models.TenantUserQuery.QueryContext(ctx)