feat: expand superadmin edits and minio docs

This commit is contained in:
2026-01-17 19:32:46 +08:00
parent 3af5b20bcc
commit 984a404b5f
25 changed files with 2688 additions and 36 deletions

View File

@@ -2,8 +2,10 @@ package v1
import (
dto "quyun/v2/app/http/super/v1/dto"
v1_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/app/services"
"quyun/v2/database/models"
"github.com/gofiber/fiber/v3"
)
@@ -26,3 +28,36 @@ type creators struct{}
func (c *creators) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) {
return services.Super.ListTenants(ctx, filter)
}
// Get creator settings
//
// @Router /super/v1/creators/:tenantID<int>/settings [get]
// @Summary Get creator settings
// @Description Get creator settings by tenant ID
// @Tags Creator
// @Accept json
// @Produce json
// @Param tenantID path int64 true "Tenant ID"
// @Success 200 {object} v1_dto.Settings
// @Bind tenantID path
func (c *creators) GetSettings(ctx fiber.Ctx, tenantID int64) (*v1_dto.Settings, error) {
return services.Super.GetCreatorSettings(ctx, tenantID)
}
// Update creator settings
//
// @Router /super/v1/creators/:tenantID<int>/settings [put]
// @Summary Update creator settings
// @Description Update creator settings by tenant ID
// @Tags Creator
// @Accept json
// @Produce json
// @Param tenantID path int64 true "Tenant ID"
// @Param form body v1_dto.Settings true "Settings form"
// @Success 200 {string} string "Updated"
// @Bind user local key(__ctx_user)
// @Bind tenantID path
// @Bind form body
func (c *creators) UpdateSettings(ctx fiber.Ctx, user *models.User, tenantID int64, form *v1_dto.Settings) error {
return services.Super.UpdateCreatorSettings(ctx, user.ID, tenantID, form)
}

View File

