refactor: move tenant apply db to service
This commit is contained in:
@@ -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).
|
- **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).
|
- 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<int>` to avoid route conflicts.
|
- HTTP module directories are `snake_case`; path params are `camelCase` and prefer typed IDs like `:orderID<int>` to avoid route conflicts.
|
||||||
- Avoid editing generated files (e.g. `backend/app/http/**/routes.gen.go`, `backend/docs/docs.go`); regenerate via `atomctl` instead.
|
- Avoid editing generated files (e.g. `backend/app/http/**/routes.gen.go`, `backend/docs/docs.go`); regenerate via `atomctl` instead.
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ import (
|
|||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
"quyun/v2/app/http/web/dto"
|
"quyun/v2/app/http/web/dto"
|
||||||
"quyun/v2/app/services"
|
"quyun/v2/app/services"
|
||||||
"quyun/v2/database/models"
|
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
"quyun/v2/providers/jwt"
|
"quyun/v2/providers/jwt"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"go.ipao.vip/gen/types"
|
|
||||||
"gorm.io/gorm"
|
"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("申请校验失败,请稍后再试")
|
return nil, errorx.Wrap(err).WithMsg("申请校验失败,请稍后再试")
|
||||||
}
|
}
|
||||||
|
|
||||||
tenant := &models.Tenant{
|
tenant, err := services.Tenant.ApplyOwnedTenant(ctx, claims.UserID, code, name)
|
||||||
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
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
return nil, errorx.ErrRecordDuplicated.WithMsg("租户 ID 已被占用,请换一个试试")
|
return nil, errorx.ErrRecordDuplicated.WithMsg("租户 ID 已被占用,请换一个试试")
|
||||||
|
|||||||
@@ -855,6 +855,68 @@ func (t *tenant) FindOwnedByUserID(ctx context.Context, userID int64) (*models.T
|
|||||||
return m, nil
|
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) {
|
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")
|
logrus.WithField("tenant_id", tenantID).WithField("user_id", userID).Info("find tenant user")
|
||||||
tbl, query := models.TenantUserQuery.QueryContext(ctx)
|
tbl, query := models.TenantUserQuery.QueryContext(ctx)
|
||||||
|
|||||||
Reference in New Issue
Block a user