feat: portal tenant apply flow
This commit is contained in:
33
backend/app/http/web/dto/tenant_apply.go
Normal file
33
backend/app/http/web/dto/tenant_apply.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"quyun/v2/pkg/consts"
|
||||
)
|
||||
|
||||
// TenantApplyForm 申请创作者(创建租户申请)表单。
|
||||
type TenantApplyForm struct {
|
||||
// Code 租户 ID(用于 URL/系统标识);全局唯一(tenants.code,忽略大小写)。
|
||||
Code string `json:"code,omitempty"`
|
||||
// Name 租户名称(展示用)。
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// TenantApplicationResponse 当前用户的租户申请信息(申请创作者)。
|
||||
type TenantApplicationResponse struct {
|
||||
// HasApplication 是否已提交过申请(或已成为创作者)。
|
||||
HasApplication bool `json:"hasApplication"`
|
||||
// TenantID 租户ID。
|
||||
TenantID int64 `json:"tenantId,omitempty"`
|
||||
// TenantCode 租户 Code。
|
||||
TenantCode string `json:"tenantCode,omitempty"`
|
||||
// TenantName 租户名称。
|
||||
TenantName string `json:"tenantName,omitempty"`
|
||||
// Status 租户状态(pending_verify/verified/banned)。
|
||||
Status consts.TenantStatus `json:"status,omitempty"`
|
||||
// StatusDescription 状态描述(便于前端展示)。
|
||||
StatusDescription string `json:"statusDescription,omitempty"`
|
||||
// CreatedAt 申请创建时间(租户记录创建时间)。
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
}
|
||||
@@ -33,11 +33,13 @@ func Provide(opts ...opt.Option) error {
|
||||
auth *auth,
|
||||
me *me,
|
||||
middlewares *middlewares.Middlewares,
|
||||
tenantApply *tenantApply,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
auth: auth,
|
||||
me: me,
|
||||
middlewares: middlewares,
|
||||
tenantApply: tenantApply,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
@@ -47,5 +49,12 @@ func Provide(opts ...opt.Option) error {
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*tenantApply, error) {
|
||||
obj := &tenantApply{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
middlewares *middlewares.Middlewares
|
||||
// Controller instances
|
||||
auth *auth
|
||||
me *me
|
||||
auth *auth
|
||||
me *me
|
||||
tenantApply *tenantApply
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
@@ -81,6 +82,16 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
router.Get("/v1/me/tenants"[len(r.Path()):], DataFunc0(
|
||||
r.me.myTenants,
|
||||
))
|
||||
// Register routes for controller: tenantApply
|
||||
r.log.Debugf("Registering route: Get /v1/tenant/application -> tenantApply.application")
|
||||
router.Get("/v1/tenant/application"[len(r.Path()):], DataFunc0(
|
||||
r.tenantApply.application,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/tenant/apply -> tenantApply.apply")
|
||||
router.Post("/v1/tenant/apply"[len(r.Path()):], DataFunc1(
|
||||
r.tenantApply.apply,
|
||||
Body[dto.TenantApplyForm]("form"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
|
||||
146
backend/app/http/web/tenant_apply.go
Normal file
146
backend/app/http/web/tenant_apply.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type tenantApply struct{}
|
||||
|
||||
var reTenantCode = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{2,63}$`)
|
||||
|
||||
// Application 获取当前用户的租户申请信息(申请创作者)。
|
||||
//
|
||||
// @Summary 获取租户申请信息(申请创作者)
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.TenantApplicationResponse "成功"
|
||||
// @Router /v1/tenant/application [get]
|
||||
func (ctl *tenantApply) application(ctx fiber.Ctx) (*dto.TenantApplicationResponse, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
m, err := services.Tenant.FindOwnedByUserID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &dto.TenantApplicationResponse{HasApplication: false}, nil
|
||||
}
|
||||
return nil, errorx.Wrap(err).WithMsg("查询申请信息失败,请稍后再试")
|
||||
}
|
||||
|
||||
return &dto.TenantApplicationResponse{
|
||||
HasApplication: true,
|
||||
TenantID: m.ID,
|
||||
TenantCode: m.Code,
|
||||
TenantName: m.Name,
|
||||
Status: m.Status,
|
||||
StatusDescription: m.Status.Description(),
|
||||
CreatedAt: m.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Apply 申请创作者(创建租户申请)。
|
||||
//
|
||||
// @Summary 提交租户申请(申请创作者)
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.TenantApplyForm true "form"
|
||||
// @Success 200 {object} dto.TenantApplicationResponse "成功"
|
||||
// @Router /v1/tenant/apply [post]
|
||||
// @Bind form body
|
||||
func (ctl *tenantApply) apply(ctx fiber.Ctx, form *dto.TenantApplyForm) (*dto.TenantApplicationResponse, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
code := strings.ToLower(strings.TrimSpace(form.Code))
|
||||
if code == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请填写租户 ID")
|
||||
}
|
||||
if !reTenantCode.MatchString(code) {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("租户 ID 需为 3-64 位小写字母/数字/下划线/短横线,且以字母或数字开头")
|
||||
}
|
||||
name := strings.TrimSpace(form.Name)
|
||||
if name == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请填写租户名称")
|
||||
}
|
||||
|
||||
// 一个用户仅可申请为一个租户创作者:若已存在 owned tenant,直接返回当前申请信息。
|
||||
existing, err := services.Tenant.FindOwnedByUserID(ctx, claims.UserID)
|
||||
if err == nil && existing != nil && existing.ID > 0 {
|
||||
return &dto.TenantApplicationResponse{
|
||||
HasApplication: true,
|
||||
TenantID: existing.ID,
|
||||
TenantCode: existing.Code,
|
||||
TenantName: existing.Name,
|
||||
Status: existing.Status,
|
||||
StatusDescription: existing.Status.Description(),
|
||||
CreatedAt: existing.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
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(`{}`)),
|
||||
}
|
||||
|
||||
db := models.Q.Tenant.WithContext(ctx).UnderlyingDB()
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.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 errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, errorx.ErrRecordDuplicated.WithMsg("租户 ID 已被占用,请换一个试试")
|
||||
}
|
||||
return nil, errorx.Wrap(err).WithMsg("提交申请失败,请稍后再试")
|
||||
}
|
||||
|
||||
return &dto.TenantApplicationResponse{
|
||||
HasApplication: true,
|
||||
TenantID: tenant.ID,
|
||||
TenantCode: tenant.Code,
|
||||
TenantName: tenant.Name,
|
||||
Status: tenant.Status,
|
||||
StatusDescription: tenant.Status.Description(),
|
||||
CreatedAt: tenant.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user