@@ -213,6 +213,16 @@ type UserItem struct {
Balance int64 `json:"balance"`
// BalanceFrozen 账户冻结余额(分)。
BalanceFrozen int64 `json:"balance_frozen"`
// Nickname 用户昵称(资料展示)。
Nickname string `json:"nickname"`
// Avatar 用户头像URL资料展示
Avatar string `json:"avatar"`
// Gender 用户性别male/female/secret
Gender consts.Gender `json:"gender"`
// Bio 用户个人简介。
Bio string `json:"bio"`
// IsRealNameVerified 是否已实名认证。
IsRealNameVerified bool `json:"is_real_name_verified"`
// OwnedTenantCount 拥有的租户数量。
OwnedTenantCount int64 `json:"owned_tenant_count"`
// JoinedTenantCount 加入的租户数量。

View File

@@ -113,6 +113,22 @@ type SuperNotificationTemplateCreateForm struct {
IsActive *bool `json:"is_active"`
}
// SuperNotificationTemplateUpdateForm 超管通知模板更新参数。
type SuperNotificationTemplateUpdateForm struct {
// TenantID 租户ID不传代表不修改租户归属
TenantID *int64 `json:"tenant_id"`
// Name 模板名称(可选)。
Name *string `json:"name"`
// Type 通知类型system/order/audit/interaction可选
Type *consts.NotificationType `json:"type"`
// Title 通知标题(可选)。
Title *string `json:"title"`
// Content 通知内容(可选)。
Content *string `json:"content"`
// IsActive 是否启用(可选)。
IsActive *bool `json:"is_active"`
}
// SuperNotificationTemplateItem 超管通知模板条目。
type SuperNotificationTemplateItem struct {
// ID 模板ID。

View File

@@ -72,3 +72,29 @@ type SuperPayoutAccountReviewForm struct {
// Reason 审核说明(驳回时必填)。
Reason string `json:"reason"`
}
// SuperPayoutAccountCreateForm 超管创建结算账户参数。
type SuperPayoutAccountCreateForm struct {
// UserID 收款账户归属用户ID。
UserID int64 `json:"user_id" validate:"required"`
// Type 账户类型bank/alipay
Type string `json:"type" validate:"required,oneof=bank alipay"`
// Name 账户名称/开户行。
Name string `json:"name" validate:"required,max=128"`
// Account 收款账号。
Account string `json:"account" validate:"required,max=128"`
// Realname 收款人姓名。
Realname string `json:"realname" validate:"required,max=128"`
}
// SuperPayoutAccountUpdateForm 超管更新结算账户参数。
type SuperPayoutAccountUpdateForm struct {
// Type 账户类型bank/alipay可选
Type *string `json:"type" validate:"omitempty,oneof=bank alipay"`
// Name 账户名称/开户行(可选)。
Name *string `json:"name" validate:"omitempty,max=128"`
// Account 收款账号(可选)。
Account *string `json:"account" validate:"omitempty,max=128"`
// Realname 收款人姓名(可选)。
Realname *string `json:"realname" validate:"omitempty,max=128"`
}

View File

@@ -117,6 +117,24 @@ type SuperUserRealNameResponse struct {
IDCardMasked string `json:"id_card_masked"`
}
// SuperUserProfileUpdateForm 超管用户资料更新表单。
type SuperUserProfileUpdateForm struct {
// Nickname 昵称(可选,空字符串表示清空)。
Nickname *string `json:"nickname"`
// Avatar 头像URL可选空字符串表示清空
Avatar *string `json:"avatar"`
// Gender 性别(可选)。
Gender *consts.Gender `json:"gender"`
// Bio 个人简介(可选,空字符串表示清空)。
Bio *string `json:"bio"`
// IsRealNameVerified 是否已实名认证(可选)。
IsRealNameVerified *bool `json:"is_real_name_verified"`
// RealName 真实姓名(可选,用于更新实名认证信息)。
RealName *string `json:"real_name"`
// IDCard 身份证号(可选,用于更新实名认证信息)。
IDCard *string `json:"id_card"`
}
// SuperUserContentActionListFilter 超管用户互动内容列表过滤条件。
type SuperUserContentActionListFilter struct {
requests.Pagination

View File

@@ -4,6 +4,7 @@ import (
dto "quyun/v2/app/http/super/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/app/services"
"quyun/v2/database/models"
"github.com/gofiber/fiber/v3"
)
@@ -70,3 +71,21 @@ func (c *notifications) ListTemplates(ctx fiber.Ctx, filter *dto.SuperNotificati
func (c *notifications) CreateTemplate(ctx fiber.Ctx, form *dto.SuperNotificationTemplateCreateForm) (*dto.SuperNotificationTemplateItem, error) {
return services.Super.CreateNotificationTemplate(ctx, form)
}
// Update notification template
//
// @Router /super/v1/notifications/templates/:id<int> [patch]
// @Summary Update notification template
// @Description Update notification template
// @Tags Notification
// @Accept json
// @Produce json
// @Param id path int64 true "Template ID"
// @Param form body dto.SuperNotificationTemplateUpdateForm true "Update form"
// @Success 200 {object} dto.SuperNotificationTemplateItem
// @Bind user local key(__ctx_user)
// @Bind id path
// @Bind form body
func (c *notifications) UpdateTemplate(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperNotificationTemplateUpdateForm) (*dto.SuperNotificationTemplateItem, error) {
return services.Super.UpdateNotificationTemplate(ctx, user.ID, id, form)
}

View File

@@ -28,6 +28,24 @@ func (c *payoutAccounts) List(ctx fiber.Ctx, filter *dto.SuperPayoutAccountListF
return services.Super.ListPayoutAccounts(ctx, filter)
}
// Create payout account
//
// @Router /super/v1/creators/:tenantID<int>/payout-accounts [post]
// @Summary Create payout account
// @Description Create payout account for tenant
// @Tags Finance
// @Accept json
// @Produce json
// @Param tenantID path int64 true "Tenant ID"
// @Param form body dto.SuperPayoutAccountCreateForm true "Create form"
// @Success 200 {string} string "Created"
// @Bind user local key(__ctx_user)
// @Bind tenantID path
// @Bind form body
func (c *payoutAccounts) Create(ctx fiber.Ctx, user *models.User, tenantID int64, form *dto.SuperPayoutAccountCreateForm) error {
return services.Super.CreatePayoutAccount(ctx, user.ID, tenantID, form)
}
// Remove payout account
//
// @Router /super/v1/payout-accounts/:id<int> [delete]
@@ -61,3 +79,21 @@ func (c *payoutAccounts) Remove(ctx fiber.Ctx, user *models.User, id int64) erro
func (c *payoutAccounts) Review(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperPayoutAccountReviewForm) error {
return services.Super.ReviewPayoutAccount(ctx, user.ID, id, form)
}
// Update payout account
//
// @Router /super/v1/payout-accounts/:id<int> [patch]
// @Summary Update payout account
// @Description Update payout account across tenants
// @Tags Finance
// @Accept json
// @Produce json
// @Param id path int64 true "Payout account ID"
// @Param form body dto.SuperPayoutAccountUpdateForm true "Update form"
// @Success 200 {string} string "Updated"
// @Bind user local key(__ctx_user)
// @Bind id path
// @Bind form body
func (c *payoutAccounts) Update(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperPayoutAccountUpdateForm) error {
return services.Super.UpdatePayoutAccount(ctx, user.ID, id, form)
}

View File

@@ -228,6 +228,18 @@ func (r *Routes) Register(router fiber.Router) {
r.creators.List,
Query[dto.TenantListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /super/v1/creators/:tenantID<int>/settings -> creators.GetSettings")
router.Get("/super/v1/creators/:tenantID<int>/settings"[len(r.Path()):], DataFunc1(
r.creators.GetSettings,
PathParam[int64]("tenantID"),
))
r.log.Debugf("Registering route: Put /super/v1/creators/:tenantID<int>/settings -> creators.UpdateSettings")
router.Put("/super/v1/creators/:tenantID<int>/settings"[len(r.Path()):], Func3(
r.creators.UpdateSettings,
Local[*models.User]("__ctx_user"),
PathParam[int64]("tenantID"),
Body[v1_dto.Settings]("form"),
))
// Register routes for controller: finance
r.log.Debugf("Registering route: Get /super/v1/finance/anomalies/balances -> finance.ListBalanceAnomalies")
router.Get("/super/v1/finance/anomalies/balances"[len(r.Path()):], DataFunc1(
@@ -261,6 +273,13 @@ func (r *Routes) Register(router fiber.Router) {
r.notifications.ListTemplates,
Query[dto.SuperNotificationTemplateListFilter]("filter"),
))
r.log.Debugf("Registering route: Patch /super/v1/notifications/templates/:id<int> -> notifications.UpdateTemplate")
router.Patch("/super/v1/notifications/templates/:id<int>"[len(r.Path()):], DataFunc3(
r.notifications.UpdateTemplate,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
Body[dto.SuperNotificationTemplateUpdateForm]("form"),
))
r.log.Debugf("Registering route: Post /super/v1/notifications/broadcast -> notifications.Broadcast")
router.Post("/super/v1/notifications/broadcast"[len(r.Path()):], Func1(
r.notifications.Broadcast,
@@ -318,6 +337,20 @@ func (r *Routes) Register(router fiber.Router) {
r.payoutAccounts.List,
Query[dto.SuperPayoutAccountListFilter]("filter"),
))
r.log.Debugf("Registering route: Patch /super/v1/payout-accounts/:id<int> -> payoutAccounts.Update")
router.Patch("/super/v1/payout-accounts/:id<int>"[len(r.Path()):], Func3(
r.payoutAccounts.Update,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
Body[dto.SuperPayoutAccountUpdateForm]("form"),
))
r.log.Debugf("Registering route: Post /super/v1/creators/:tenantID<int>/payout-accounts -> payoutAccounts.Create")
router.Post("/super/v1/creators/:tenantID<int>/payout-accounts"[len(r.Path()):], Func3(
r.payoutAccounts.Create,
Local[*models.User]("__ctx_user"),
PathParam[int64]("tenantID"),
Body[dto.SuperPayoutAccountCreateForm]("form"),
))
r.log.Debugf("Registering route: Post /super/v1/payout-accounts/:id<int>/review -> payoutAccounts.Review")
router.Post("/super/v1/payout-accounts/:id<int>/review"[len(r.Path()):], Func3(
r.payoutAccounts.Review,
@@ -487,6 +520,13 @@ func (r *Routes) Register(router fiber.Router) {
router.Get("/super/v1/users/statuses"[len(r.Path()):], DataFunc0(
r.users.Statuses,
))
r.log.Debugf("Registering route: Patch /super/v1/users/:id<int> -> users.UpdateProfile")
router.Patch("/super/v1/users/:id<int>"[len(r.Path()):], Func3(
r.users.UpdateProfile,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
Body[dto.SuperUserProfileUpdateForm]("form"),
))
r.log.Debugf("Registering route: Patch /super/v1/users/:id<int>/roles -> users.UpdateRoles")
router.Patch("/super/v1/users/:id<int>/roles"[len(r.Path()):], Func2(
r.users.UpdateRoles,

View File

@@ -4,6 +4,7 @@ import (
dto "quyun/v2/app/http/super/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/app/services"
"quyun/v2/database/models"
"github.com/gofiber/fiber/v3"
)
@@ -233,6 +234,24 @@ func (c *users) UpdateRoles(ctx fiber.Ctx, id int64, form *dto.UserRolesUpdateFo
return services.Super.UpdateUserRoles(ctx, id, form)
}
// Update user profile
//
// @Router /super/v1/users/:id<int> [patch]
// @Summary Update user profile
// @Description Update user profile fields
// @Tags User
// @Accept json
// @Produce json
// @Param id path int64 true "User ID"
// @Param form body dto.SuperUserProfileUpdateForm true "Update form"
// @Success 200 {string} string "Updated"
// @Bind user local key(__ctx_user)
// @Bind id path
// @Bind form body
func (c *users) UpdateProfile(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperUserProfileUpdateForm) error {
return services.Super.UpdateUserProfile(ctx, user.ID, id, form)
}
// User statistics
//
// @Router /super/v1/users/statistics [get]

View File

@@ -261,10 +261,15 @@ func (s *super) ListUsers(ctx context.Context, filter *super_dto.UserListFilter)
CreatedAt: s.formatTime(u.CreatedAt),
UpdatedAt: s.formatTime(u.UpdatedAt),
},
Balance: u.Balance,
BalanceFrozen: u.BalanceFrozen,
OwnedTenantCount: ownedCountMap[u.ID],
JoinedTenantCount: joinedCountMap[u.ID],
Balance: u.Balance,
BalanceFrozen: u.BalanceFrozen,
Nickname: u.Nickname,
Avatar: u.Avatar,
Gender: u.Gender,
Bio: u.Bio,
IsRealNameVerified: u.IsRealNameVerified,
OwnedTenantCount: ownedCountMap[u.ID],
JoinedTenantCount: joinedCountMap[u.ID],
})
}
@@ -295,8 +300,13 @@ func (s *super) GetUser(ctx context.Context, id int64) (*super_dto.UserItem, err
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
},
Balance: u.Balance,
BalanceFrozen: u.BalanceFrozen,
Balance: u.Balance,
BalanceFrozen: u.BalanceFrozen,
Nickname: u.Nickname,
Avatar: u.Avatar,
Gender: u.Gender,
Bio: u.Bio,
IsRealNameVerified: u.IsRealNameVerified,
}, nil
}
@@ -705,6 +715,84 @@ func (s *super) UpdateUserRoles(ctx context.Context, id int64, form *super_dto.U
return nil
}
func (s *super) UpdateUserProfile(ctx context.Context, operatorID, userID int64, form *super_dto.SuperUserProfileUpdateForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if userID == 0 || form == nil {
return errorx.ErrBadRequest.WithMsg("更新参数不能为空")
}
if form.Gender != nil && !form.Gender.IsValid() {
return errorx.ErrBadRequest.WithMsg("性别非法")
}
tbl, q := models.UserQuery.QueryContext(ctx)
user, err := q.Where(tbl.ID.Eq(userID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound
}
return errorx.ErrDatabaseError.WithCause(err)
}
updates := make(map[string]any)
if form.Nickname != nil {
updates["nickname"] = strings.TrimSpace(*form.Nickname)
}
if form.Avatar != nil {
updates["avatar"] = strings.TrimSpace(*form.Avatar)
}
if form.Gender != nil {
updates["gender"] = *form.Gender
}
if form.Bio != nil {
updates["bio"] = strings.TrimSpace(*form.Bio)
}
if form.IsRealNameVerified != nil {
updates["is_real_name_verified"] = *form.IsRealNameVerified
if *form.IsRealNameVerified {
updates["verified_at"] = time.Now()
} else {
updates["verified_at"] = time.Time{}
}
}
if form.RealName != nil || form.IDCard != nil {
// 更新实名信息时保持原有元数据,避免覆盖其它字段。
metaMap := make(map[string]any)
if len(user.Metas) > 0 {
_ = json.Unmarshal(user.Metas, &metaMap)
}
if form.RealName != nil {
metaMap["real_name"] = strings.TrimSpace(*form.RealName)
}
if form.IDCard != nil {
idCard := strings.TrimSpace(*form.IDCard)
if idCard != "" {
metaMap["id_card"] = "ENC:" + idCard
} else {
metaMap["id_card"] = ""
}
}
raw, _ := json.Marshal(metaMap)
updates["metas"] = types.JSON(raw)
}
if len(updates) == 0 {
return errorx.ErrBadRequest.WithMsg("没有可更新的字段")
}
if _, err := q.Where(tbl.ID.Eq(userID)).Updates(updates); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if Audit != nil {
Audit.Log(ctx, 0, operatorID, "update_user_profile", cast.ToString(userID), "Update user profile")
}
return nil
}
func (s *super) ListTenants(ctx context.Context, filter *super_dto.TenantListFilter) (*requests.Pager, error) {
if filter == nil {
filter = &super_dto.TenantListFilter{}
@@ -1333,25 +1421,114 @@ func (s *super) ReviewCreatorApplication(ctx context.Context, operatorID, tenant
return nil
}
func (s *super) CreateTenant(ctx context.Context, form *super_dto.TenantCreateForm) error {
uid := form.AdminUserID
if _, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First(); err != nil {
return errorx.ErrRecordNotFound.WithMsg("用户不存在")
func (s *super) GetCreatorSettings(ctx context.Context, tenantID int64) (*v1_dto.Settings, error) {
if tenantID == 0 {
return nil, errorx.ErrBadRequest.WithMsg("租户ID不能为空")
}
t := &models.Tenant{
UserID: uid,
Name: form.Name,
Code: form.Code,
UUID: types.UUID(uuid.New()),
Status: consts.TenantStatusVerified,
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenantID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
if err := models.TenantQuery.WithContext(ctx).Create(t); err != nil {
cfg := t.Config.Data()
return &v1_dto.Settings{
ID: t.ID,
Name: t.Name,
Bio: cfg.Bio,
Avatar: cfg.Avatar,
Cover: cfg.Cover,
Description: cfg.Description,
}, nil
}
func (s *super) UpdateCreatorSettings(ctx context.Context, operatorID, tenantID int64, form *v1_dto.Settings) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if tenantID == 0 || form == nil {
return errorx.ErrBadRequest.WithMsg("更新参数不能为空")
}
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenantID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound
}
return errorx.ErrDatabaseError.WithCause(err)
}
// 超管可直接更新租户设置与展示信息。
cfg := t.Config.Data()
cfg.Bio = form.Bio
cfg.Avatar = form.Avatar
cfg.Cover = form.Cover
cfg.Description = form.Description
_, err = models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenantID)).Updates(&models.Tenant{
Name: form.Name,
Config: types.NewJSONType(cfg),
})
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if Audit != nil {
Audit.Log(ctx, tenantID, operatorID, "update_creator_settings", cast.ToString(tenantID), "Update creator settings")
}
return nil
}
func (s *super) CreateTenant(ctx context.Context, form *super_dto.TenantCreateForm) error {
if form == nil {
return errorx.ErrBadRequest.WithMsg("创建参数不能为空")
}
if form.Duration <= 0 {
return errorx.ErrBadRequest.WithMsg("租户有效期不能为空")
}
uid := form.AdminUserID
expiredAt := time.Now().AddDate(0, 0, form.Duration)
return models.Q.Transaction(func(tx *models.Query) error {
// 校验管理员用户存在,避免创建脏数据。
if _, err := tx.User.WithContext(ctx).Where(tx.User.ID.Eq(uid)).First(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("用户不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
t := &models.Tenant{
UserID: uid,
Name: form.Name,
Code: form.Code,
UUID: types.UUID(uuid.New()),
Status: consts.TenantStatusVerified,
ExpiredAt: expiredAt,
}
if err := tx.Tenant.WithContext(ctx).Create(t); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
// 同步写入管理员成员关系,保证租户成员视角一致。
tu := &models.TenantUser{
TenantID: t.ID,
UserID: uid,
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleTenantAdmin},
Status: consts.UserStatusVerified,
}
if err := tx.TenantUser.WithContext(ctx).Create(tu); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
})
}
func (s *super) GetTenant(ctx context.Context, id int64) (*super_dto.TenantItem, error) {
tbl, q := models.TenantQuery.QueryContext(ctx)
t, err := q.Where(tbl.ID.Eq(id)).First()
@@ -1611,6 +1788,123 @@ func (s *super) ListPayoutAccounts(ctx context.Context, filter *super_dto.SuperP
}, nil
}
func (s *super) CreatePayoutAccount(ctx context.Context, operatorID, tenantID int64, form *super_dto.SuperPayoutAccountCreateForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if tenantID == 0 || form == nil {
return errorx.ErrBadRequest.WithMsg("创建参数不能为空")
}
if form.UserID == 0 {
return errorx.ErrBadRequest.WithMsg("用户ID不能为空")
}
accountType := strings.TrimSpace(form.Type)
if accountType == "" {
return errorx.ErrBadRequest.WithMsg("账户类型不能为空")
}
if !consts.PayoutAccountType(accountType).IsValid() {
return errorx.ErrBadRequest.WithMsg("账户类型非法")
}
// 校验租户与用户存在,避免关联脏数据。
if _, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenantID)).First(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
if _, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(form.UserID)).First(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("用户不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
pa := &models.PayoutAccount{
TenantID: tenantID,
UserID: form.UserID,
Type: consts.PayoutAccountType(accountType),
Name: strings.TrimSpace(form.Name),
Account: strings.TrimSpace(form.Account),
Realname: strings.TrimSpace(form.Realname),
Status: consts.PayoutAccountStatusPending,
}
if err := models.PayoutAccountQuery.WithContext(ctx).Create(pa); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if Audit != nil {
Audit.Log(ctx, tenantID, operatorID, "create_payout_account", cast.ToString(pa.ID), "Create payout account")
}
return nil
}
func (s *super) UpdatePayoutAccount(ctx context.Context, operatorID, id int64, form *super_dto.SuperPayoutAccountUpdateForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if id == 0 || form == nil {
return errorx.ErrBadRequest.WithMsg("更新参数不能为空")
}
tbl, q := models.PayoutAccountQuery.QueryContext(ctx)
account, err := q.Where(tbl.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("结算账户不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
updates := make(map[string]any)
changed := false
if form.Type != nil {
typ := strings.TrimSpace(*form.Type)
if typ == "" {
return errorx.ErrBadRequest.WithMsg("账户类型不能为空")
}
if !consts.PayoutAccountType(typ).IsValid() {
return errorx.ErrBadRequest.WithMsg("账户类型非法")
}
updates["type"] = consts.PayoutAccountType(typ)
changed = true
}
if form.Name != nil {
updates["name"] = strings.TrimSpace(*form.Name)
changed = true
}
if form.Account != nil {
updates["account"] = strings.TrimSpace(*form.Account)
changed = true
}
if form.Realname != nil {
updates["realname"] = strings.TrimSpace(*form.Realname)
changed = true
}
if len(updates) == 0 {
return errorx.ErrBadRequest.WithMsg("没有可更新的字段")
}
if changed && account.Status == consts.PayoutAccountStatusApproved {
updates["status"] = consts.PayoutAccountStatusPending
updates["reviewed_by"] = int64(0)
updates["reviewed_at"] = time.Time{}
updates["review_reason"] = ""
}
if _, err := q.Where(tbl.ID.Eq(id)).Updates(updates); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if Audit != nil {
Audit.Log(ctx, account.TenantID, operatorID, "update_payout_account", cast.ToString(id), "Update payout account")
}
return nil
}
func (s *super) RemovePayoutAccount(ctx context.Context, operatorID, id int64) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
@@ -4716,6 +5010,112 @@ func (s *super) CreateNotificationTemplate(ctx context.Context, form *super_dto.
return item, nil
}
func (s *super) UpdateNotificationTemplate(ctx context.Context, operatorID, id int64, form *super_dto.SuperNotificationTemplateUpdateForm) (*super_dto.SuperNotificationTemplateItem, error) {
if operatorID == 0 {
return nil, errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if id == 0 || form == nil {
return nil, errorx.ErrBadRequest.WithMsg("模板参数不能为空")
}
tbl, q := models.NotificationTemplateQuery.QueryContext(ctx)
if _, err := q.Where(tbl.ID.Eq(id)).First(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound.WithMsg("模板不存在")
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
updates := make(map[string]any)
if form.TenantID != nil {
if *form.TenantID < 0 {
return nil, errorx.ErrBadRequest.WithMsg("租户ID无效")
}
if *form.TenantID > 0 {
// 校验租户存在,避免模板指向无效租户。
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
if _, err := tenantQuery.Where(tenantTbl.ID.Eq(*form.TenantID)).First(); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
}
updates["tenant_id"] = *form.TenantID
}
if form.Name != nil {
name := strings.TrimSpace(*form.Name)
if name == "" {
return nil, errorx.ErrBadRequest.WithMsg("模板名称不能为空")
}
updates["name"] = name
}
if form.Type != nil {
if !form.Type.IsValid() {
return nil, errorx.ErrBadRequest.WithMsg("通知类型非法")
}
updates["type"] = *form.Type
}
if form.Title != nil {
title := strings.TrimSpace(*form.Title)
if title == "" {
return nil, errorx.ErrBadRequest.WithMsg("模板标题不能为空")
}
updates["title"] = title
}
if form.Content != nil {
content := strings.TrimSpace(*form.Content)
if content == "" {
return nil, errorx.ErrBadRequest.WithMsg("模板内容不能为空")
}
updates["content"] = content
}
if form.IsActive != nil {
updates["is_active"] = *form.IsActive
}
if len(updates) == 0 {
return nil, errorx.ErrBadRequest.WithMsg("没有可更新的字段")
}
if _, err := q.Where(tbl.ID.Eq(id)).Updates(updates); err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
updated, err := q.Where(tbl.ID.Eq(id)).First()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
item := &super_dto.SuperNotificationTemplateItem{
ID: updated.ID,
TenantID: updated.TenantID,
Name: updated.Name,
Type: updated.Type,
Title: updated.Title,
Content: updated.Content,
IsActive: updated.IsActive,
CreatedAt: s.formatTime(updated.CreatedAt),
UpdatedAt: s.formatTime(updated.UpdatedAt),
}
if updated.TenantID > 0 {
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
tenant, err := tenantQuery.Where(tenantTbl.ID.Eq(updated.TenantID)).First()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
if tenant != nil {
item.TenantCode = tenant.Code
item.TenantName = tenant.Name
}
}
if Audit != nil {
Audit.Log(ctx, updated.TenantID, operatorID, "update_notification_template", cast.ToString(id), "Update notification template")
}
return item, nil
}
func (s *super) ListAuditLogs(ctx context.Context, filter *super_dto.SuperAuditLogListFilter) (*requests.Pager, error) {
if filter == nil {
filter = &super_dto.SuperAuditLogListFilter{}

View File

@@ -2,6 +2,7 @@ package services
import (
"database/sql"
"encoding/json"
"errors"
"testing"
"time"
@@ -9,8 +10,10 @@ import (
"quyun/v2/app/commands/testx"
"quyun/v2/app/errorx"
super_dto "quyun/v2/app/http/super/v1/dto"
v1_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/database"
"quyun/v2/database/fields"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
@@ -140,10 +143,12 @@ func (s *SuperTestSuite) Test_CreateTenant() {
models.UserQuery.WithContext(ctx).Create(u)
Convey("should create tenant", func() {
startAt := time.Now()
form := &super_dto.TenantCreateForm{
Name: "Super Tenant",
Code: "st1",
AdminUserID: u.ID,
Duration: 7,
}
err := Super.CreateTenant(ctx, form)
So(err, ShouldBeNil)
@@ -153,6 +158,13 @@ func (s *SuperTestSuite) Test_CreateTenant() {
So(t.Name, ShouldEqual, "Super Tenant")
So(t.UserID, ShouldEqual, u.ID)
So(t.Status, ShouldEqual, consts.TenantStatusVerified)
So(t.ExpiredAt.After(startAt), ShouldBeTrue)
tu, _ := models.TenantUserQuery.WithContext(ctx).
Where(models.TenantUserQuery.TenantID.Eq(t.ID), models.TenantUserQuery.UserID.Eq(u.ID)).
First()
So(tu, ShouldNotBeNil)
So(tu.Status, ShouldEqual, consts.UserStatusVerified)
})
})
}
@@ -1203,3 +1215,195 @@ func (s *SuperTestSuite) Test_PayoutAccountReview() {
})
})
}
func (s *SuperTestSuite) Test_UpdateUserProfile() {
Convey("UpdateUserProfile", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameUser)
meta := types.JSON([]byte(`{"real_name":"Old Name","id_card":"ENC:1111"}`))
u := &models.User{
Username: "profile_user",
Nickname: "Old",
Avatar: "http://old-avatar",
Gender: consts.GenderSecret,
Bio: "old bio",
Metas: meta,
}
models.UserQuery.WithContext(ctx).Create(u)
Convey("should update profile fields and real-name meta", func() {
form := &super_dto.SuperUserProfileUpdateForm{
Nickname: lo.ToPtr("New Nick"),
Avatar: lo.ToPtr("http://new-avatar"),
Gender: lo.ToPtr(consts.GenderMale),
Bio: lo.ToPtr("new bio"),
IsRealNameVerified: lo.ToPtr(true),
RealName: lo.ToPtr("New Name"),
IDCard: lo.ToPtr("123456789012345678"),
}
err := Super.UpdateUserProfile(ctx, 1001, u.ID, form)
So(err, ShouldBeNil)
updated, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(u.ID)).First()
So(err, ShouldBeNil)
So(updated.Nickname, ShouldEqual, "New Nick")
So(updated.Avatar, ShouldEqual, "http://new-avatar")
So(updated.Gender, ShouldEqual, consts.GenderMale)
So(updated.Bio, ShouldEqual, "new bio")
So(updated.IsRealNameVerified, ShouldBeTrue)
So(updated.VerifiedAt.IsZero(), ShouldBeFalse)
metaMap := make(map[string]interface{})
So(json.Unmarshal(updated.Metas, &metaMap), ShouldBeNil)
So(metaMap["real_name"], ShouldEqual, "New Name")
So(metaMap["id_card"], ShouldEqual, "ENC:123456789012345678")
})
})
}
func (s *SuperTestSuite) Test_UpdateCreatorSettings() {
Convey("UpdateCreatorSettings", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameUser)
owner := &models.User{Username: "settings_owner"}
models.UserQuery.WithContext(ctx).Create(owner)
tenant := &models.Tenant{
UserID: owner.ID,
Name: "Old Tenant",
Code: "creator-settings",
Status: consts.TenantStatusVerified,
Config: types.NewJSONType(fields.TenantConfig{}),
}
models.TenantQuery.WithContext(ctx).Create(tenant)
form := &v1_dto.Settings{
Name: "New Tenant",
Bio: "new bio",
Avatar: "http://avatar",
Cover: "http://cover",
Description: "new description",
}
err := Super.UpdateCreatorSettings(ctx, 2001, tenant.ID, form)
So(err, ShouldBeNil)
updated, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenant.ID)).First()
So(err, ShouldBeNil)
So(updated.Name, ShouldEqual, "New Tenant")
cfg := updated.Config.Data()
So(cfg.Bio, ShouldEqual, "new bio")
So(cfg.Avatar, ShouldEqual, "http://avatar")
So(cfg.Cover, ShouldEqual, "http://cover")
So(cfg.Description, ShouldEqual, "new description")
})
}
func (s *SuperTestSuite) Test_PayoutAccountCreateUpdate() {
Convey("PayoutAccountCreateUpdate", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNamePayoutAccount, models.TableNameUser, models.TableNameTenant)
admin := &models.User{Username: "payout_admin_2"}
owner := &models.User{Username: "payout_owner_2"}
models.UserQuery.WithContext(ctx).Create(admin, owner)
tenant := &models.Tenant{
UserID: owner.ID,
Name: "Payout Tenant 2",
Code: "payout-2",
Status: consts.TenantStatusVerified,
}
models.TenantQuery.WithContext(ctx).Create(tenant)
Convey("should create payout account", func() {
err := Super.CreatePayoutAccount(ctx, admin.ID, tenant.ID, &super_dto.SuperPayoutAccountCreateForm{
UserID: owner.ID,
Type: "bank",
Name: "Bank",
Account: "123",
Realname: "Owner",
})
So(err, ShouldBeNil)
account, err := models.PayoutAccountQuery.WithContext(ctx).
Where(models.PayoutAccountQuery.TenantID.Eq(tenant.ID), models.PayoutAccountQuery.UserID.Eq(owner.ID)).
First()
So(err, ShouldBeNil)
So(account.Status, ShouldEqual, consts.PayoutAccountStatusPending)
})
Convey("should reset status when updating approved account", func() {
account := &models.PayoutAccount{
TenantID: tenant.ID,
UserID: owner.ID,
Type: consts.PayoutAccountTypeBank,
Name: "Bank",
Account: "111",
Realname: "Owner",
Status: consts.PayoutAccountStatusApproved,
ReviewedBy: admin.ID,
ReviewReason: "ok",
ReviewedAt: time.Now(),
}
models.PayoutAccountQuery.WithContext(ctx).Create(account)
err := Super.UpdatePayoutAccount(ctx, admin.ID, account.ID, &super_dto.SuperPayoutAccountUpdateForm{
Account: lo.ToPtr("222"),
})
So(err, ShouldBeNil)
updated, err := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.ID.Eq(account.ID)).First()
So(err, ShouldBeNil)
So(updated.Account, ShouldEqual, "222")
So(updated.Status, ShouldEqual, consts.PayoutAccountStatusPending)
So(updated.ReviewedBy, ShouldEqual, int64(0))
So(updated.ReviewReason, ShouldEqual, "")
})
})
}
func (s *SuperTestSuite) Test_UpdateNotificationTemplate() {
Convey("UpdateNotificationTemplate", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameNotificationTemplate, models.TableNameTenant, models.TableNameUser)
owner := &models.User{Username: "tmpl_owner"}
models.UserQuery.WithContext(ctx).Create(owner)
tenant := &models.Tenant{
UserID: owner.ID,
Name: "Template Tenant",
Code: "tmpl",
Status: consts.TenantStatusVerified,
}
models.TenantQuery.WithContext(ctx).Create(tenant)
tmpl := &models.NotificationTemplate{
TenantID: tenant.ID,
Name: "Old Template",
Type: consts.NotificationTypeSystem,
Title: "Old Title",
Content: "Old Content",
IsActive: true,
}
models.NotificationTemplateQuery.WithContext(ctx).Create(tmpl)
item, err := Super.UpdateNotificationTemplate(ctx, 3001, tmpl.ID, &super_dto.SuperNotificationTemplateUpdateForm{
Name: lo.ToPtr("New Template"),
Title: lo.ToPtr("New Title"),
Content: lo.ToPtr("New Content"),
IsActive: lo.ToPtr(false),
})
So(err, ShouldBeNil)
So(item.Name, ShouldEqual, "New Template")
So(item.IsActive, ShouldBeFalse)
updated, err := models.NotificationTemplateQuery.WithContext(ctx).Where(models.NotificationTemplateQuery.ID.Eq(tmpl.ID)).First()
So(err, ShouldBeNil)
So(updated.Title, ShouldEqual, "New Title")
So(updated.Content, ShouldEqual, "New Content")
So(updated.IsActive, ShouldBeFalse)
})
}

