feat: expand superadmin edits and minio docs
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 加入的租户数量。
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
81
backend/config.minio.toml
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
65
docs/storage_minio_smoke_test.md
Normal file
65
docs/storage_minio_smoke_test.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# MinIO Storage Smoke Test
|
||||
|
||||
This note records local MinIO setup for S3-compatible storage simulation.
|
||||
|
||||
## Docker setup (local)
|
||||
|
||||
Image sources used in this environment:
|
||||
- `docker.hub.ipao.vip/minio/minio:latest`
|
||||
- `docker.hub.ipao.vip/minio/mc:latest`
|
||||
|
||||
Start MinIO (console on 9001):
|
||||
|
||||
```bash
|
||||
sudo -n docker run -d --name quyun-minio \
|
||||
-p 9000:9000 -p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=minioadmin \
|
||||
docker.hub.ipao.vip/minio/minio:latest server /data --console-address ":9001"
|
||||
```
|
||||
|
||||
## Bucket init
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/quyun-mc
|
||||
sudo -n docker run --rm --network host -v /tmp/quyun-mc:/root/.mc docker.hub.ipao.vip/minio/mc:latest \
|
||||
alias set local http://127.0.0.1:9000 minioadmin minioadmin
|
||||
sudo -n docker run --rm --network host -v /tmp/quyun-mc:/root/.mc docker.hub.ipao.vip/minio/mc:latest \
|
||||
mb -p local/quyun-assets
|
||||
sudo -n docker run --rm --network host -v /tmp/quyun-mc:/root/.mc docker.hub.ipao.vip/minio/mc:latest \
|
||||
ls local
|
||||
```
|
||||
|
||||
## Config sample
|
||||
|
||||
```toml
|
||||
[Storage]
|
||||
Type = "s3"
|
||||
AccessKey = "minioadmin"
|
||||
SecretKey = "minioadmin"
|
||||
Region = "us-east-1"
|
||||
Bucket = "quyun-assets"
|
||||
Endpoint = "http://127.0.0.1:9000"
|
||||
PathStyle = true
|
||||
```
|
||||
|
||||
Use the bundled config file for local runs:
|
||||
|
||||
```bash
|
||||
ENV_LOCAL=minio make serve
|
||||
```
|
||||
|
||||
## Smoke test (optional)
|
||||
|
||||
```bash
|
||||
sudo -n docker run --rm --network host -v /tmp/quyun-mc:/root/.mc -v "$(pwd)":/work -w /work docker.hub.ipao.vip/minio/mc:latest \
|
||||
cp ./README.md local/quyun-assets/smoke/README.md
|
||||
sudo -n docker run --rm --network host -v /tmp/quyun-mc:/root/.mc docker.hub.ipao.vip/minio/mc:latest \
|
||||
ls local/quyun-assets/smoke
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
sudo -n docker rm -f quyun-minio
|
||||
```
|
||||
@@ -30,3 +30,5 @@ PathStyle = true
|
||||
```
|
||||
|
||||
For AWS S3, set `Endpoint` with `https://` (e.g., `https://s3.amazonaws.com`) and keep `PathStyle` as `false` unless your provider requires path-style access.
|
||||
|
||||
For local MinIO setup and a smoke test, see `docs/storage_minio_smoke_test.md` (includes `ENV_LOCAL=minio`).
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 当前范围与约束
|
||||
- 认证仅使用 JWT(不做 OAuth/Cookie 方案)。
|
||||
- 支付集成暂不做,订单/退款仅按既有数据结构做流程与统计。
|
||||
- 存储仅使用本地 FS 模拟,真实 Provider 延后统一接入。
|
||||
- 存储需要接入本地 MinIO 进行真实 Provider 模拟,保留本地 FS 作为兜底。
|
||||
- 多租户路由强隔离(`/t/:tenantCode/v1` + TenantResolver)已启用,后续仅做细节优化。
|
||||
|
||||
## 统一原则
|
||||
@@ -63,11 +63,63 @@
|
||||
**测试方案**
|
||||
- owner 与非 owner 上传分支。
|
||||
|
||||
### 4) 超管全量可编辑(补齐写操作缺口)(已完成)
|
||||
**需求目标**
|
||||
- 超管可对系统内现有对象执行“修改/新增/删除/审核”等操作,不受租户边界限制。
|
||||
- 补齐已存在能力但缺少 `/super/v1` 写入口的模块。
|
||||
|
||||
**技术方案(后端)**
|
||||
- 租户创建完善
|
||||
- `POST /super/v1/tenants`:补齐 `expired_at`(基于 `duration`)与 `tenant_users` 管理员成员关系写入。
|
||||
- 创作者设置超管入口
|
||||
- 新增 `GET /super/v1/creators/:tenantID<int>/settings`
|
||||
- 新增 `PUT /super/v1/creators/:tenantID<int>/settings`
|
||||
- 复用 `services.Creator.GetSettings/UpdateSettings`,由超管直传 tenantID + operatorID。
|
||||
- 收款账户超管管理
|
||||
- 新增 `POST /super/v1/creators/:tenantID<int>/payout-accounts`
|
||||
- 新增 `PATCH /super/v1/payout-accounts/:id<int>`(编辑/禁用/备注)
|
||||
- 复用现有收款账户结构体与校验逻辑。
|
||||
- 用户资料超管编辑
|
||||
- 新增 `PATCH /super/v1/users/:id<int>`(允许更新昵称/头像/实名标记等基础字段)。
|
||||
|
||||
**测试方案**
|
||||
- CreateTenant 后 `expired_at` 与 `tenant_users` 均落库。
|
||||
- 超管可读取/更新创作者设置,权限校验通过。
|
||||
- 超管可新增/编辑收款账户。
|
||||
- 超管可更新用户资料,字段校验生效。
|
||||
|
||||
### 5) 通知模板支持编辑(已完成)
|
||||
**需求目标**
|
||||
- 支持模板更新、启用/停用,避免误建模板不可修正。
|
||||
|
||||
**技术方案(后端/前端)**
|
||||
- API
|
||||
- 新增 `PATCH /super/v1/notifications/templates/:id<int>`
|
||||
- (可选)新增 `DELETE /super/v1/notifications/templates/:id<int>`
|
||||
- 前端:`Notifications.vue` 增加“编辑模板”弹窗与启用开关。
|
||||
|
||||
**测试方案**
|
||||
- 编辑模板后列表与详情一致。
|
||||
- 禁用模板后不允许用于群发(或提示不可用)。
|
||||
|
||||
### 6) 本地 MinIO 模拟真实存储 Provider(已完成)
|
||||
**需求目标**
|
||||
- 本地环境可用 MinIO 模拟真实 S3 Provider,验证上传/访问链路。
|
||||
|
||||
**技术方案(后端/本地环境)**
|
||||
- Docker:拉起 MinIO(含 access/secret、bucket 初始化)。
|
||||
- `Storage` 配置支持 S3/MinIO 模式(endpoint + path style)。
|
||||
- 更新 `docs/storage_provider.md` 与新增 smoke test 文档。
|
||||
|
||||
**测试方案**
|
||||
- 上传 -> MinIO -> 读取 URL 可访问。
|
||||
- 本地 FS 与 MinIO 配置切换可用。
|
||||
|
||||
---
|
||||
|
||||
## P1(高优先)
|
||||
|
||||
### 4) ID 类型统一(int64 / model 注入)(已完成)
|
||||
### 7) ID 类型统一(int64 / model 注入)(已完成)
|
||||
**需求目标**
|
||||
- 所有业务 ID 使用 `int64`,路由参数统一 `:id<int>`。
|
||||
|
||||
@@ -79,7 +131,7 @@
|
||||
**测试方案**
|
||||
- 关键接口:正常请求 + 参数类型错误时返回明确错误。
|
||||
|
||||
### 5) 内容访问策略完善(资源权限与预览差异化)(已完成)
|
||||
### 8) 内容访问策略完善(资源权限与预览差异化)(已完成)
|
||||
**需求目标**
|
||||
- 媒体资源访问遵循:未购仅预览,已购全量,作者/管理员全量。
|
||||
- 签名 URL 或下载地址生成前进行权限校验。
|
||||
@@ -96,7 +148,7 @@
|
||||
**测试方案**
|
||||
- 未登录/未购/已购/作者/管理员的可见资源集合一致性。
|
||||
|
||||
### 6) 审计参数传递规范化(已完成)
|
||||
### 9) 审计参数传递规范化(已完成)
|
||||
**需求目标**
|
||||
- 审计服务禁止自行读取 `ctx`,改为显式传入操作者信息。
|
||||
|
||||
@@ -111,7 +163,7 @@
|
||||
|
||||
## P2(中优先)
|
||||
|
||||
### 7) 运营统计报表(曝光/转化/订单/退款)(已完成)
|
||||
### 10) 运营统计报表(曝光/转化/订单/退款)(已完成)
|
||||
**需求目标**
|
||||
- 提供租户维度与时间范围的核心指标统计与导出。
|
||||
|
||||
@@ -127,7 +179,7 @@
|
||||
**测试方案**
|
||||
- 统计口径一致性;筛选组合;导出任务可用性。
|
||||
|
||||
### 8) 超管后台治理能力(健康度/异常监控/内容审核)(已完成)
|
||||
### 11) 超管后台治理能力(健康度/异常监控/内容审核)(已完成)
|
||||
**需求目标**
|
||||
- 提供超管对租户的健康指标、异常趋势、内容合规审核。
|
||||
|
||||
@@ -142,7 +194,7 @@
|
||||
**测试方案**
|
||||
- 审核状态流转有效性;异常阈值命中结果。
|
||||
|
||||
### 9) 性能优化(避免 N+1)(已完成)
|
||||
### 12) 性能优化(避免 N+1)(已完成)
|
||||
**需求目标**
|
||||
- 列表/统计场景避免逐条查询。
|
||||
|
||||
@@ -157,17 +209,17 @@
|
||||
|
||||
## P3(延后)
|
||||
|
||||
### 10) 真实存储 Provider 接入
|
||||
### 13) 真实存储 Provider 接入(生产)
|
||||
**需求目标**
|
||||
- 接入 OSS/云存储,统一上传/访问路径策略。
|
||||
- 接入 OSS/云存储(生产环境),统一上传/访问路径策略。
|
||||
|
||||
**技术方案(后端)**
|
||||
- 通过配置注入 Provider,保留本地 FS 作为 dev fallback。
|
||||
- 通过配置注入 Provider,保留本地 FS/MinIO 作为 dev fallback。
|
||||
|
||||
**测试方案**
|
||||
- 本地 FS + 真实 Provider 两套配置可用性。
|
||||
- 本地 FS + MinIO + 真实 Provider 三套配置可用性。
|
||||
|
||||
### 11) 支付集成
|
||||
### 14) 支付集成
|
||||
**需求目标**
|
||||
- 最终阶段对接真实支付。
|
||||
|
||||
@@ -191,6 +243,9 @@
|
||||
- ID 类型已统一为 int64(仅保留 upload_id/external_id/uuid 等非数字标识)。
|
||||
- 内容资源权限与预览差异化(未购预览、已购/管理员/成员全量)。
|
||||
- 审计操作显式传入操作者信息(服务层不再依赖 ctx 读取)。
|
||||
- 超管全量可编辑(租户创建补齐、创作者设置/结算账户/用户资料写入口)。
|
||||
- 通知模板支持编辑(超管接口 + 前端编辑入口)。
|
||||
- 本地 MinIO 模拟真实存储 Provider(本地容器与文档指引)。
|
||||
- 运营统计报表(overview + CSV 导出基础版)。
|
||||
- 超管后台治理能力(健康度/异常监控/内容审核)。
|
||||
- 性能优化(避免 N+1:订单/租户列表批量聚合 + topics 聚合)。
|
||||
|
||||
@@ -39,6 +39,23 @@ export const CreatorService = {
|
||||
items: normalizeItems(data?.items)
|
||||
};
|
||||
},
|
||||
async getCreatorSettings(tenantID) {
|
||||
if (!tenantID) throw new Error('tenantID is required');
|
||||
return requestJson(`/super/v1/creators/${tenantID}/settings`);
|
||||
},
|
||||
async updateCreatorSettings(tenantID, { name, bio, avatar, cover, description } = {}) {
|
||||
if (!tenantID) throw new Error('tenantID is required');
|
||||
return requestJson(`/super/v1/creators/${tenantID}/settings`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
name,
|
||||
bio,
|
||||
avatar,
|
||||
cover,
|
||||
description
|
||||
}
|
||||
});
|
||||
},
|
||||
async listJoinRequests({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, status, created_at_from, created_at_to } = {}) {
|
||||
const iso = (d) => {
|
||||
if (!d) return undefined;
|
||||
@@ -140,6 +157,31 @@ export const CreatorService = {
|
||||
items: normalizeItems(data?.items)
|
||||
};
|
||||
},
|
||||
async createPayoutAccount(tenantID, { user_id, type, name, account, realname } = {}) {
|
||||
if (!tenantID) throw new Error('tenantID is required');
|
||||
return requestJson(`/super/v1/creators/${tenantID}/payout-accounts`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
user_id,
|
||||
type,
|
||||
name,
|
||||
account,
|
||||
realname
|
||||
}
|
||||
});
|
||||
},
|
||||
async updatePayoutAccount(accountID, { type, name, account, realname } = {}) {
|
||||
if (!accountID) throw new Error('accountID is required');
|
||||
return requestJson(`/super/v1/payout-accounts/${accountID}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
type,
|
||||
name,
|
||||
account,
|
||||
realname
|
||||
}
|
||||
});
|
||||
},
|
||||
async removePayoutAccount(accountID) {
|
||||
if (!accountID) throw new Error('accountID is required');
|
||||
return requestJson(`/super/v1/payout-accounts/${accountID}`, { method: 'DELETE' });
|
||||
|
||||
@@ -98,5 +98,19 @@ export const NotificationService = {
|
||||
is_active
|
||||
}
|
||||
});
|
||||
},
|
||||
async updateTemplate({ id, tenant_id, name, type, title, content, is_active } = {}) {
|
||||
if (!id) throw new Error('template id is required');
|
||||
return requestJson(`/super/v1/notifications/templates/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
title,
|
||||
content,
|
||||
is_active
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,6 +68,20 @@ export const UserService = {
|
||||
if (!Array.isArray(roles) || roles.length === 0) throw new Error('roles is required');
|
||||
return requestJson(`/super/v1/users/${userID}/roles`, { method: 'PATCH', body: { roles } });
|
||||
},
|
||||
async updateUserProfile({ userID, nickname, avatar, gender, bio, is_real_name_verified, real_name, id_card } = {}) {
|
||||
if (!userID) throw new Error('userID is required');
|
||||
|
||||
const body = {};
|
||||
if (nickname !== undefined) body.nickname = nickname;
|
||||
if (avatar !== undefined) body.avatar = avatar;
|
||||
if (gender !== undefined) body.gender = gender;
|
||||
if (bio !== undefined) body.bio = bio;
|
||||
if (is_real_name_verified !== undefined) body.is_real_name_verified = is_real_name_verified;
|
||||
if (real_name !== undefined) body.real_name = real_name;
|
||||
if (id_card !== undefined) body.id_card = id_card;
|
||||
|
||||
return requestJson(`/super/v1/users/${userID}`, { method: 'PATCH', body });
|
||||
},
|
||||
async getUserStatistics() {
|
||||
try {
|
||||
const data = await requestJson('/super/v1/users/statistics');
|
||||
|
||||
@@ -34,6 +34,16 @@ const statusUpdating = ref(false);
|
||||
const statusTenant = ref(null);
|
||||
const statusValue = ref(null);
|
||||
|
||||
const settingsDialogVisible = ref(false);
|
||||
const settingsLoading = ref(false);
|
||||
const settingsSubmitting = ref(false);
|
||||
const settingsTenant = ref(null);
|
||||
const settingsName = ref('');
|
||||
const settingsBio = ref('');
|
||||
const settingsAvatar = ref('');
|
||||
const settingsCover = ref('');
|
||||
const settingsDescription = ref('');
|
||||
|
||||
const applicationStatusOptions = computed(() => [{ label: '全部', value: '' }, ...(statusOptions.value || [])]);
|
||||
|
||||
const joinRequests = ref([]);
|
||||
@@ -92,6 +102,11 @@ const payoutStatusOptions = [
|
||||
{ label: '已驳回', value: 'rejected' }
|
||||
];
|
||||
|
||||
const payoutTypeOptions = [
|
||||
{ label: 'bank', value: 'bank' },
|
||||
{ label: 'alipay', value: 'alipay' }
|
||||
];
|
||||
|
||||
const payoutReviewOptions = [
|
||||
{ label: '通过', value: 'approve' },
|
||||
{ label: '驳回', value: 'reject' }
|
||||
@@ -119,6 +134,17 @@ const payoutReviewAction = ref('approve');
|
||||
const payoutReviewReason = ref('');
|
||||
const payoutReviewTarget = ref(null);
|
||||
|
||||
const payoutDialogVisible = ref(false);
|
||||
const payoutDialogMode = ref('create');
|
||||
const payoutDialogSubmitting = ref(false);
|
||||
const payoutDialogEditingID = ref(null);
|
||||
const payoutFormTenantID = ref(null);
|
||||
const payoutFormUserID = ref(null);
|
||||
const payoutFormType = ref('bank');
|
||||
const payoutFormName = ref('');
|
||||
const payoutFormAccount = ref('');
|
||||
const payoutFormRealname = ref('');
|
||||
|
||||
const inviteDialogVisible = ref(false);
|
||||
const inviteSubmitting = ref(false);
|
||||
const inviteTenantID = ref(null);
|
||||
@@ -420,6 +446,54 @@ async function confirmUpdateStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openSettingsDialog(row) {
|
||||
const tenantIDValue = Number(row?.id ?? row?.tenant_id ?? 0);
|
||||
if (!tenantIDValue) return;
|
||||
settingsTenant.value = row;
|
||||
settingsDialogVisible.value = true;
|
||||
settingsLoading.value = true;
|
||||
try {
|
||||
const data = await CreatorService.getCreatorSettings(tenantIDValue);
|
||||
settingsName.value = data?.name ?? '';
|
||||
settingsBio.value = data?.bio ?? '';
|
||||
settingsAvatar.value = data?.avatar ?? '';
|
||||
settingsCover.value = data?.cover ?? '';
|
||||
settingsDescription.value = data?.description ?? '';
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载创作者设置', life: 4000 });
|
||||
} finally {
|
||||
settingsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmUpdateSettings() {
|
||||
const tenantIDValue = Number(settingsTenant.value?.id ?? settingsTenant.value?.tenant_id ?? 0);
|
||||
if (!tenantIDValue) return;
|
||||
const name = settingsName.value.trim();
|
||||
if (!name) {
|
||||
toast.add({ severity: 'warn', summary: '提示', detail: '名称不能为空', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
settingsSubmitting.value = true;
|
||||
try {
|
||||
await CreatorService.updateCreatorSettings(tenantIDValue, {
|
||||
name,
|
||||
bio: settingsBio.value,
|
||||
avatar: settingsAvatar.value,
|
||||
cover: settingsCover.value,
|
||||
description: settingsDescription.value
|
||||
});
|
||||
toast.add({ severity: 'success', summary: '更新成功', detail: `TenantID: ${tenantIDValue}`, life: 3000 });
|
||||
settingsDialogVisible.value = false;
|
||||
await loadCreators();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '更新失败', detail: error?.message || '无法更新创作者设置', life: 4000 });
|
||||
} finally {
|
||||
settingsSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openReviewDialog(row, action) {
|
||||
reviewTarget.value = row;
|
||||
reviewAction.value = action || 'approve';
|
||||
@@ -529,6 +603,77 @@ async function confirmPayoutReview() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPayoutDialog(mode, row) {
|
||||
payoutDialogMode.value = mode;
|
||||
if (mode === 'edit' && row) {
|
||||
payoutDialogEditingID.value = row.id;
|
||||
payoutFormTenantID.value = row.tenant_id ?? null;
|
||||
payoutFormUserID.value = row.user_id ?? null;
|
||||
payoutFormType.value = row.type || 'bank';
|
||||
payoutFormName.value = row.name || '';
|
||||
payoutFormAccount.value = row.account || '';
|
||||
payoutFormRealname.value = row.realname || '';
|
||||
} else {
|
||||
payoutDialogEditingID.value = null;
|
||||
payoutFormTenantID.value = null;
|
||||
payoutFormUserID.value = null;
|
||||
payoutFormType.value = 'bank';
|
||||
payoutFormName.value = '';
|
||||
payoutFormAccount.value = '';
|
||||
payoutFormRealname.value = '';
|
||||
}
|
||||
payoutDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmPayoutDialog() {
|
||||
const tenantIDValue = Number(payoutFormTenantID.value ?? 0);
|
||||
const userIDValue = Number(payoutFormUserID.value ?? 0);
|
||||
const name = payoutFormName.value.trim();
|
||||
const account = payoutFormAccount.value.trim();
|
||||
const realname = payoutFormRealname.value.trim();
|
||||
|
||||
if (!name || !account || !realname) {
|
||||
toast.add({ severity: 'warn', summary: '提示', detail: '请完善账户信息', life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (payoutDialogMode.value === 'create' && (!tenantIDValue || !userIDValue)) {
|
||||
toast.add({ severity: 'warn', summary: '提示', detail: 'TenantID 与 UserID 不能为空', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
payoutDialogSubmitting.value = true;
|
||||
try {
|
||||
if (payoutDialogMode.value === 'edit') {
|
||||
const accountID = payoutDialogEditingID.value;
|
||||
if (!accountID) return;
|
||||
await CreatorService.updatePayoutAccount(accountID, {
|
||||
type: payoutFormType.value,
|
||||
name,
|
||||
account,
|
||||
realname
|
||||
});
|
||||
toast.add({ severity: 'success', summary: '更新成功', detail: `账户ID: ${accountID}`, life: 3000 });
|
||||
} else {
|
||||
await CreatorService.createPayoutAccount(tenantIDValue, {
|
||||
user_id: userIDValue,
|
||||
type: payoutFormType.value,
|
||||
name,
|
||||
account,
|
||||
realname
|
||||
});
|
||||
toast.add({ severity: 'success', summary: '已创建', detail: '结算账户已创建', life: 3000 });
|
||||
}
|
||||
payoutDialogVisible.value = false;
|
||||
await loadPayoutAccounts();
|
||||
} catch (error) {
|
||||
const summary = payoutDialogMode.value === 'edit' ? '更新失败' : '创建失败';
|
||||
const detail = payoutDialogMode.value === 'edit' ? '无法更新结算账户' : '无法创建结算账户';
|
||||
toast.add({ severity: 'error', summary, detail: error?.message || detail, life: 4000 });
|
||||
} finally {
|
||||
payoutDialogSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openInviteDialog(row) {
|
||||
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
|
||||
inviteMaxUses.value = 1;
|
||||
@@ -663,6 +808,11 @@ onMounted(() => {
|
||||
{{ formatDate(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<Button label="设置" icon="pi pi-cog" text size="small" class="p-0" @click="openSettingsDialog(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
<TabPanel value="applications">
|
||||
@@ -863,6 +1013,7 @@ onMounted(() => {
|
||||
<h4 class="m-0">结算账户</h4>
|
||||
<span class="text-muted-color">跨租户结算账户审查</span>
|
||||
</div>
|
||||
<Button label="新增账户" icon="pi pi-plus" @click="openPayoutDialog('create')" />
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="payoutAccountsLoading" @search="onPayoutSearch" @reset="onPayoutReset">
|
||||
@@ -956,6 +1107,7 @@ onMounted(() => {
|
||||
<Column header="操作" style="min-width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<Button label="编辑" icon="pi pi-pencil" text size="small" class="p-0 justify-start" @click="openPayoutDialog('edit', data)" />
|
||||
<Button v-if="data?.status === 'pending'" label="通过" icon="pi pi-check" text size="small" class="p-0 justify-start" @click="openPayoutReviewDialog(data, 'approve')" />
|
||||
<Button v-if="data?.status === 'pending'" label="驳回" icon="pi pi-times" severity="danger" text size="small" class="p-0 justify-start" @click="openPayoutReviewDialog(data, 'reject')" />
|
||||
<Button label="删除" icon="pi pi-trash" severity="danger" text size="small" class="p-0 justify-start" @click="openPayoutRemoveDialog(data)" />
|
||||
@@ -968,6 +1120,44 @@ onMounted(() => {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="settingsDialogVisible" :modal="true" :style="{ width: '620px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">创作者设置</span>
|
||||
<span class="text-muted-color truncate max-w-[240px]">{{ settingsTenant?.name ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="settingsLoading" class="flex items-center justify-center py-10">
|
||||
<ProgressSpinner style="width: 36px; height: 36px" strokeWidth="6" />
|
||||
</div>
|
||||
<div v-else class="grid gap-4">
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">名称</label>
|
||||
<InputText v-model="settingsName" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">头像 URL</label>
|
||||
<InputText v-model="settingsAvatar" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">封面 URL</label>
|
||||
<InputText v-model="settingsCover" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">简介</label>
|
||||
<Textarea v-model="settingsBio" rows="2" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">描述</label>
|
||||
<Textarea v-model="settingsDescription" rows="3" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="settingsDialogVisible = false" :disabled="settingsSubmitting" />
|
||||
<Button label="确认" icon="pi pi-check" @click="confirmUpdateSettings" :loading="settingsSubmitting" :disabled="settingsSubmitting || settingsLoading" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="statusDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -1060,6 +1250,48 @@ onMounted(() => {
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="payoutDialogVisible" :modal="true" :style="{ width: '560px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{{ payoutDialogMode === 'edit' ? '编辑结算账户' : '新增结算账户' }}</span>
|
||||
<span v-if="payoutDialogMode === 'edit'" class="text-muted-color">账户ID: {{ payoutDialogEditingID ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">TenantID</label>
|
||||
<InputNumber v-model="payoutFormTenantID" :min="1" placeholder="必填" class="w-full" :disabled="payoutDialogMode === 'edit'" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">UserID</label>
|
||||
<InputNumber v-model="payoutFormUserID" :min="1" placeholder="必填" class="w-full" :disabled="payoutDialogMode === 'edit'" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">类型</label>
|
||||
<Select v-model="payoutFormType" :options="payoutTypeOptions" optionLabel="label" optionValue="value" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">账户名称</label>
|
||||
<InputText v-model="payoutFormName" placeholder="开户行/账户名称" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">账号</label>
|
||||
<InputText v-model="payoutFormAccount" placeholder="收款账号" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">收款人</label>
|
||||
<InputText v-model="payoutFormRealname" placeholder="收款人姓名" class="w-full" />
|
||||
</div>
|
||||
<div v-if="payoutDialogMode === 'edit'" class="text-xs text-muted-color">提示:更新已审核账户将重置为待审核状态。</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="payoutDialogVisible = false" :disabled="payoutDialogSubmitting" />
|
||||
<Button label="确认" icon="pi pi-check" @click="confirmPayoutDialog" :loading="payoutDialogSubmitting" :disabled="payoutDialogSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="payoutRemoveDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -69,6 +69,8 @@ const templateCreatedAtFrom = ref(null);
|
||||
const templateCreatedAtTo = ref(null);
|
||||
|
||||
const templateDialogVisible = ref(false);
|
||||
const templateDialogMode = ref('create');
|
||||
const templateEditingID = ref(null);
|
||||
const templateSubmitting = ref(false);
|
||||
const createTenantID = ref(null);
|
||||
const createName = ref('');
|
||||
@@ -278,6 +280,8 @@ function onTemplateSort(event) {
|
||||
}
|
||||
|
||||
function openTemplateDialog() {
|
||||
templateDialogMode.value = 'create';
|
||||
templateEditingID.value = null;
|
||||
createTenantID.value = null;
|
||||
createName.value = '';
|
||||
createType.value = 'system';
|
||||
@@ -287,7 +291,20 @@ function openTemplateDialog() {
|
||||
templateDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmCreateTemplate() {
|
||||
function openTemplateEditDialog(row) {
|
||||
if (!row) return;
|
||||
templateDialogMode.value = 'edit';
|
||||
templateEditingID.value = row.id;
|
||||
createTenantID.value = row.tenant_id > 0 ? row.tenant_id : null;
|
||||
createName.value = row.name || '';
|
||||
createType.value = row.type || 'system';
|
||||
createTitle.value = row.title || '';
|
||||
createContent.value = row.content || '';
|
||||
createIsActive.value = row.is_active ?? true;
|
||||
templateDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmTemplate() {
|
||||
const name = createName.value.trim();
|
||||
const title = createTitle.value.trim();
|
||||
const content = createContent.value.trim();
|
||||
@@ -295,19 +312,29 @@ async function confirmCreateTemplate() {
|
||||
|
||||
templateSubmitting.value = true;
|
||||
try {
|
||||
await NotificationService.createTemplate({
|
||||
const payload = {
|
||||
tenant_id: createTenantID.value || undefined,
|
||||
name,
|
||||
type: createType.value,
|
||||
title,
|
||||
content,
|
||||
is_active: createIsActive.value
|
||||
});
|
||||
toast.add({ severity: 'success', summary: '已创建', detail: '模板已创建', life: 3000 });
|
||||
};
|
||||
if (templateDialogMode.value === 'edit') {
|
||||
const templateID = templateEditingID.value;
|
||||
if (!templateID) return;
|
||||
await NotificationService.updateTemplate({ id: templateID, ...payload });
|
||||
toast.add({ severity: 'success', summary: '已更新', detail: `模板ID: ${templateID}`, life: 3000 });
|
||||
} else {
|
||||
await NotificationService.createTemplate(payload);
|
||||
toast.add({ severity: 'success', summary: '已创建', detail: '模板已创建', life: 3000 });
|
||||
}
|
||||
templateDialogVisible.value = false;
|
||||
loadTemplates();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '创建失败', detail: error?.message || '无法创建模板', life: 4000 });
|
||||
const label = templateDialogMode.value === 'edit' ? '更新失败' : '创建失败';
|
||||
const detail = templateDialogMode.value === 'edit' ? '无法更新模板' : '无法创建模板';
|
||||
toast.add({ severity: 'error', summary: label, detail: error?.message || detail, life: 4000 });
|
||||
} finally {
|
||||
templateSubmitting.value = false;
|
||||
}
|
||||
@@ -483,6 +510,11 @@ loadTemplates();
|
||||
{{ formatDate(data.updated_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="min-width: 8rem">
|
||||
<template #body="{ data }">
|
||||
<Button label="编辑" icon="pi pi-pencil" text size="small" class="p-0" @click="openTemplateEditDialog(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</TabPanel>
|
||||
@@ -526,7 +558,7 @@ loadTemplates();
|
||||
|
||||
<Dialog v-model:visible="templateDialogVisible" modal :style="{ width: '560px' }">
|
||||
<template #header>
|
||||
<span class="font-medium">创建模板</span>
|
||||
<span class="font-medium">{{ templateDialogMode === 'edit' ? '编辑模板' : '创建模板' }}</span>
|
||||
</template>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -558,7 +590,13 @@ loadTemplates();
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="templateDialogVisible = false" />
|
||||
<Button label="创建" icon="pi pi-check" :loading="templateSubmitting" :disabled="templateSubmitting || !createName.trim() || !createTitle.trim() || !createContent.trim()" @click="confirmCreateTemplate" />
|
||||
<Button
|
||||
:label="templateDialogMode === 'edit' ? '更新' : '创建'"
|
||||
icon="pi pi-check"
|
||||
:loading="templateSubmitting"
|
||||
:disabled="templateSubmitting || !createName.trim() || !createTitle.trim() || !createContent.trim()"
|
||||
@click="confirmTemplate"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +83,12 @@ const orderStatusOptions = [
|
||||
{ label: 'failed', value: 'failed' }
|
||||
];
|
||||
|
||||
const genderOptions = [
|
||||
{ label: '男', value: 'male' },
|
||||
{ label: '女', value: 'female' },
|
||||
{ label: '保密', value: 'secret' }
|
||||
];
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-';
|
||||
if (String(value).startsWith('0001-01-01')) return '-';
|
||||
@@ -116,6 +122,19 @@ function formatOrderType(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatGender(value) {
|
||||
switch (value) {
|
||||
case 'male':
|
||||
return '男';
|
||||
case 'female':
|
||||
return '女';
|
||||
case 'secret':
|
||||
return '保密';
|
||||
default:
|
||||
return value || '-';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusSeverity(status) {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
@@ -387,6 +406,16 @@ async function loadRechargeOrders() {
|
||||
}
|
||||
}
|
||||
|
||||
const profileDialogVisible = ref(false);
|
||||
const profileSubmitting = ref(false);
|
||||
const profileNickname = ref('');
|
||||
const profileAvatar = ref('');
|
||||
const profileGender = ref('secret');
|
||||
const profileBio = ref('');
|
||||
const profileIsVerified = ref(false);
|
||||
const profileRealName = ref('');
|
||||
const profileIDCard = ref('');
|
||||
|
||||
const statusDialogVisible = ref(false);
|
||||
const statusLoading = ref(false);
|
||||
const statusOptionsLoading = ref(false);
|
||||
@@ -457,6 +486,53 @@ async function confirmUpdateRoles() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openProfileDialog() {
|
||||
profileNickname.value = user.value?.nickname ?? '';
|
||||
profileAvatar.value = user.value?.avatar ?? '';
|
||||
profileGender.value = user.value?.gender || 'secret';
|
||||
profileBio.value = user.value?.bio ?? '';
|
||||
profileIsVerified.value = !!user.value?.is_real_name_verified;
|
||||
|
||||
if (!realName.value && !realNameLoading.value) {
|
||||
await loadRealName();
|
||||
}
|
||||
profileRealName.value = realName.value?.real_name ?? '';
|
||||
profileIDCard.value = '';
|
||||
profileDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmUpdateProfile() {
|
||||
const id = userID.value;
|
||||
if (!id) return;
|
||||
|
||||
const payload = {
|
||||
userID: id,
|
||||
nickname: profileNickname.value?.trim() ?? '',
|
||||
avatar: profileAvatar.value?.trim() ?? '',
|
||||
gender: profileGender.value,
|
||||
bio: profileBio.value?.trim() ?? '',
|
||||
is_real_name_verified: profileIsVerified.value
|
||||
};
|
||||
|
||||
const realNameValue = profileRealName.value.trim();
|
||||
if (realNameValue) payload.real_name = realNameValue;
|
||||
const idCardValue = profileIDCard.value.trim();
|
||||
if (idCardValue) payload.id_card = idCardValue;
|
||||
|
||||
profileSubmitting.value = true;
|
||||
try {
|
||||
await UserService.updateUserProfile(payload);
|
||||
toast.add({ severity: 'success', summary: '更新成功', detail: `用户ID: ${id}`, life: 3000 });
|
||||
profileDialogVisible.value = false;
|
||||
await loadUser();
|
||||
await loadRealName();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '更新失败', detail: error?.message || '无法更新用户资料', life: 4000 });
|
||||
} finally {
|
||||
profileSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const ownedTenantsLoading = ref(false);
|
||||
const ownedTenants = ref([]);
|
||||
const ownedTenantsTotal = ref(0);
|
||||
@@ -748,6 +824,7 @@ onMounted(() => {
|
||||
<span class="text-muted-color">UserID: {{ userID || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button label="资料" icon="pi pi-user-edit" severity="secondary" @click="openProfileDialog" :disabled="loading" />
|
||||
<Button label="角色" icon="pi pi-user-edit" severity="secondary" @click="openRolesDialog" :disabled="loading" />
|
||||
<Button label="状态" icon="pi pi-tag" @click="openStatusDialog" :disabled="loading" />
|
||||
</div>
|
||||
@@ -766,6 +843,22 @@ onMounted(() => {
|
||||
<div class="text-sm text-muted-color">状态</div>
|
||||
<Tag :value="user?.status_description || user?.status || '-'" :severity="getStatusSeverity(user?.status)" />
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="text-sm text-muted-color">昵称</div>
|
||||
<div class="font-medium">{{ user?.nickname || '-' }}</div>
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="text-sm text-muted-color">性别</div>
|
||||
<div class="font-medium">{{ formatGender(user?.gender) }}</div>
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="text-sm text-muted-color">头像</div>
|
||||
<div class="font-medium break-all">{{ user?.avatar || '-' }}</div>
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<div class="text-sm text-muted-color">简介</div>
|
||||
<div class="font-medium">{{ user?.bio || '-' }}</div>
|
||||
</div>
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<div class="text-sm text-muted-color">余额</div>
|
||||
<div class="font-medium">{{ formatCny(user?.balance) }}</div>
|
||||
@@ -1581,6 +1674,54 @@ onMounted(() => {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="profileDialogVisible" :modal="true" :style="{ width: '560px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">编辑用户资料</span>
|
||||
<span class="text-muted-color truncate max-w-[240px]">{{ user?.username ?? '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">昵称</label>
|
||||
<InputText v-model="profileNickname" placeholder="可为空" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">性别</label>
|
||||
<Select v-model="profileGender" :options="genderOptions" optionLabel="label" optionValue="value" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">头像 URL</label>
|
||||
<InputText v-model="profileAvatar" placeholder="可为空" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">个人简介</label>
|
||||
<Textarea v-model="profileBio" rows="3" placeholder="可为空" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Checkbox v-model="profileIsVerified" binary inputId="profileRealNameVerified" />
|
||||
<label for="profileRealNameVerified">已实名认证</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">真实姓名</label>
|
||||
<InputText v-model="profileRealName" placeholder="留空则不修改" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-muted-color mb-2 block">身份证号</label>
|
||||
<InputText v-model="profileIDCard" placeholder="留空则不修改" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-color">提示:更新实名认证信息将同步写入用户元数据。</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="profileDialogVisible = false" :disabled="profileSubmitting" />
|
||||
<Button label="确认" icon="pi pi-check" @click="confirmUpdateProfile" :loading="profileSubmitting" :disabled="profileSubmitting" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="statusDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
76
implementation-plan.md
Normal file
76
implementation-plan.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# QuyUn v2 Implementation Plan
|
||||
|
||||
根据当前项目状态 review,以下是针对后续开发的实施计划,涵盖清理废弃功能与补全缺失业务模块。
|
||||
|
||||
## 1. 功能清理 (Cleanup: WeChat Removal)
|
||||
|
||||
微信生态相关功能确认不再需要,需从代码库和文档中彻底移除,以降低维护成本和避免混淆。
|
||||
|
||||
### 1.1 API 接口 (Backend)
|
||||
- [ ] 移除 `backend/app/http/v1/auth` 下微信授权登录相关路由 (`/auth/wechat`, `/auth/wechat/callback`)。
|
||||
- [ ] 移除 WeChat JS-SDK 签名接口及相关 Service 逻辑。
|
||||
- [ ] 清理 `go.mod` 中可能存在的无用微信 SDK 依赖。
|
||||
|
||||
### 1.2 前端代码 (Frontend)
|
||||
- [ ] **Portal**: 修改 `frontend/portal/src/views/auth/LoginView.vue`,移除微信登录图标及相关 UI 占位。
|
||||
- [ ] **Portal**: 全局搜索并清理残留的 `wx.config`、`wx.ready` 及微信分享相关代码。
|
||||
|
||||
### 1.3 文档与配置
|
||||
- [ ] 更新 `specs/PRD.md` 和 `API.md`,删除关于微信登录、分享及支付的所有规格说明。
|
||||
- [ ] 更新 `backend/config.toml` (及模板),移除 `[wechat]` 相关配置项 (AppID, Secret 等)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 缺失功能需求 (Missing Features)
|
||||
|
||||
以下功能在后端 API 已有部分支持,但前端缺失页面,或业务逻辑需适配生产环境(特别是 S3)。
|
||||
|
||||
### 2.1 创作者中心 - 团队成员管理 (Creator Member Management)
|
||||
**优先级**: High (P0)
|
||||
**现状**: 后端已提供 `CreateInvite`, `ReviewMember`, `CreateMemberInvite` 等 API,但前端 `frontend/portal/src/views/creator/` 目录下完全缺失对应 UI。
|
||||
|
||||
**需求描述**:
|
||||
1. **成员列表页 (Member List)**
|
||||
- 展示当前租户下的所有成员。
|
||||
- 显示成员基本信息(头像、昵称、加入时间)及角色(Owner/Admin/Member)。
|
||||
- 提供移除成员的操作入口(仅 Admin/Owner 可见)。
|
||||
|
||||
2. **邀请功能 (Invite System)**
|
||||
- **生成邀请**: 提供表单生成邀请链接或邀请码(设置有效期、最大使用次数)。
|
||||
- **邀请记录**: 展示当前有效的邀请列表,支持“撤销/禁用”邀请。
|
||||
|
||||
3. **审核功能 (Join Requests)**
|
||||
- **待审核列表**: 展示用户主动发起的加入申请 (Reason, User Info)。
|
||||
- **审批操作**: 提供“通过”和“拒绝”按钮,调用后端 `ReviewMember` 接口。
|
||||
|
||||
### 2.2 媒体处理管线适配 (Media Pipeline for S3)
|
||||
**优先级**: High (P1)
|
||||
**现状**: `backend/app/jobs/media_process_job.go` 目前逻辑强依赖本地文件系统 (`Local Storage`),无法处理 S3 上的文件。
|
||||
|
||||
**需求描述**:
|
||||
1. **处理流程重构**:
|
||||
- 兼容 S3 存储模式:Worker 需先将源文件从 S3 下载到本地临时目录。
|
||||
- 执行 FFmpeg 处理(视频转码、截取封面、音频波形提取)。
|
||||
- 将处理后的产物(封面图、预览片段)上传回 S3。
|
||||
- 清理本地临时文件。
|
||||
2. **封面图自动生成**:
|
||||
- 视频上传完成后,必须自动截取第一帧或指定帧作为封面 (`cover`),避免前端展示空白。
|
||||
|
||||
### 2.3 租户公开页完善 (Tenant Public Page)
|
||||
**优先级**: Medium (P2)
|
||||
**现状**: `frontend/portal/src/views/tenant/` 目前仅有基础 `HomeView`。
|
||||
|
||||
**需求描述**:
|
||||
1. **关于/简介页**:
|
||||
- 增加展示租户详细介绍 (`Description`, `Bio`) 的区域或模态框。
|
||||
2. **内容聚合优化**:
|
||||
- 确认 `HomeView` 支持按“专辑/Topic”或“最新/最热”维度筛选展示内容。
|
||||
|
||||
---
|
||||
|
||||
## 3. 执行路线图 (Roadmap)
|
||||
|
||||
1. **Phase 1 (Cleanup)**: 优先执行 [1. 功能清理],确保代码库整洁,去除干扰项。
|
||||
2. **Phase 2 (Frontend)**: 开发 Creator Portal 的 [2.1 团队成员管理] 模块,补全多租户协作能力。
|
||||
3. **Phase 3 (Backend)**: 配合 S3 调试进度,重构 [2.2 媒体处理管线],确保线上媒体资源可用。
|
||||
4. **Phase 4 (Polish)**: 完善租户公开页细节及最终文档更新。
|
||||
Reference in New Issue
Block a user