diff --git a/backend/app/http/super/v1/coupons.go b/backend/app/http/super/v1/coupons.go index 6670fe3..6779f65 100644 --- a/backend/app/http/super/v1/coupons.go +++ b/backend/app/http/super/v1/coupons.go @@ -29,6 +29,40 @@ func (c *coupons) List(ctx fiber.Ctx, filter *dto.SuperCouponListFilter) (*reque return services.Super.ListCoupons(ctx, filter) } +// List coupon grants +// +// @Router /super/v1/coupon-grants [get] +// @Summary List coupon grants +// @Description List coupon grant records across tenants +// @Tags Coupon +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param limit query int false "Page size" +// @Success 200 {object} requests.Pager{items=[]dto.SuperCouponGrantItem} +// @Bind filter query +func (c *coupons) ListGrants(ctx fiber.Ctx, filter *dto.SuperCouponGrantListFilter) (*requests.Pager, error) { + return services.Super.ListCouponGrants(ctx, filter) +} + +// Update coupon status +// +// @Router /super/v1/coupons/:id/status [patch] +// @Summary Update coupon status +// @Description Update coupon status across tenants +// @Tags Coupon +// @Accept json +// @Produce json +// @Param id path int64 true "Coupon ID" +// @Param form body dto.SuperCouponStatusUpdateForm true "Update form" +// @Success 200 {string} string "Updated" +// @Bind user local key(__ctx_user) +// @Bind id path +// @Bind form body +func (c *coupons) UpdateStatus(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperCouponStatusUpdateForm) error { + return services.Super.UpdateCouponStatus(ctx, user.ID, id, form) +} + // Create coupon // // @Router /super/v1/tenants/:tenantID/coupons [post] diff --git a/backend/app/http/super/v1/creator_applications.go b/backend/app/http/super/v1/creator_applications.go new file mode 100644 index 0000000..bbafbb7 --- /dev/null +++ b/backend/app/http/super/v1/creator_applications.go @@ -0,0 +1,47 @@ +package v1 + +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" +) + +// @provider +type creatorApplications struct{} + +// List creator applications +// +// @Router /super/v1/creator-applications [get] +// @Summary List creator applications +// @Description List creator applications across tenants +// @Tags Creator +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param limit query int false "Page size" +// @Success 200 {object} requests.Pager{items=[]dto.TenantItem} +// @Bind filter query +func (c *creatorApplications) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) { + return services.Super.ListCreatorApplications(ctx, filter) +} + +// Review creator application +// +// @Router /super/v1/creator-applications/:id/review [post] +// @Summary Review creator application +// @Description Approve or reject creator application +// @Tags Creator +// @Accept json +// @Produce json +// @Param id path int64 true "Tenant ID" +// @Param form body dto.SuperCreatorApplicationReviewForm true "Review form" +// @Success 200 {string} string "Reviewed" +// @Bind user local key(__ctx_user) +// @Bind id path +// @Bind form body +func (c *creatorApplications) Review(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperCreatorApplicationReviewForm) error { + return services.Super.ReviewCreatorApplication(ctx, user.ID, id, form) +} diff --git a/backend/app/http/super/v1/dto/super_coupon.go b/backend/app/http/super/v1/dto/super_coupon.go index d00801b..6d324da 100644 --- a/backend/app/http/super/v1/dto/super_coupon.go +++ b/backend/app/http/super/v1/dto/super_coupon.go @@ -79,3 +79,66 @@ type SuperCouponGrantResponse struct { // Granted 实际发放数量。 Granted int `json:"granted"` } + +// SuperCouponStatusUpdateForm 超管优惠券状态更新表单。 +type SuperCouponStatusUpdateForm struct { + // Status 目标状态(frozen)。 + Status string `json:"status" validate:"required,oneof=frozen"` +} + +// SuperCouponGrantListFilter 超管优惠券发放记录过滤条件。 +type SuperCouponGrantListFilter struct { + requests.Pagination + // CouponID 优惠券ID过滤(精确匹配)。 + CouponID *int64 `query:"coupon_id"` + // TenantID 租户ID过滤(精确匹配)。 + TenantID *int64 `query:"tenant_id"` + // TenantCode 租户编码过滤(模糊匹配)。 + TenantCode *string `query:"tenant_code"` + // TenantName 租户名称过滤(模糊匹配)。 + TenantName *string `query:"tenant_name"` + // UserID 用户ID过滤(精确匹配)。 + UserID *int64 `query:"user_id"` + // Username 用户名过滤(模糊匹配)。 + Username *string `query:"username"` + // Status 用户券状态过滤(unused/used/expired)。 + Status *consts.UserCouponStatus `query:"status"` + // CreatedAtFrom 领取时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 领取时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` + // UsedAtFrom 使用时间起始(RFC3339)。 + UsedAtFrom *string `query:"used_at_from"` + // UsedAtTo 使用时间结束(RFC3339)。 + UsedAtTo *string `query:"used_at_to"` +} + +// SuperCouponGrantItem 超管优惠券发放记录项。 +type SuperCouponGrantItem struct { + // ID 用户券ID。 + ID int64 `json:"id"` + // CouponID 优惠券ID。 + CouponID int64 `json:"coupon_id"` + // CouponTitle 优惠券标题。 + CouponTitle string `json:"coupon_title"` + // TenantID 租户ID。 + TenantID int64 `json:"tenant_id"` + // TenantCode 租户编码。 + TenantCode string `json:"tenant_code"` + // TenantName 租户名称。 + TenantName string `json:"tenant_name"` + // UserID 用户ID。 + UserID int64 `json:"user_id"` + // Username 用户名。 + Username string `json:"username"` + // Status 用户券状态。 + Status consts.UserCouponStatus `json:"status"` + // StatusDescription 状态描述(用于展示)。 + StatusDescription string `json:"status_description"` + // OrderID 使用订单ID。 + OrderID int64 `json:"order_id"` + // UsedAt 使用时间(RFC3339)。 + UsedAt string `json:"used_at"` + // CreatedAt 领取时间(RFC3339)。 + CreatedAt string `json:"created_at"` +} diff --git a/backend/app/http/super/v1/dto/super_creator.go b/backend/app/http/super/v1/dto/super_creator.go new file mode 100644 index 0000000..88d0e32 --- /dev/null +++ b/backend/app/http/super/v1/dto/super_creator.go @@ -0,0 +1,9 @@ +package dto + +// SuperCreatorApplicationReviewForm 超管创作者申请审核表单。 +type SuperCreatorApplicationReviewForm struct { + // Action 审核动作(approve/reject)。 + Action string `json:"action" validate:"required,oneof=approve reject"` + // Reason 审核说明(可选,驳回时填写)。 + Reason string `json:"reason"` +} diff --git a/backend/app/http/super/v1/dto/super_payout.go b/backend/app/http/super/v1/dto/super_payout.go new file mode 100644 index 0000000..e66dd65 --- /dev/null +++ b/backend/app/http/super/v1/dto/super_payout.go @@ -0,0 +1,52 @@ +package dto + +import "quyun/v2/app/requests" + +// SuperPayoutAccountListFilter 超管结算账户列表过滤条件。 +type SuperPayoutAccountListFilter struct { + requests.Pagination + // TenantID 租户ID过滤(精确匹配)。 + TenantID *int64 `query:"tenant_id"` + // TenantCode 租户编码过滤(模糊匹配)。 + TenantCode *string `query:"tenant_code"` + // TenantName 租户名称过滤(模糊匹配)。 + TenantName *string `query:"tenant_name"` + // UserID 用户ID过滤(精确匹配)。 + UserID *int64 `query:"user_id"` + // Username 用户名过滤(模糊匹配)。 + Username *string `query:"username"` + // Type 账户类型过滤(bank/alipay)。 + Type *string `query:"type"` + // CreatedAtFrom 创建时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 创建时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` +} + +// SuperPayoutAccountItem 超管结算账户列表项。 +type SuperPayoutAccountItem struct { + // ID 结算账户ID。 + ID int64 `json:"id"` + // TenantID 租户ID。 + TenantID int64 `json:"tenant_id"` + // TenantCode 租户编码。 + TenantCode string `json:"tenant_code"` + // TenantName 租户名称。 + TenantName string `json:"tenant_name"` + // UserID 用户ID。 + UserID int64 `json:"user_id"` + // Username 用户名。 + Username string `json:"username"` + // Type 账户类型。 + Type string `json:"type"` + // Name 账户名称/开户行。 + Name string `json:"name"` + // Account 收款账号。 + Account string `json:"account"` + // Realname 收款人姓名。 + Realname string `json:"realname"` + // CreatedAt 创建时间(RFC3339)。 + CreatedAt string `json:"created_at"` + // UpdatedAt 更新时间(RFC3339)。 + UpdatedAt string `json:"updated_at"` +} diff --git a/backend/app/http/super/v1/payout_accounts.go b/backend/app/http/super/v1/payout_accounts.go new file mode 100644 index 0000000..b28866a --- /dev/null +++ b/backend/app/http/super/v1/payout_accounts.go @@ -0,0 +1,45 @@ +package v1 + +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" +) + +// @provider +type payoutAccounts struct{} + +// List payout accounts +// +// @Router /super/v1/payout-accounts [get] +// @Summary List payout accounts +// @Description List payout accounts across tenants +// @Tags Finance +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param limit query int false "Page size" +// @Success 200 {object} requests.Pager{items=[]dto.SuperPayoutAccountItem} +// @Bind filter query +func (c *payoutAccounts) List(ctx fiber.Ctx, filter *dto.SuperPayoutAccountListFilter) (*requests.Pager, error) { + return services.Super.ListPayoutAccounts(ctx, filter) +} + +// Remove payout account +// +// @Router /super/v1/payout-accounts/:id [delete] +// @Summary Remove payout account +// @Description Remove payout account across tenants +// @Tags Finance +// @Accept json +// @Produce json +// @Param id path int64 true "Payout account ID" +// @Success 200 {string} string "Removed" +// @Bind user local key(__ctx_user) +// @Bind id path +func (c *payoutAccounts) Remove(ctx fiber.Ctx, user *models.User, id int64) error { + return services.Super.RemovePayoutAccount(ctx, user.ID, id) +} diff --git a/backend/app/http/super/v1/provider.gen.go b/backend/app/http/super/v1/provider.gen.go index 67310ab..3b4e327 100755 --- a/backend/app/http/super/v1/provider.gen.go +++ b/backend/app/http/super/v1/provider.gen.go @@ -24,6 +24,13 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func() (*creatorApplications, error) { + obj := &creatorApplications{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func() (*creators, error) { obj := &creators{} @@ -38,6 +45,13 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func() (*payoutAccounts, error) { + obj := &payoutAccounts{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func() (*reports, error) { obj := &reports{} @@ -48,24 +62,28 @@ func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( contents *contents, coupons *coupons, + creatorApplications *creatorApplications, creators *creators, middlewares *middlewares.Middlewares, orders *orders, + payoutAccounts *payoutAccounts, reports *reports, tenants *tenants, users *users, withdrawals *withdrawals, ) (contracts.HttpRoute, error) { obj := &Routes{ - contents: contents, - coupons: coupons, - creators: creators, - middlewares: middlewares, - orders: orders, - reports: reports, - tenants: tenants, - users: users, - withdrawals: withdrawals, + contents: contents, + coupons: coupons, + creatorApplications: creatorApplications, + creators: creators, + middlewares: middlewares, + orders: orders, + payoutAccounts: payoutAccounts, + reports: reports, + tenants: tenants, + users: users, + withdrawals: withdrawals, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/app/http/super/v1/routes.gen.go b/backend/app/http/super/v1/routes.gen.go index 65dc564..231ef85 100644 --- a/backend/app/http/super/v1/routes.gen.go +++ b/backend/app/http/super/v1/routes.gen.go @@ -25,14 +25,16 @@ type Routes struct { log *log.Entry `inject:"false"` middlewares *middlewares.Middlewares // Controller instances - contents *contents - coupons *coupons - creators *creators - orders *orders - reports *reports - tenants *tenants - users *users - withdrawals *withdrawals + contents *contents + coupons *coupons + creatorApplications *creatorApplications + creators *creators + orders *orders + payoutAccounts *payoutAccounts + reports *reports + tenants *tenants + users *users + withdrawals *withdrawals } // Prepare initializes the routes provider with logging configuration. @@ -83,6 +85,11 @@ func (r *Routes) Register(router fiber.Router) { Body[dto.SuperContentBatchReviewForm]("form"), )) // Register routes for controller: coupons + r.log.Debugf("Registering route: Get /super/v1/coupon-grants -> coupons.ListGrants") + router.Get("/super/v1/coupon-grants"[len(r.Path()):], DataFunc1( + r.coupons.ListGrants, + Query[dto.SuperCouponGrantListFilter]("filter"), + )) r.log.Debugf("Registering route: Get /super/v1/coupons -> coupons.List") router.Get("/super/v1/coupons"[len(r.Path()):], DataFunc1( r.coupons.List, @@ -94,6 +101,13 @@ func (r *Routes) Register(router fiber.Router) { PathParam[int64]("tenantID"), PathParam[int64]("id"), )) + r.log.Debugf("Registering route: Patch /super/v1/coupons/:id/status -> coupons.UpdateStatus") + router.Patch("/super/v1/coupons/:id/status"[len(r.Path()):], Func3( + r.coupons.UpdateStatus, + Local[*models.User]("__ctx_user"), + PathParam[int64]("id"), + Body[dto.SuperCouponStatusUpdateForm]("form"), + )) r.log.Debugf("Registering route: Post /super/v1/tenants/:tenantID/coupons -> coupons.Create") router.Post("/super/v1/tenants/:tenantID/coupons"[len(r.Path()):], DataFunc3( r.coupons.Create, @@ -116,6 +130,19 @@ func (r *Routes) Register(router fiber.Router) { PathParam[int64]("id"), Body[v1_dto.CouponUpdateForm]("form"), )) + // Register routes for controller: creatorApplications + r.log.Debugf("Registering route: Get /super/v1/creator-applications -> creatorApplications.List") + router.Get("/super/v1/creator-applications"[len(r.Path()):], DataFunc1( + r.creatorApplications.List, + Query[dto.TenantListFilter]("filter"), + )) + r.log.Debugf("Registering route: Post /super/v1/creator-applications/:id/review -> creatorApplications.Review") + router.Post("/super/v1/creator-applications/:id/review"[len(r.Path()):], Func3( + r.creatorApplications.Review, + Local[*models.User]("__ctx_user"), + PathParam[int64]("id"), + Body[dto.SuperCreatorApplicationReviewForm]("form"), + )) // Register routes for controller: creators r.log.Debugf("Registering route: Get /super/v1/creators -> creators.List") router.Get("/super/v1/creators"[len(r.Path()):], DataFunc1( @@ -143,6 +170,18 @@ func (r *Routes) Register(router fiber.Router) { PathParam[int64]("id"), Body[dto.SuperOrderRefundForm]("form"), )) + // Register routes for controller: payoutAccounts + r.log.Debugf("Registering route: Delete /super/v1/payout-accounts/:id -> payoutAccounts.Remove") + router.Delete("/super/v1/payout-accounts/:id"[len(r.Path()):], Func2( + r.payoutAccounts.Remove, + Local[*models.User]("__ctx_user"), + PathParam[int64]("id"), + )) + r.log.Debugf("Registering route: Get /super/v1/payout-accounts -> payoutAccounts.List") + router.Get("/super/v1/payout-accounts"[len(r.Path()):], DataFunc1( + r.payoutAccounts.List, + Query[dto.SuperPayoutAccountListFilter]("filter"), + )) // Register routes for controller: reports r.log.Debugf("Registering route: Get /super/v1/reports/overview -> reports.Overview") router.Get("/super/v1/reports/overview"[len(r.Path()):], DataFunc1( diff --git a/backend/app/services/super.go b/backend/app/services/super.go index f420da3..bb057fe 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -953,6 +953,76 @@ func (s *super) TenantHealth(ctx context.Context, filter *super_dto.TenantListFi }, nil } +func (s *super) ListCreatorApplications(ctx context.Context, filter *super_dto.TenantListFilter) (*requests.Pager, error) { + if filter == nil { + filter = &super_dto.TenantListFilter{} + } + if filter.Status == nil || *filter.Status == "" { + status := consts.TenantStatusPendingVerify + filter.Status = &status + } + return s.ListTenants(ctx, filter) +} + +func (s *super) ReviewCreatorApplication(ctx context.Context, operatorID, tenantID int64, form *super_dto.SuperCreatorApplicationReviewForm) error { + if operatorID == 0 { + return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") + } + if tenantID == 0 || form == nil { + return errorx.ErrBadRequest.WithMsg("审核参数无效") + } + + action := strings.ToLower(strings.TrimSpace(form.Action)) + if action != "approve" && action != "reject" { + return errorx.ErrBadRequest.WithMsg("审核动作无效") + } + + tbl, q := models.TenantQuery.QueryContext(ctx) + tenant, err := q.Where(tbl.ID.Eq(tenantID)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errorx.ErrRecordNotFound.WithMsg("创作者申请不存在") + } + return errorx.ErrDatabaseError.WithCause(err) + } + if tenant.Status != consts.TenantStatusPendingVerify { + return errorx.ErrBadRequest.WithMsg("创作者申请已处理") + } + + nextStatus := consts.TenantStatusVerified + if action == "reject" { + nextStatus = consts.TenantStatusBanned + } + _, err = q.Where(tbl.ID.Eq(tenant.ID), tbl.Status.Eq(consts.TenantStatusPendingVerify)).Update(tbl.Status, nextStatus) + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + + if Notification != nil { + title := "创作者申请审核结果" + detail := "您的创作者申请已通过" + if nextStatus == consts.TenantStatusBanned { + detail = "您的创作者申请已驳回" + if strings.TrimSpace(form.Reason) != "" { + detail += ",原因:" + strings.TrimSpace(form.Reason) + } + } + _ = Notification.Send(ctx, tenant.ID, tenant.UserID, string(consts.NotificationTypeAudit), title, detail) + } + if Audit != nil { + detail := "approve" + if nextStatus == consts.TenantStatusBanned { + detail = "reject" + } + if strings.TrimSpace(form.Reason) != "" { + detail += ",原因:" + strings.TrimSpace(form.Reason) + } + Audit.Log(ctx, operatorID, "review_creator_application", cast.ToString(tenant.ID), detail) + } + + 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 { @@ -1065,6 +1135,191 @@ func (s *super) ListTenantUsers(ctx context.Context, tenantID int64, filter *sup }, nil } +func (s *super) ListPayoutAccounts(ctx context.Context, filter *super_dto.SuperPayoutAccountListFilter) (*requests.Pager, error) { + if filter == nil { + filter = &super_dto.SuperPayoutAccountListFilter{} + } + + tbl, q := models.PayoutAccountQuery.QueryContext(ctx) + if filter.TenantID != nil && *filter.TenantID > 0 { + q = q.Where(tbl.TenantID.Eq(*filter.TenantID)) + } + if filter.UserID != nil && *filter.UserID > 0 { + q = q.Where(tbl.UserID.Eq(*filter.UserID)) + } + if filter.Type != nil && strings.TrimSpace(*filter.Type) != "" { + q = q.Where(tbl.Type.Eq(strings.TrimSpace(*filter.Type))) + } + + tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) + if err != nil { + return nil, err + } + if tenantFilter { + if len(tenantIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.TenantID.In(tenantIDs...)) + } + } + + userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username) + if err != nil { + return nil, err + } + if userFilter { + if len(userIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.UserID.In(userIDs...)) + } + } + + if filter.CreatedAtFrom != nil { + from, err := s.parseFilterTime(filter.CreatedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.CreatedAt.Gte(*from)) + } + } + if filter.CreatedAtTo != nil { + to, err := s.parseFilterTime(filter.CreatedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.CreatedAt.Lte(*to)) + } + } + + filter.Pagination.Format() + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + list, err := q.Order(tbl.CreatedAt.Desc()). + Offset(int(filter.Pagination.Offset())). + Limit(int(filter.Pagination.Limit)). + Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + tenantMap := make(map[int64]*models.Tenant) + userMap := make(map[int64]*models.User) + if len(list) > 0 { + tenantIDSet := make(map[int64]struct{}) + userIDSet := make(map[int64]struct{}) + for _, pa := range list { + if pa.TenantID > 0 { + tenantIDSet[pa.TenantID] = struct{}{} + } + if pa.UserID > 0 { + userIDSet[pa.UserID] = struct{}{} + } + } + + tenantIDs = tenantIDs[:0] + for id := range tenantIDSet { + tenantIDs = append(tenantIDs, id) + } + if len(tenantIDs) > 0 { + tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx) + tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, tenant := range tenants { + tenantMap[tenant.ID] = tenant + } + } + + userIDs = userIDs[:0] + for id := range userIDSet { + userIDs = append(userIDs, id) + } + if len(userIDs) > 0 { + userTbl, userQuery := models.UserQuery.QueryContext(ctx) + users, err := userQuery.Where(userTbl.ID.In(userIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, user := range users { + userMap[user.ID] = user + } + } + } + + items := make([]super_dto.SuperPayoutAccountItem, 0, len(list)) + for _, pa := range list { + tenant := tenantMap[pa.TenantID] + user := userMap[pa.UserID] + tenantCode := "" + tenantName := "" + if tenant != nil { + tenantCode = tenant.Code + tenantName = tenant.Name + } + username := "" + if user != nil { + username = user.Username + } + if username == "" && pa.UserID > 0 { + username = "ID:" + strconv.FormatInt(pa.UserID, 10) + } + + items = append(items, super_dto.SuperPayoutAccountItem{ + ID: pa.ID, + TenantID: pa.TenantID, + TenantCode: tenantCode, + TenantName: tenantName, + UserID: pa.UserID, + Username: username, + Type: pa.Type, + Name: pa.Name, + Account: pa.Account, + Realname: pa.Realname, + CreatedAt: s.formatTime(pa.CreatedAt), + UpdatedAt: s.formatTime(pa.UpdatedAt), + }) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + +func (s *super) RemovePayoutAccount(ctx context.Context, operatorID, id int64) error { + if operatorID == 0 { + return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") + } + if id == 0 { + return errorx.ErrBadRequest.WithMsg("结算账户ID不能为空") + } + + 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) + } + + if _, err := q.Where(tbl.ID.Eq(account.ID)).Delete(); err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + + if Audit != nil { + Audit.Log(ctx, operatorID, "remove_payout_account", cast.ToString(account.ID), "Removed payout account") + } + return nil +} + func (s *super) ListTenantJoinRequests(ctx context.Context, filter *super_dto.SuperTenantJoinRequestListFilter) (*requests.Pager, error) { if filter == nil { filter = &super_dto.SuperTenantJoinRequestListFilter{} @@ -3214,6 +3469,241 @@ func (s *super) ListCoupons(ctx context.Context, filter *super_dto.SuperCouponLi }, nil } +func (s *super) ListCouponGrants(ctx context.Context, filter *super_dto.SuperCouponGrantListFilter) (*requests.Pager, error) { + if filter == nil { + filter = &super_dto.SuperCouponGrantListFilter{} + } + + tbl, q := models.UserCouponQuery.QueryContext(ctx) + if filter.UserID != nil && *filter.UserID > 0 { + q = q.Where(tbl.UserID.Eq(*filter.UserID)) + } + if filter.Status != nil && *filter.Status != "" { + q = q.Where(tbl.Status.Eq(*filter.Status)) + } + + userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username) + if err != nil { + return nil, err + } + if userFilter { + if len(userIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.UserID.In(userIDs...)) + } + } + + couponIDs, couponFilter, err := s.filterCouponGrantCouponIDs(ctx, filter) + if err != nil { + return nil, err + } + if couponFilter { + if len(couponIDs) == 0 { + filter.Pagination.Format() + return &requests.Pager{ + Pagination: filter.Pagination, + Total: 0, + Items: []super_dto.SuperCouponGrantItem{}, + }, nil + } + q = q.Where(tbl.CouponID.In(couponIDs...)) + } + + if filter.CreatedAtFrom != nil { + from, err := s.parseFilterTime(filter.CreatedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.CreatedAt.Gte(*from)) + } + } + if filter.CreatedAtTo != nil { + to, err := s.parseFilterTime(filter.CreatedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.CreatedAt.Lte(*to)) + } + } + if filter.UsedAtFrom != nil { + from, err := s.parseFilterTime(filter.UsedAtFrom) + if err != nil { + return nil, err + } + if from != nil { + q = q.Where(tbl.UsedAt.Gte(*from)) + } + } + if filter.UsedAtTo != nil { + to, err := s.parseFilterTime(filter.UsedAtTo) + if err != nil { + return nil, err + } + if to != nil { + q = q.Where(tbl.UsedAt.Lte(*to)) + } + } + + filter.Pagination.Format() + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + list, err := q.Order(tbl.CreatedAt.Desc()). + Offset(int(filter.Pagination.Offset())). + Limit(int(filter.Pagination.Limit)). + Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + if len(list) == 0 { + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: []super_dto.SuperCouponGrantItem{}, + }, nil + } + + couponIDSet := make(map[int64]struct{}) + userIDSet := make(map[int64]struct{}) + for _, uc := range list { + if uc.CouponID > 0 { + couponIDSet[uc.CouponID] = struct{}{} + } + if uc.UserID > 0 { + userIDSet[uc.UserID] = struct{}{} + } + } + + couponIDs = couponIDs[:0] + for id := range couponIDSet { + couponIDs = append(couponIDs, id) + } + userIDs = userIDs[:0] + for id := range userIDSet { + userIDs = append(userIDs, id) + } + + couponMap := make(map[int64]*models.Coupon, len(couponIDs)) + tenantMap := make(map[int64]*models.Tenant) + if len(couponIDs) > 0 { + couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx) + coupons, err := couponQuery.Where(couponTbl.ID.In(couponIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + tenantSet := make(map[int64]struct{}) + for _, coupon := range coupons { + couponMap[coupon.ID] = coupon + if coupon.TenantID > 0 { + tenantSet[coupon.TenantID] = struct{}{} + } + } + tenantIDs := make([]int64, 0, len(tenantSet)) + for id := range tenantSet { + tenantIDs = append(tenantIDs, id) + } + if len(tenantIDs) > 0 { + tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx) + tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, tenant := range tenants { + tenantMap[tenant.ID] = tenant + } + } + } + + userMap := make(map[int64]*models.User, len(userIDs)) + if len(userIDs) > 0 { + userTbl, userQuery := models.UserQuery.QueryContext(ctx) + users, err := userQuery.Where(userTbl.ID.In(userIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, user := range users { + userMap[user.ID] = user + } + } + + items := make([]super_dto.SuperCouponGrantItem, 0, len(list)) + for _, uc := range list { + item := super_dto.SuperCouponGrantItem{ + ID: uc.ID, + CouponID: uc.CouponID, + UserID: uc.UserID, + Status: uc.Status, + StatusDescription: uc.Status.Description(), + OrderID: uc.OrderID, + UsedAt: s.formatTime(uc.UsedAt), + CreatedAt: s.formatTime(uc.CreatedAt), + } + if user := userMap[uc.UserID]; user != nil { + item.Username = user.Username + } else if uc.UserID > 0 { + item.Username = "ID:" + strconv.FormatInt(uc.UserID, 10) + } + if coupon := couponMap[uc.CouponID]; coupon != nil { + item.CouponTitle = coupon.Title + item.TenantID = coupon.TenantID + if tenant := tenantMap[coupon.TenantID]; tenant != nil { + item.TenantCode = tenant.Code + item.TenantName = tenant.Name + } + } + items = append(items, item) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + +func (s *super) UpdateCouponStatus(ctx context.Context, operatorID, couponID int64, form *super_dto.SuperCouponStatusUpdateForm) error { + if operatorID == 0 { + return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") + } + if couponID == 0 || form == nil { + return errorx.ErrBadRequest.WithMsg("参数无效") + } + status := strings.ToLower(strings.TrimSpace(form.Status)) + if status != "frozen" { + return errorx.ErrBadRequest.WithMsg("仅支持冻结操作") + } + + tbl, q := models.CouponQuery.QueryContext(ctx) + coupon, err := q.Where(tbl.ID.Eq(couponID)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errorx.ErrRecordNotFound.WithMsg("优惠券不存在") + } + return errorx.ErrDatabaseError.WithCause(err) + } + + now := time.Now() + if !coupon.EndAt.IsZero() && now.After(coupon.EndAt) { + return nil + } + _, err = q.Where(tbl.ID.Eq(coupon.ID)).UpdateSimple( + tbl.EndAt.Value(now), + tbl.UpdatedAt.Value(now), + ) + if err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + + if Audit != nil { + Audit.Log(ctx, operatorID, "freeze_coupon", cast.ToString(coupon.ID), "Freeze coupon") + } + return nil +} + func (s *super) ReportOverview(ctx context.Context, filter *super_dto.SuperReportOverviewFilter) (*v1_dto.ReportOverviewResponse, error) { // 统一统计时间范围与粒度。 rg, err := s.normalizeReportRange(filter) @@ -3766,6 +4256,50 @@ func (s *super) filterCouponIDs(ctx context.Context, filter *super_dto.SuperUser return ids, true, nil } +func (s *super) filterCouponGrantCouponIDs(ctx context.Context, filter *super_dto.SuperCouponGrantListFilter) ([]int64, bool, error) { + if filter == nil { + return nil, false, nil + } + + if filter.CouponID != nil && *filter.CouponID > 0 { + return []int64{*filter.CouponID}, true, nil + } + + tenantIDs := make([]int64, 0) + applied := false + if filter.TenantID != nil && *filter.TenantID > 0 { + applied = true + tenantIDs = append(tenantIDs, *filter.TenantID) + } + + lookupIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) + if err != nil { + return nil, true, err + } + if tenantFilter { + applied = true + tenantIDs = append(tenantIDs, lookupIDs...) + } + + if !applied { + return nil, false, nil + } + if len(tenantIDs) == 0 { + return []int64{}, true, nil + } + + couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx) + coupons, err := couponQuery.Where(couponTbl.TenantID.In(tenantIDs...)).Select(couponTbl.ID).Find() + if err != nil { + return nil, true, errorx.ErrDatabaseError.WithCause(err) + } + ids := make([]int64, 0, len(coupons)) + for _, coupon := range coupons { + ids = append(ids, coupon.ID) + } + return ids, true, nil +} + func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reason string) error { if operatorID == 0 { return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") diff --git a/backend/docs/docs.go b/backend/docs/docs.go index ad60097..0882fd6 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -209,6 +209,58 @@ const docTemplate = `{ } } }, + "/super/v1/coupon-grants": { + "get": { + "description": "List coupon grant records across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "List coupon grants", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperCouponGrantItem" + } + } + } + } + ] + } + } + } + } + }, "/super/v1/coupons": { "get": { "description": "List coupon templates across tenants", @@ -261,6 +313,142 @@ const docTemplate = `{ } } }, + "/super/v1/coupons/{id}/status": { + "patch": { + "description": "Update coupon status across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Update coupon status", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperCouponStatusUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "type": "string" + } + } + } + } + }, + "/super/v1/creator-applications": { + "get": { + "description": "List creator applications across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Creator" + ], + "summary": "List creator applications", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/creator-applications/{id}/review": { + "post": { + "description": "Approve or reject creator application", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Creator" + ], + "summary": "Review creator application", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Review form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperCreatorApplicationReviewForm" + } + } + ], + "responses": { + "200": { + "description": "Reviewed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/creators": { "get": { "description": "List creator tenants (channels) across the platform", @@ -463,6 +651,91 @@ const docTemplate = `{ } } }, + "/super/v1/payout-accounts": { + "get": { + "description": "List payout accounts across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Finance" + ], + "summary": "List payout accounts", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperPayoutAccountItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/payout-accounts/{id}": { + "delete": { + "description": "Remove payout account across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Finance" + ], + "summary": "Remove payout account", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Payout account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Removed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/reports/export": { "post": { "description": "Export platform report data", @@ -6118,6 +6391,67 @@ const docTemplate = `{ } } }, + "dto.SuperCouponGrantItem": { + "type": "object", + "properties": { + "coupon_id": { + "description": "CouponID 优惠券ID。", + "type": "integer" + }, + "coupon_title": { + "description": "CouponTitle 优惠券标题。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 领取时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 用户券ID。", + "type": "integer" + }, + "order_id": { + "description": "OrderID 使用订单ID。", + "type": "integer" + }, + "status": { + "description": "Status 用户券状态。", + "allOf": [ + { + "$ref": "#/definitions/consts.UserCouponStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 状态描述(用于展示)。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "used_at": { + "description": "UsedAt 使用时间(RFC3339)。", + "type": "string" + }, + "user_id": { + "description": "UserID 用户ID。", + "type": "integer" + }, + "username": { + "description": "Username 用户名。", + "type": "string" + } + } + }, "dto.SuperCouponGrantResponse": { "type": "object", "properties": { @@ -6212,6 +6546,41 @@ const docTemplate = `{ } } }, + "dto.SuperCouponStatusUpdateForm": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "description": "Status 目标状态(frozen)。", + "type": "string", + "enum": [ + "frozen" + ] + } + } + }, + "dto.SuperCreatorApplicationReviewForm": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "description": "Action 审核动作(approve/reject)。", + "type": "string", + "enum": [ + "approve", + "reject" + ] + }, + "reason": { + "description": "Reason 审核说明(可选,驳回时填写)。", + "type": "string" + } + } + }, "dto.SuperOrderDetail": { "type": "object", "properties": { @@ -6369,6 +6738,59 @@ const docTemplate = `{ } } }, + "dto.SuperPayoutAccountItem": { + "type": "object", + "properties": { + "account": { + "description": "Account 收款账号。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 结算账户ID。", + "type": "integer" + }, + "name": { + "description": "Name 账户名称/开户行。", + "type": "string" + }, + "realname": { + "description": "Realname 收款人姓名。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "type": { + "description": "Type 账户类型。", + "type": "string" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user_id": { + "description": "UserID 用户ID。", + "type": "integer" + }, + "username": { + "description": "Username 用户名。", + "type": "string" + } + } + }, "dto.SuperReportExportForm": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index c2ed9ad..1f877c5 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -203,6 +203,58 @@ } } }, + "/super/v1/coupon-grants": { + "get": { + "description": "List coupon grant records across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "List coupon grants", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperCouponGrantItem" + } + } + } + } + ] + } + } + } + } + }, "/super/v1/coupons": { "get": { "description": "List coupon templates across tenants", @@ -255,6 +307,142 @@ } } }, + "/super/v1/coupons/{id}/status": { + "patch": { + "description": "Update coupon status across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Update coupon status", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperCouponStatusUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "type": "string" + } + } + } + } + }, + "/super/v1/creator-applications": { + "get": { + "description": "List creator applications across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Creator" + ], + "summary": "List creator applications", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/creator-applications/{id}/review": { + "post": { + "description": "Approve or reject creator application", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Creator" + ], + "summary": "Review creator application", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Review form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SuperCreatorApplicationReviewForm" + } + } + ], + "responses": { + "200": { + "description": "Reviewed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/creators": { "get": { "description": "List creator tenants (channels) across the platform", @@ -457,6 +645,91 @@ } } }, + "/super/v1/payout-accounts": { + "get": { + "description": "List payout accounts across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Finance" + ], + "summary": "List payout accounts", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperPayoutAccountItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/payout-accounts/{id}": { + "delete": { + "description": "Remove payout account across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Finance" + ], + "summary": "Remove payout account", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Payout account ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Removed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/reports/export": { "post": { "description": "Export platform report data", @@ -6112,6 +6385,67 @@ } } }, + "dto.SuperCouponGrantItem": { + "type": "object", + "properties": { + "coupon_id": { + "description": "CouponID 优惠券ID。", + "type": "integer" + }, + "coupon_title": { + "description": "CouponTitle 优惠券标题。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 领取时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 用户券ID。", + "type": "integer" + }, + "order_id": { + "description": "OrderID 使用订单ID。", + "type": "integer" + }, + "status": { + "description": "Status 用户券状态。", + "allOf": [ + { + "$ref": "#/definitions/consts.UserCouponStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 状态描述(用于展示)。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "used_at": { + "description": "UsedAt 使用时间(RFC3339)。", + "type": "string" + }, + "user_id": { + "description": "UserID 用户ID。", + "type": "integer" + }, + "username": { + "description": "Username 用户名。", + "type": "string" + } + } + }, "dto.SuperCouponGrantResponse": { "type": "object", "properties": { @@ -6206,6 +6540,41 @@ } } }, + "dto.SuperCouponStatusUpdateForm": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "description": "Status 目标状态(frozen)。", + "type": "string", + "enum": [ + "frozen" + ] + } + } + }, + "dto.SuperCreatorApplicationReviewForm": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "description": "Action 审核动作(approve/reject)。", + "type": "string", + "enum": [ + "approve", + "reject" + ] + }, + "reason": { + "description": "Reason 审核说明(可选,驳回时填写)。", + "type": "string" + } + } + }, "dto.SuperOrderDetail": { "type": "object", "properties": { @@ -6363,6 +6732,59 @@ } } }, + "dto.SuperPayoutAccountItem": { + "type": "object", + "properties": { + "account": { + "description": "Account 收款账号。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 结算账户ID。", + "type": "integer" + }, + "name": { + "description": "Name 账户名称/开户行。", + "type": "string" + }, + "realname": { + "description": "Realname 收款人姓名。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "type": { + "description": "Type 账户类型。", + "type": "string" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user_id": { + "description": "UserID 用户ID。", + "type": "integer" + }, + "username": { + "description": "Username 用户名。", + "type": "string" + } + } + }, "dto.SuperReportExportForm": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 7d3f1cf..895fc5b 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1060,6 +1060,49 @@ definitions: description: Name 租户名称。 type: string type: object + dto.SuperCouponGrantItem: + properties: + coupon_id: + description: CouponID 优惠券ID。 + type: integer + coupon_title: + description: CouponTitle 优惠券标题。 + type: string + created_at: + description: CreatedAt 领取时间(RFC3339)。 + type: string + id: + description: ID 用户券ID。 + type: integer + order_id: + description: OrderID 使用订单ID。 + type: integer + status: + allOf: + - $ref: '#/definitions/consts.UserCouponStatus' + description: Status 用户券状态。 + status_description: + description: StatusDescription 状态描述(用于展示)。 + type: string + tenant_code: + description: TenantCode 租户编码。 + type: string + tenant_id: + description: TenantID 租户ID。 + type: integer + tenant_name: + description: TenantName 租户名称。 + type: string + used_at: + description: UsedAt 使用时间(RFC3339)。 + type: string + user_id: + description: UserID 用户ID。 + type: integer + username: + description: Username 用户名。 + type: string + type: object dto.SuperCouponGrantResponse: properties: granted: @@ -1127,6 +1170,30 @@ definitions: description: Value 优惠券面额/折扣值。 type: integer type: object + dto.SuperCouponStatusUpdateForm: + properties: + status: + description: Status 目标状态(frozen)。 + enum: + - frozen + type: string + required: + - status + type: object + dto.SuperCreatorApplicationReviewForm: + properties: + action: + description: Action 审核动作(approve/reject)。 + enum: + - approve + - reject + type: string + reason: + description: Reason 审核说明(可选,驳回时填写)。 + type: string + required: + - action + type: object dto.SuperOrderDetail: properties: buyer: @@ -1225,6 +1292,45 @@ definitions: description: Reason 退款原因说明。 type: string type: object + dto.SuperPayoutAccountItem: + properties: + account: + description: Account 收款账号。 + type: string + created_at: + description: CreatedAt 创建时间(RFC3339)。 + type: string + id: + description: ID 结算账户ID。 + type: integer + name: + description: Name 账户名称/开户行。 + type: string + realname: + description: Realname 收款人姓名。 + type: string + tenant_code: + description: TenantCode 租户编码。 + type: string + tenant_id: + description: TenantID 租户ID。 + type: integer + tenant_name: + description: TenantName 租户名称。 + type: string + type: + description: Type 账户类型。 + type: string + updated_at: + description: UpdatedAt 更新时间(RFC3339)。 + type: string + user_id: + description: UserID 用户ID。 + type: integer + username: + description: Username 用户名。 + type: string + type: object dto.SuperReportExportForm: properties: end_at: @@ -2394,6 +2500,37 @@ paths: summary: Batch review contents tags: - Content + /super/v1/coupon-grants: + get: + consumes: + - application/json + description: List coupon grant records across tenants + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperCouponGrantItem' + type: array + type: object + summary: List coupon grants + tags: + - Coupon /super/v1/coupons: get: consumes: @@ -2425,6 +2562,93 @@ paths: summary: List coupons tags: - Coupon + /super/v1/coupons/{id}/status: + patch: + consumes: + - application/json + description: Update coupon status across tenants + parameters: + - description: Coupon ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Update form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.SuperCouponStatusUpdateForm' + produces: + - application/json + responses: + "200": + description: Updated + schema: + type: string + summary: Update coupon status + tags: + - Coupon + /super/v1/creator-applications: + get: + consumes: + - application/json + description: List creator applications across tenants + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.TenantItem' + type: array + type: object + summary: List creator applications + tags: + - Creator + /super/v1/creator-applications/{id}/review: + post: + consumes: + - application/json + description: Approve or reject creator application + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Review form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.SuperCreatorApplicationReviewForm' + produces: + - application/json + responses: + "200": + description: Reviewed + schema: + type: string + summary: Review creator application + tags: + - Creator /super/v1/creators: get: consumes: @@ -2552,6 +2776,59 @@ paths: summary: Order statistics tags: - Order + /super/v1/payout-accounts: + get: + consumes: + - application/json + description: List payout accounts across tenants + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperPayoutAccountItem' + type: array + type: object + summary: List payout accounts + tags: + - Finance + /super/v1/payout-accounts/{id}: + delete: + consumes: + - application/json + description: Remove payout account across tenants + parameters: + - description: Payout account ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Removed + schema: + type: string + summary: Remove payout account + tags: + - Finance /super/v1/reports/export: post: consumes: diff --git a/docs/superadmin_progress.md b/docs/superadmin_progress.md index 0816e0a..2a0c5f0 100644 --- a/docs/superadmin_progress.md +++ b/docs/superadmin_progress.md @@ -52,13 +52,13 @@ ### 2.9 创作者与成员审核 `/superadmin/creators` - 状态:**部分完成** -- 已有:创作者(租户)列表、状态更新、成员申请列表/审核、成员邀请创建。 -- 缺口:跨租户创作者申请审核、结算账户审核、提现审核入口(与财务联动)。 +- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除。 +- 缺口:提现审核入口(与财务联动)、结算账户审批流(若需要区分通过/驳回状态)。 ### 2.10 优惠券 `/superadmin/coupons` - 状态:**部分完成** -- 已有:跨租户优惠券列表、创建/编辑、发放。 -- 缺口:冻结/归档、发放记录与异常核查。 +- 已有:跨租户优惠券列表、创建/编辑、发放、冻结、发放记录查询。 +- 缺口:异常核查与自动告警策略。 ### 2.11 财务与钱包 `/superadmin/finance` - 状态:**部分完成** @@ -83,11 +83,11 @@ ## 3) `/super/v1` 接口覆盖度概览 - **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放)、Creators(列表)。 -- **缺失/待补**:资产治理、通知中心、用户互动明细(收藏/点赞/关注)、创作者申请/结算账户审核、优惠券冻结与发放记录。 +- **缺失/待补**:资产治理、通知中心、用户互动明细(收藏/点赞/关注)、创作者提现审核、优惠券异常核查与风控。 ## 4) 建议的下一步(按优先级) -1. **创作者/优惠券深度治理**:补齐创作者申请/结算账户审核、优惠券冻结/发放记录。 -2. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。 -3. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。 -4. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。 +1. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。 +2. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。 +3. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。 +4. **优惠券异常核查**:完善发放/核销异常监测与风控处理流程。 diff --git a/frontend/superadmin/src/service/CouponService.js b/frontend/superadmin/src/service/CouponService.js index 68b6721..45b5791 100644 --- a/frontend/superadmin/src/service/CouponService.js +++ b/frontend/superadmin/src/service/CouponService.js @@ -69,6 +69,46 @@ export const CouponService = { method: 'POST', body: { user_ids: userIDs } }); + }, + async updateCouponStatus(couponID, status) { + if (!couponID) throw new Error('couponID is required'); + if (!status) throw new Error('status is required'); + return requestJson(`/super/v1/coupons/${couponID}/status`, { + method: 'PATCH', + body: { status } + }); + }, + async listCouponGrants({ page, limit, coupon_id, tenant_id, tenant_code, tenant_name, user_id, username, status, created_at_from, created_at_to, used_at_from, used_at_to } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + coupon_id, + tenant_id, + tenant_code, + tenant_name, + user_id, + username, + status, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to), + used_at_from: iso(used_at_from), + used_at_to: iso(used_at_to) + }; + + const data = await requestJson('/super/v1/coupon-grants', { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; } }; diff --git a/frontend/superadmin/src/service/CreatorService.js b/frontend/superadmin/src/service/CreatorService.js index be5b217..1691ced 100644 --- a/frontend/superadmin/src/service/CreatorService.js +++ b/frontend/superadmin/src/service/CreatorService.js @@ -68,6 +68,41 @@ export const CreatorService = { items: normalizeItems(data?.items) }; }, + async listCreatorApplications({ page, limit, id, user_id, name, code, status, created_at_from, created_at_to } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + id, + user_id, + name, + code, + status, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + + const data = await requestJson('/super/v1/creator-applications', { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; + }, + async reviewCreatorApplication(tenantID, { action, reason } = {}) { + if (!tenantID) throw new Error('tenantID is required'); + return requestJson(`/super/v1/creator-applications/${tenantID}/review`, { + method: 'POST', + body: { action, reason } + }); + }, async reviewJoinRequest(requestID, { action, reason } = {}) { if (!requestID) throw new Error('requestID is required'); return requestJson(`/super/v1/tenant-join-requests/${requestID}/review`, { @@ -75,6 +110,39 @@ export const CreatorService = { body: { action, reason } }); }, + async listPayoutAccounts({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, type, created_at_from, created_at_to } = {}) { + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + tenant_id, + tenant_code, + tenant_name, + user_id, + username, + type, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + + const data = await requestJson('/super/v1/payout-accounts', { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; + }, + async removePayoutAccount(accountID) { + if (!accountID) throw new Error('accountID is required'); + return requestJson(`/super/v1/payout-accounts/${accountID}`, { method: 'DELETE' }); + }, async createInvite(tenantID, { max_uses, expires_at, remark } = {}) { if (!tenantID) throw new Error('tenantID is required'); return requestJson(`/super/v1/tenants/${tenantID}/invites`, { diff --git a/frontend/superadmin/src/views/superadmin/Coupons.vue b/frontend/superadmin/src/views/superadmin/Coupons.vue index 8db2a54..5798982 100644 --- a/frontend/superadmin/src/views/superadmin/Coupons.vue +++ b/frontend/superadmin/src/views/superadmin/Coupons.vue @@ -7,6 +7,8 @@ import { onMounted, ref } from 'vue'; const toast = useToast(); +const tabValue = ref('coupons'); + const coupons = ref([]); const loading = ref(false); @@ -26,6 +28,23 @@ const createdAtTo = ref(null); const sortField = ref('created_at'); const sortOrder = ref(-1); +const grants = ref([]); +const grantsLoading = ref(false); +const grantsTotal = ref(0); +const grantsPage = ref(1); +const grantsRows = ref(10); +const grantCouponIDFilter = ref(null); +const grantTenantID = ref(null); +const grantTenantCode = ref(''); +const grantTenantName = ref(''); +const grantUserID = ref(null); +const grantUsername = ref(''); +const grantStatus = ref(''); +const grantCreatedAtFrom = ref(null); +const grantCreatedAtTo = ref(null); +const grantUsedAtFrom = ref(null); +const grantUsedAtTo = ref(null); + const typeOptions = [ { label: '全部', value: '' }, { label: '固定金额', value: 'fix_amount' }, @@ -39,6 +58,13 @@ const statusOptions = [ { label: '已过期', value: 'expired' } ]; +const grantStatusOptions = [ + { label: '全部', value: '' }, + { label: '未使用', value: 'unused' }, + { label: '已使用', value: 'used' }, + { label: '已过期', value: 'expired' } +]; + const editDialogVisible = ref(false); const couponSubmitting = ref(false); const editingCoupon = ref(null); @@ -58,6 +84,10 @@ const grantSubmitting = ref(false); const grantCoupon = ref(null); const grantUserIDsText = ref(''); +const freezeDialogVisible = ref(false); +const freezeSubmitting = ref(false); +const freezeTarget = ref(null); + function formatDate(value) { if (!value) return '-'; if (String(value).startsWith('0001-01-01')) return '-'; @@ -92,6 +122,19 @@ function getStatusSeverity(value) { } } +function getGrantStatusSeverity(value) { + switch (value) { + case 'unused': + return 'success'; + case 'used': + return 'secondary'; + case 'expired': + return 'danger'; + default: + return 'secondary'; + } +} + function resetCouponForm() { formTenantID.value = null; formTitle.value = ''; @@ -197,6 +240,27 @@ async function confirmGrant() { } } +function openFreezeDialog(row) { + freezeTarget.value = row; + freezeDialogVisible.value = true; +} + +async function confirmFreeze() { + const couponIDValue = freezeTarget.value?.id; + if (!couponIDValue) return; + freezeSubmitting.value = true; + try { + await CouponService.updateCouponStatus(couponIDValue, 'frozen'); + toast.add({ severity: 'success', summary: '已冻结', detail: `CouponID: ${couponIDValue}`, life: 3000 }); + freezeDialogVisible.value = false; + await loadCoupons(); + } catch (error) { + toast.add({ severity: 'error', summary: '冻结失败', detail: error?.message || '无法冻结优惠券', life: 4000 }); + } finally { + freezeSubmitting.value = false; + } +} + async function loadCoupons() { loading.value = true; try { @@ -224,11 +288,43 @@ async function loadCoupons() { } } +async function loadGrants() { + grantsLoading.value = true; + try { + const result = await CouponService.listCouponGrants({ + page: grantsPage.value, + limit: grantsRows.value, + coupon_id: grantCouponIDFilter.value || undefined, + tenant_id: grantTenantID.value || undefined, + tenant_code: grantTenantCode.value, + tenant_name: grantTenantName.value, + user_id: grantUserID.value || undefined, + username: grantUsername.value, + status: grantStatus.value || undefined, + created_at_from: grantCreatedAtFrom.value || undefined, + created_at_to: grantCreatedAtTo.value || undefined, + used_at_from: grantUsedAtFrom.value || undefined, + used_at_to: grantUsedAtTo.value || undefined + }); + grants.value = result.items; + grantsTotal.value = result.total; + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载发放记录', life: 4000 }); + } finally { + grantsLoading.value = false; + } +} + function onSearch() { page.value = 1; loadCoupons(); } +function onGrantSearch() { + grantsPage.value = 1; + loadGrants(); +} + function onReset() { couponID.value = null; tenantID.value = null; @@ -246,12 +342,35 @@ function onReset() { loadCoupons(); } +function onGrantReset() { + grantCouponIDFilter.value = null; + grantTenantID.value = null; + grantTenantCode.value = ''; + grantTenantName.value = ''; + grantUserID.value = null; + grantUsername.value = ''; + grantStatus.value = ''; + grantCreatedAtFrom.value = null; + grantCreatedAtTo.value = null; + grantUsedAtFrom.value = null; + grantUsedAtTo.value = null; + grantsPage.value = 1; + grantsRows.value = 10; + loadGrants(); +} + function onPage(event) { page.value = (event.page ?? 0) + 1; rows.value = event.rows ?? rows.value; loadCoupons(); } +function onGrantPage(event) { + grantsPage.value = (event.page ?? 0) + 1; + grantsRows.value = event.rows ?? grantsRows.value; + loadGrants(); +} + function onSort(event) { sortField.value = event.sortField ?? sortField.value; sortOrder.value = event.sortOrder ?? sortOrder.value; @@ -260,131 +379,254 @@ function onSort(event) { onMounted(() => { loadCoupons(); + loadGrants(); }); + + + +
+
冻结后将停止该券的发放与使用,请确认。
+
+ {{ freezeTarget?.title || '-' }} + (TenantID: {{ freezeTarget?.tenant_id ?? '-' }}) +
+
+ +
diff --git a/frontend/superadmin/src/views/superadmin/Creators.vue b/frontend/superadmin/src/views/superadmin/Creators.vue index 3e42c73..d749db0 100644 --- a/frontend/superadmin/src/views/superadmin/Creators.vue +++ b/frontend/superadmin/src/views/superadmin/Creators.vue @@ -4,7 +4,7 @@ import SearchPanel from '@/components/SearchPanel.vue'; import { CreatorService } from '@/service/CreatorService'; import { TenantService } from '@/service/TenantService'; import { useToast } from 'primevue/usetoast'; -import { onMounted, ref } from 'vue'; +import { computed, onMounted, ref } from 'vue'; const toast = useToast(); @@ -34,6 +34,8 @@ const statusUpdating = ref(false); const statusTenant = ref(null); const statusValue = ref(null); +const applicationStatusOptions = computed(() => [{ label: '全部', value: '' }, ...(statusOptions.value || [])]); + const joinRequests = ref([]); const joinRequestsLoading = ref(false); const joinRequestsTotal = ref(0); @@ -48,6 +50,33 @@ const joinStatus = ref('pending'); const joinCreatedAtFrom = ref(null); const joinCreatedAtTo = ref(null); +const applications = ref([]); +const applicationsLoading = ref(false); +const applicationsTotal = ref(0); +const applicationsPage = ref(1); +const applicationsRows = ref(10); +const applicationTenantID = ref(null); +const applicationOwnerUserID = ref(null); +const applicationName = ref(''); +const applicationCode = ref(''); +const applicationStatus = ref('pending_verify'); +const applicationCreatedAtFrom = ref(null); +const applicationCreatedAtTo = ref(null); + +const payoutAccounts = ref([]); +const payoutAccountsLoading = ref(false); +const payoutAccountsTotal = ref(0); +const payoutAccountsPage = ref(1); +const payoutAccountsRows = ref(10); +const payoutTenantID = ref(null); +const payoutTenantCode = ref(''); +const payoutTenantName = ref(''); +const payoutUserID = ref(null); +const payoutUsername = ref(''); +const payoutType = ref(''); +const payoutCreatedAtFrom = ref(null); +const payoutCreatedAtTo = ref(null); + const joinStatusOptions = [ { label: '全部', value: '' }, { label: '待审核', value: 'pending' }, @@ -61,6 +90,16 @@ const reviewAction = ref('approve'); const reviewReason = ref(''); const reviewTarget = ref(null); +const applicationReviewDialogVisible = ref(false); +const applicationReviewSubmitting = ref(false); +const applicationReviewAction = ref('approve'); +const applicationReviewReason = ref(''); +const applicationReviewTarget = ref(null); + +const payoutRemoveDialogVisible = ref(false); +const payoutRemoveSubmitting = ref(false); +const payoutRemoveTarget = ref(null); + const inviteDialogVisible = ref(false); const inviteSubmitting = ref(false); const inviteTenantID = ref(null); @@ -166,6 +205,53 @@ async function loadJoinRequests() { } } +async function loadCreatorApplications() { + applicationsLoading.value = true; + try { + const result = await CreatorService.listCreatorApplications({ + page: applicationsPage.value, + limit: applicationsRows.value, + id: applicationTenantID.value || undefined, + user_id: applicationOwnerUserID.value || undefined, + name: applicationName.value, + code: applicationCode.value, + status: applicationStatus.value || undefined, + created_at_from: applicationCreatedAtFrom.value || undefined, + created_at_to: applicationCreatedAtTo.value || undefined + }); + applications.value = result.items; + applicationsTotal.value = result.total; + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载创作者申请', life: 4000 }); + } finally { + applicationsLoading.value = false; + } +} + +async function loadPayoutAccounts() { + payoutAccountsLoading.value = true; + try { + const result = await CreatorService.listPayoutAccounts({ + page: payoutAccountsPage.value, + limit: payoutAccountsRows.value, + tenant_id: payoutTenantID.value || undefined, + tenant_code: payoutTenantCode.value, + tenant_name: payoutTenantName.value, + user_id: payoutUserID.value || undefined, + username: payoutUsername.value, + type: payoutType.value || undefined, + created_at_from: payoutCreatedAtFrom.value || undefined, + created_at_to: payoutCreatedAtTo.value || undefined + }); + payoutAccounts.value = result.items; + payoutAccountsTotal.value = result.total; + } catch (error) { + toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载结算账户', life: 4000 }); + } finally { + payoutAccountsLoading.value = false; + } +} + function onSearch() { page.value = 1; loadCreators(); @@ -223,6 +309,55 @@ function onJoinPage(event) { loadJoinRequests(); } +function onApplicationSearch() { + applicationsPage.value = 1; + loadCreatorApplications(); +} + +function onApplicationReset() { + applicationTenantID.value = null; + applicationOwnerUserID.value = null; + applicationName.value = ''; + applicationCode.value = ''; + applicationStatus.value = 'pending_verify'; + applicationCreatedAtFrom.value = null; + applicationCreatedAtTo.value = null; + applicationsPage.value = 1; + applicationsRows.value = 10; + loadCreatorApplications(); +} + +function onApplicationPage(event) { + applicationsPage.value = (event.page ?? 0) + 1; + applicationsRows.value = event.rows ?? applicationsRows.value; + loadCreatorApplications(); +} + +function onPayoutSearch() { + payoutAccountsPage.value = 1; + loadPayoutAccounts(); +} + +function onPayoutReset() { + payoutTenantID.value = null; + payoutTenantCode.value = ''; + payoutTenantName.value = ''; + payoutUserID.value = null; + payoutUsername.value = ''; + payoutType.value = ''; + payoutCreatedAtFrom.value = null; + payoutCreatedAtTo.value = null; + payoutAccountsPage.value = 1; + payoutAccountsRows.value = 10; + loadPayoutAccounts(); +} + +function onPayoutPage(event) { + payoutAccountsPage.value = (event.page ?? 0) + 1; + payoutAccountsRows.value = event.rows ?? payoutAccountsRows.value; + loadPayoutAccounts(); +} + async function openStatusDialog(tenant) { statusTenant.value = tenant; statusValue.value = tenant?.status ?? null; @@ -280,6 +415,57 @@ async function confirmReview() { } } +function openApplicationReviewDialog(row, action) { + applicationReviewTarget.value = row; + applicationReviewAction.value = action || 'approve'; + applicationReviewReason.value = ''; + applicationReviewDialogVisible.value = true; +} + +async function confirmApplicationReview() { + const targetID = applicationReviewTarget.value?.id; + if (!targetID) return; + const reason = applicationReviewReason.value.trim(); + if (applicationReviewAction.value === 'reject' && !reason) { + toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回时需填写原因', life: 3000 }); + return; + } + + applicationReviewSubmitting.value = true; + try { + await CreatorService.reviewCreatorApplication(targetID, { action: applicationReviewAction.value, reason }); + toast.add({ severity: 'success', summary: '审核完成', detail: `TenantID: ${targetID}`, life: 3000 }); + applicationReviewDialogVisible.value = false; + await loadCreatorApplications(); + await loadCreators(); + } catch (error) { + toast.add({ severity: 'error', summary: '审核失败', detail: error?.message || '无法审核申请', life: 4000 }); + } finally { + applicationReviewSubmitting.value = false; + } +} + +function openPayoutRemoveDialog(row) { + payoutRemoveTarget.value = row; + payoutRemoveDialogVisible.value = true; +} + +async function confirmRemovePayoutAccount() { + const targetID = payoutRemoveTarget.value?.id; + if (!targetID) return; + payoutRemoveSubmitting.value = true; + try { + await CreatorService.removePayoutAccount(targetID); + toast.add({ severity: 'success', summary: '已删除', detail: `账户ID: ${targetID}`, life: 3000 }); + payoutRemoveDialogVisible.value = false; + await loadPayoutAccounts(); + } catch (error) { + toast.add({ severity: 'error', summary: '删除失败', detail: error?.message || '无法删除结算账户', life: 4000 }); + } finally { + payoutRemoveSubmitting.value = false; + } +} + function openInviteDialog(row) { inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null; inviteMaxUses.value = 1; @@ -315,6 +501,8 @@ onMounted(() => { loadCreators(); ensureStatusOptionsLoaded().catch(() => {}); loadJoinRequests(); + loadCreatorApplications(); + loadPayoutAccounts(); }); @@ -323,7 +511,9 @@ onMounted(() => { 创作者列表 + 申请审核 成员审核 + 结算账户 @@ -412,6 +602,96 @@ onMounted(() => { + +
+
+

创作者申请

+ 审核待开通创作者 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+
该操作将移除结算账户信息,请确认。
+
+ {{ payoutRemoveTarget?.name || '-' }} + ({{ payoutRemoveTarget?.account || '-' }}) +
+
+ +
+