81
backend/config.minio.toml Normal file
View File

@@ -0,0 +1,81 @@
# MinIO configuration for local S3-compatible storage.
# Use with: ENV_LOCAL=minio
[App]
Mode = "testing"
BaseURI = "http://localhost:8080"
[App.Super]
Token = ""
[Http]
Port = 18080
[Http.Cors]
Mode = "dev"
[[Http.Cors.Whitelist]]
AllowOrigin = "http://localhost:5173"
AllowHeaders = "Content-Type,Authorization"
AllowMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
ExposeHeaders = "*"
AllowCredentials = true
[Http.RateLimit]
Enabled = true
Max = 5
WindowSeconds = 10
Message = "Too Many Requests"
[Http.RateLimit.Redis]
Addrs = ["127.0.0.1:6379"]
Username = ""
Password = "testpass"
DB = 2
Prefix = "rl:"
[Database]
Host = "10.1.1.2"
Port = 5433
Database = "quyun_v2"
Username = "postgres"
Password = "xixi0202"
SslMode = "disable"
TimeZone = "Asia/Shanghai"
MaxIdleConns = 10
MaxOpenConns = 100
ConnMaxLifetime = "1800s"
ConnMaxIdleTime = "300s"
[JWT]
SigningKey = "test-secret"
ExpiresTime = "168h"
Issuer = "v2"
[HashIDs]
Salt = "test-salt"
MinLength = 8
[Redis]
Host = "127.0.0.1"
Port = 6379
Password = ""
DB = 0
PoolSize = 20
MinIdleConns = 5
MaxRetries = 3
DialTimeout = "5s"
ReadTimeout = "3s"
WriteTimeout = "3s"
[Storage]
Type = "s3"
LocalPath = "./storage"
Secret = "test-storage-secret"
BaseURL = "/v1/storage"
AccessKey = "minioadmin"
SecretKey = "minioadmin"
Region = "us-east-1"
Bucket = "quyun-assets"
Endpoint = "http://127.0.0.1:9000"
PathStyle = true

View File

@@ -997,6 +997,121 @@ const docTemplate = `{
}
}
},
"/super/v1/creators/{tenantID}/payout-accounts": {
"post": {
"description": "Create payout account for tenant",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Finance"
],
"summary": "Create payout account",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Tenant ID",
"name": "tenantID",
"in": "path",
"required": true
},
{
"description": "Create form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperPayoutAccountCreateForm"
}
}
],
"responses": {
"200": {
"description": "Created",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/creators/{tenantID}/settings": {
"get": {
"description": "Get creator settings by tenant ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Creator"
],
"summary": "Get creator settings",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Tenant ID",
"name": "tenantID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.Settings"
}
}
}
},
"put": {
"description": "Update creator settings by tenant ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Creator"
],
"summary": "Update creator settings",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Tenant ID",
"name": "tenantID",
"in": "path",
"required": true
},
{
"description": "Settings form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Settings"
}
}
],
"responses": {
"200": {
"description": "Updated",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/finance/anomalies/balances": {
"get": {
"description": "List balance anomalies across users",
@@ -1358,6 +1473,48 @@ const docTemplate = `{
}
}
},
"/super/v1/notifications/templates/{id}": {
"patch": {
"description": "Update notification template",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Notification"
],
"summary": "Update notification template",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Template ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperNotificationTemplateUpdateForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.SuperNotificationTemplateItem"
}
}
}
}
},
"/super/v1/orders": {
"get": {
"description": "List orders",
@@ -1675,6 +1832,46 @@ const docTemplate = `{
}
}
}
},
"patch": {
"description": "Update payout account across tenants",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Finance"
],
"summary": "Update payout account",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Payout account ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperPayoutAccountUpdateForm"
}
}
],
"responses": {
"200": {
"description": "Updated",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/payout-accounts/{id}/review": {
@@ -2813,6 +3010,46 @@ const docTemplate = `{
}
}
}
},
"patch": {
"description": "Update user profile fields",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "Update user profile",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperUserProfileUpdateForm"
}
}
],
"responses": {
"200": {
"description": "Updated",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/users/{id}/coupons": {
@@ -9099,6 +9336,39 @@ const docTemplate = `{
}
}
},
"dto.SuperNotificationTemplateUpdateForm": {
"type": "object",
"properties": {
"content": {
"description": "Content 通知内容(可选)。",
"type": "string"
},
"is_active": {
"description": "IsActive 是否启用(可选)。",
"type": "boolean"
},
"name": {
"description": "Name 模板名称(可选)。",
"type": "string"
},
"tenant_id": {
"description": "TenantID 租户ID不传代表不修改租户归属。",
"type": "integer"
},
"title": {
"description": "Title 通知标题(可选)。",
"type": "string"
},
"type": {
"description": "Type 通知类型system/order/audit/interaction可选。",
"allOf": [
{
"$ref": "#/definitions/consts.NotificationType"
}
]
}
}
},
"dto.SuperOrderAnomalyItem": {
"type": "object",
"properties": {
@@ -9383,6 +9653,45 @@ const docTemplate = `{
}
}
},
"dto.SuperPayoutAccountCreateForm": {
"type": "object",
"required": [
"account",
"name",
"realname",
"type",
"user_id"
],
"properties": {
"account": {
"description": "Account 收款账号。",
"type": "string",
"maxLength": 128
},
"name": {
"description": "Name 账户名称/开户行。",
"type": "string",
"maxLength": 128
},
"realname": {
"description": "Realname 收款人姓名。",
"type": "string",
"maxLength": 128
},
"type": {
"description": "Type 账户类型bank/alipay。",
"type": "string",
"enum": [
"bank",
"alipay"
]
},
"user_id": {
"description": "UserID 收款账户归属用户ID。",
"type": "integer"
}
}
},
"dto.SuperPayoutAccountItem": {
"type": "object",
"properties": {
@@ -9480,6 +9789,34 @@ const docTemplate = `{
}
}
},
"dto.SuperPayoutAccountUpdateForm": {
"type": "object",
"properties": {
"account": {
"description": "Account 收款账号(可选)。",
"type": "string",
"maxLength": 128
},
"name": {
"description": "Name 账户名称/开户行(可选)。",
"type": "string",
"maxLength": 128
},
"realname": {
"description": "Realname 收款人姓名(可选)。",
"type": "string",
"maxLength": 128
},
"type": {
"description": "Type 账户类型bank/alipay可选。",
"type": "string",
"enum": [
"bank",
"alipay"
]
}
}
},
"dto.SuperReportExportForm": {
"type": "object",
"properties": {
@@ -9954,6 +10291,43 @@ const docTemplate = `{
}
}
},
"dto.SuperUserProfileUpdateForm": {
"type": "object",
"properties": {
"avatar": {
"description": "Avatar 头像URL可选空字符串表示清空。",
"type": "string"
},
"bio": {
"description": "Bio 个人简介(可选,空字符串表示清空)。",
"type": "string"
},
"gender": {
"description": "Gender 性别(可选)。",
"allOf": [
{
"$ref": "#/definitions/consts.Gender"
}
]
},
"id_card": {
"description": "IDCard 身份证号(可选,用于更新实名认证信息)。",
"type": "string"
},
"is_real_name_verified": {
"description": "IsRealNameVerified 是否已实名认证(可选)。",
"type": "boolean"
},
"nickname": {
"description": "Nickname 昵称(可选,空字符串表示清空)。",
"type": "string"
},
"real_name": {
"description": "RealName 真实姓名(可选,用于更新实名认证信息)。",
"type": "string"
}
}
},
"dto.SuperUserRealNameResponse": {
"type": "object",
"properties": {
@@ -10663,6 +11037,10 @@ const docTemplate = `{
"dto.UserItem": {
"type": "object",
"properties": {
"avatar": {
"description": "Avatar 用户头像URL资料展示。",
"type": "string"
},
"balance": {
"description": "Balance 账户可用余额(分)。",
"type": "integer"
@@ -10671,18 +11049,38 @@ const docTemplate = `{
"description": "BalanceFrozen 账户冻结余额(分)。",
"type": "integer"
},
"bio": {
"description": "Bio 用户个人简介。",
"type": "string"
},
"created_at": {
"description": "CreatedAt 创建时间RFC3339。",
"type": "string"
},
"gender": {
"description": "Gender 用户性别male/female/secret。",
"allOf": [
{
"$ref": "#/definitions/consts.Gender"
}
]
},
"id": {
"description": "ID 用户ID。",
"type": "integer"
},
"is_real_name_verified": {
"description": "IsRealNameVerified 是否已实名认证。",
"type": "boolean"
},
"joined_tenant_count": {
"description": "JoinedTenantCount 加入的租户数量。",
"type": "integer"
},
"nickname": {
"description": "Nickname 用户昵称(资料展示)。",
"type": "string"
},
"owned_tenant_count": {
"description": "OwnedTenantCount 拥有的租户数量。",
"type": "integer"

View File

@@ -991,6 +991,121 @@
}
}
},
"/super/v1/creators/{tenantID}/payout-accounts": {
"post": {
"description": "Create payout account for tenant",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Finance"
],
"summary": "Create payout account",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Tenant ID",
"name": "tenantID",
"in": "path",
"required": true
},
{
"description": "Create form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperPayoutAccountCreateForm"
}
}
],
"responses": {
"200": {
"description": "Created",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/creators/{tenantID}/settings": {
"get": {
"description": "Get creator settings by tenant ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Creator"
],
"summary": "Get creator settings",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Tenant ID",
"name": "tenantID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.Settings"
}
}
}
},
"put": {
"description": "Update creator settings by tenant ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Creator"
],
"summary": "Update creator settings",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Tenant ID",
"name": "tenantID",
"in": "path",
"required": true
},
{
"description": "Settings form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.Settings"
}
}
],
"responses": {
"200": {
"description": "Updated",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/finance/anomalies/balances": {
"get": {
"description": "List balance anomalies across users",
@@ -1352,6 +1467,48 @@
}
}
},
"/super/v1/notifications/templates/{id}": {
"patch": {
"description": "Update notification template",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Notification"
],
"summary": "Update notification template",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Template ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperNotificationTemplateUpdateForm"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.SuperNotificationTemplateItem"
}
}
}
}
},
"/super/v1/orders": {
"get": {
"description": "List orders",
@@ -1669,6 +1826,46 @@
}
}
}
},
"patch": {
"description": "Update payout account across tenants",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Finance"
],
"summary": "Update payout account",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Payout account ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperPayoutAccountUpdateForm"
}
}
],
"responses": {
"200": {
"description": "Updated",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/payout-accounts/{id}/review": {
@@ -2807,6 +3004,46 @@
}
}
}
},
"patch": {
"description": "Update user profile fields",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "Update user profile",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Update form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperUserProfileUpdateForm"
}
}
],
"responses": {
"200": {
"description": "Updated",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/users/{id}/coupons": {
@@ -9093,6 +9330,39 @@
}
}
},
"dto.SuperNotificationTemplateUpdateForm": {
"type": "object",
"properties": {
"content": {
"description": "Content 通知内容(可选)。",
"type": "string"
},
"is_active": {
"description": "IsActive 是否启用(可选)。",
"type": "boolean"
},
"name": {
"description": "Name 模板名称(可选)。",
"type": "string"
},
"tenant_id": {
"description": "TenantID 租户ID不传代表不修改租户归属。",
"type": "integer"
},
"title": {
"description": "Title 通知标题(可选)。",
"type": "string"
},
"type": {
"description": "Type 通知类型system/order/audit/interaction可选。",
"allOf": [
{
"$ref": "#/definitions/consts.NotificationType"
}
]
}
}
},
"dto.SuperOrderAnomalyItem": {
"type": "object",
"properties": {
@@ -9377,6 +9647,45 @@
}
}
},
"dto.SuperPayoutAccountCreateForm": {
"type": "object",
"required": [
"account",
"name",
"realname",
"type",
"user_id"
],
"properties": {
"account": {
"description": "Account 收款账号。",
"type": "string",
"maxLength": 128
},
"name": {
"description": "Name 账户名称/开户行。",
"type": "string",
"maxLength": 128
},
"realname": {
"description": "Realname 收款人姓名。",
"type": "string",
"maxLength": 128
},
"type": {
"description": "Type 账户类型bank/alipay。",
"type": "string",
"enum": [
"bank",
"alipay"
]
},
"user_id": {
"description": "UserID 收款账户归属用户ID。",
"type": "integer"
}
}
},
"dto.SuperPayoutAccountItem": {
"type": "object",
"properties": {
@@ -9474,6 +9783,34 @@
}
}
},
"dto.SuperPayoutAccountUpdateForm": {
"type": "object",
"properties": {
"account": {
"description": "Account 收款账号(可选)。",
"type": "string",
"maxLength": 128
},
"name": {
"description": "Name 账户名称/开户行(可选)。",
"type": "string",
"maxLength": 128
},
"realname": {
"description": "Realname 收款人姓名(可选)。",
"type": "string",
"maxLength": 128
},
"type": {
"description": "Type 账户类型bank/alipay可选。",
"type": "string",
"enum": [
"bank",
"alipay"
]
}
}
},
"dto.SuperReportExportForm": {
"type": "object",
"properties": {
@@ -9948,6 +10285,43 @@
}
}
},
"dto.SuperUserProfileUpdateForm": {
"type": "object",
"properties": {
"avatar": {
"description": "Avatar 头像URL可选空字符串表示清空。",
"type": "string"
},
"bio": {
"description": "Bio 个人简介(可选,空字符串表示清空)。",
"type": "string"
},
"gender": {
"description": "Gender 性别(可选)。",
"allOf": [
{
"$ref": "#/definitions/consts.Gender"
}
]
},
"id_card": {
"description": "IDCard 身份证号(可选,用于更新实名认证信息)。",
"type": "string"
},
"is_real_name_verified": {
"description": "IsRealNameVerified 是否已实名认证(可选)。",
"type": "boolean"
},
"nickname": {
"description": "Nickname 昵称(可选,空字符串表示清空)。",
"type": "string"
},
"real_name": {
"description": "RealName 真实姓名(可选,用于更新实名认证信息)。",
"type": "string"
}
}
},
"dto.SuperUserRealNameResponse": {
"type": "object",
"properties": {
@@ -10657,6 +11031,10 @@
"dto.UserItem": {
"type": "object",
"properties": {
"avatar": {
"description": "Avatar 用户头像URL资料展示。",
"type": "string"
},
"balance": {
"description": "Balance 账户可用余额(分)。",
"type": "integer"
@@ -10665,18 +11043,38 @@
"description": "BalanceFrozen 账户冻结余额(分)。",
"type": "integer"
},
"bio": {
"description": "Bio 用户个人简介。",
"type": "string"
},
"created_at": {
"description": "CreatedAt 创建时间RFC3339。",
"type": "string"
},
"gender": {
"description": "Gender 用户性别male/female/secret。",
"allOf": [
{
"$ref": "#/definitions/consts.Gender"
}
]
},
"id": {
"description": "ID 用户ID。",
"type": "integer"
},
"is_real_name_verified": {
"description": "IsRealNameVerified 是否已实名认证。",
"type": "boolean"
},
"joined_tenant_count": {
"description": "JoinedTenantCount 加入的租户数量。",
"type": "integer"
},
"nickname": {
"description": "Nickname 用户昵称(资料展示)。",
"type": "string"
},
"owned_tenant_count": {
"description": "OwnedTenantCount 拥有的租户数量。",
"type": "integer"

View File

@@ -2032,6 +2032,28 @@ definitions:
description: UpdatedAt 更新时间RFC3339
type: string
type: object
dto.SuperNotificationTemplateUpdateForm:
properties:
content:
description: Content 通知内容(可选)。
type: string
is_active:
description: IsActive 是否启用(可选)。
type: boolean
name:
description: Name 模板名称(可选)。
type: string
tenant_id:
description: TenantID 租户ID不传代表不修改租户归属
type: integer
title:
description: Title 通知标题(可选)。
type: string
type:
allOf:
- $ref: '#/definitions/consts.NotificationType'
description: Type 通知类型system/order/audit/interaction可选
type: object
dto.SuperOrderAnomalyItem:
properties:
amount_paid:
@@ -2219,6 +2241,36 @@ definitions:
description: Reason 退款原因说明。
type: string
type: object
dto.SuperPayoutAccountCreateForm:
properties:
account:
description: Account 收款账号。
maxLength: 128
type: string
name:
description: Name 账户名称/开户行。
maxLength: 128
type: string
realname:
description: Realname 收款人姓名。
maxLength: 128
type: string
type:
description: Type 账户类型bank/alipay
enum:
- bank
- alipay
type: string
user_id:
description: UserID 收款账户归属用户ID。
type: integer
required:
- account
- name
- realname
- type
- user_id
type: object
dto.SuperPayoutAccountItem:
properties:
account:
@@ -2288,6 +2340,27 @@ definitions:
required:
- action
type: object
dto.SuperPayoutAccountUpdateForm:
properties:
account:
description: Account 收款账号(可选)。
maxLength: 128
type: string
name:
description: Name 账户名称/开户行(可选)。
maxLength: 128
type: string
realname:
description: Realname 收款人姓名(可选)。
maxLength: 128
type: string
type:
description: Type 账户类型bank/alipay可选
enum:
- bank
- alipay
type: string
type: object
dto.SuperReportExportForm:
properties:
end_at:
@@ -2609,6 +2682,31 @@ definitions:
description: Type 通知类型。
type: string
type: object
dto.SuperUserProfileUpdateForm:
properties:
avatar:
description: Avatar 头像URL可选空字符串表示清空
type: string
bio:
description: Bio 个人简介(可选,空字符串表示清空)。
type: string
gender:
allOf:
- $ref: '#/definitions/consts.Gender'
description: Gender 性别(可选)。
id_card:
description: IDCard 身份证号(可选,用于更新实名认证信息)。
type: string
is_real_name_verified:
description: IsRealNameVerified 是否已实名认证(可选)。
type: boolean
nickname:
description: Nickname 昵称(可选,空字符串表示清空)。
type: string
real_name:
description: RealName 真实姓名(可选,用于更新实名认证信息)。
type: string
type: object
dto.SuperUserRealNameResponse:
properties:
id_card_masked:
@@ -3106,21 +3204,37 @@ definitions:
type: object
dto.UserItem:
properties:
avatar:
description: Avatar 用户头像URL资料展示
type: string
balance:
description: Balance 账户可用余额(分)。
type: integer
balance_frozen:
description: BalanceFrozen 账户冻结余额(分)。
type: integer
bio:
description: Bio 用户个人简介。
type: string
created_at:
description: CreatedAt 创建时间RFC3339
type: string
gender:
allOf:
- $ref: '#/definitions/consts.Gender'
description: Gender 用户性别male/female/secret
id:
description: ID 用户ID。
type: integer
is_real_name_verified:
description: IsRealNameVerified 是否已实名认证。
type: boolean
joined_tenant_count:
description: JoinedTenantCount 加入的租户数量。
type: integer
nickname:
description: Nickname 用户昵称(资料展示)。
type: string
owned_tenant_count:
description: OwnedTenantCount 拥有的租户数量。
type: integer
@@ -4060,6 +4174,83 @@ paths:
summary: List creators
tags:
- Creator
/super/v1/creators/{tenantID}/payout-accounts:
post:
consumes:
- application/json
description: Create payout account for tenant
parameters:
- description: Tenant ID
format: int64
in: path
name: tenantID
required: true
type: integer
- description: Create form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperPayoutAccountCreateForm'
produces:
- application/json
responses:
"200":
description: Created
schema:
type: string
summary: Create payout account
tags:
- Finance
/super/v1/creators/{tenantID}/settings:
get:
consumes:
- application/json
description: Get creator settings by tenant ID
parameters:
- description: Tenant ID
format: int64
in: path
name: tenantID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.Settings'
summary: Get creator settings
tags:
- Creator
put:
consumes:
- application/json
description: Update creator settings by tenant ID
parameters:
- description: Tenant ID
format: int64
in: path
name: tenantID
required: true
type: integer
- description: Settings form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.Settings'
produces:
- application/json
responses:
"200":
description: Updated
schema:
type: string
summary: Update creator settings
tags:
- Creator
/super/v1/finance/anomalies/balances:
get:
consumes:
@@ -4281,6 +4472,34 @@ paths:
summary: Create notification template
tags:
- Notification
/super/v1/notifications/templates/{id}:
patch:
consumes:
- application/json
description: Update notification template
parameters:
- description: Template ID
format: int64
in: path
name: id
required: true
type: integer
- description: Update form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperNotificationTemplateUpdateForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SuperNotificationTemplateItem'
summary: Update notification template
tags:
- Notification
/super/v1/orders:
get:
consumes:
@@ -4486,6 +4705,33 @@ paths:
summary: Remove payout account
tags:
- Finance
patch:
consumes:
- application/json
description: Update payout account across tenants
parameters:
- description: Payout account ID
format: int64
in: path
name: id
required: true
type: integer
- description: Update form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperPayoutAccountUpdateForm'
produces:
- application/json
responses:
"200":
description: Updated
schema:
type: string
summary: Update payout account
tags:
- Finance
/super/v1/payout-accounts/{id}/review:
post:
consumes:
@@ -5187,6 +5433,33 @@ paths:
summary: Get user
tags:
- User
patch:
consumes:
- application/json
description: Update user profile fields
parameters:
- description: User ID
format: int64
in: path
name: id
required: true
type: integer
- description: Update form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperUserProfileUpdateForm'
produces:
- application/json
responses:
"200":
description: Updated
schema:
type: string
summary: Update user profile
tags:
- User
/super/v1/users/{id}/coupons:
get:
consumes: