feat: add payout account review flow

This commit is contained in:
2026-01-16 15:17:43 +08:00
parent daaacc3fa4
commit 028c462eaa
21 changed files with 1100 additions and 151 deletions

View File

@@ -1,6 +1,9 @@
package dto
import "quyun/v2/app/requests"
import (
"quyun/v2/app/requests"
"quyun/v2/pkg/consts"
)
// SuperPayoutAccountListFilter 超管结算账户列表过滤条件。
type SuperPayoutAccountListFilter struct {
@@ -17,6 +20,8 @@ type SuperPayoutAccountListFilter struct {
Username *string `query:"username"`
// Type 账户类型过滤bank/alipay
Type *string `query:"type"`
// Status 审核状态过滤pending/approved/rejected
Status *consts.PayoutAccountStatus `query:"status"`
// CreatedAtFrom 创建时间起始RFC3339
CreatedAtFrom *string `query:"created_at_from"`
// CreatedAtTo 创建时间结束RFC3339
@@ -45,8 +50,25 @@ type SuperPayoutAccountItem struct {
Account string `json:"account"`
// Realname 收款人姓名。
Realname string `json:"realname"`
// Status 审核状态。
Status consts.PayoutAccountStatus `json:"status"`
// StatusDescription 审核状态描述(用于展示)。
StatusDescription string `json:"status_description"`
// ReviewedBy 审核操作者ID。
ReviewedBy int64 `json:"reviewed_by"`
// ReviewedAt 审核时间RFC3339
ReviewedAt string `json:"reviewed_at"`
// ReviewReason 审核说明/驳回原因。
ReviewReason string `json:"review_reason"`
// CreatedAt 创建时间RFC3339
CreatedAt string `json:"created_at"`
// UpdatedAt 更新时间RFC3339
UpdatedAt string `json:"updated_at"`
}
type SuperPayoutAccountReviewForm struct {
// Action 审核动作approve/reject
Action string `json:"action" validate:"required,oneof=approve reject"`
// Reason 审核说明(驳回时必填)。
Reason string `json:"reason"`
}

View File

@@ -43,3 +43,21 @@ func (c *payoutAccounts) List(ctx fiber.Ctx, filter *dto.SuperPayoutAccountListF
func (c *payoutAccounts) Remove(ctx fiber.Ctx, user *models.User, id int64) error {
return services.Super.RemovePayoutAccount(ctx, user.ID, id)
}
// Review payout account
//
// @Router /super/v1/payout-accounts/:id<int>/review [post]
// @Summary Review payout account
// @Description Review payout account across tenants
// @Tags Finance
// @Accept json
// @Produce json
// @Param id path int64 true "Payout account ID"
// @Param form body dto.SuperPayoutAccountReviewForm true "Review form"
// @Success 200 {string} string "Reviewed"
// @Bind user local key(__ctx_user)
// @Bind id path
// @Bind form body
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)
}

View File

@@ -305,6 +305,13 @@ func (r *Routes) Register(router fiber.Router) {
r.payoutAccounts.List,
Query[dto.SuperPayoutAccountListFilter]("filter"),
))
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,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
Body[dto.SuperPayoutAccountReviewForm]("form"),
))
// 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(

View File

@@ -1,6 +1,9 @@
package dto
import "quyun/v2/app/requests"
import (
"quyun/v2/app/requests"
"quyun/v2/pkg/consts"
)
type ApplyForm struct {
// Name 频道/创作者名称。
@@ -206,6 +209,14 @@ type PayoutAccount struct {
Account string `json:"account"`
// Realname 收款人姓名。
Realname string `json:"realname"`
// Status 审核状态pending/approved/rejected
Status consts.PayoutAccountStatus `json:"status"`
// StatusDescription 审核状态描述(用于展示)。
StatusDescription string `json:"status_description"`
// ReviewedAt 审核时间RFC3339
ReviewedAt string `json:"reviewed_at"`
// ReviewReason 审核说明/驳回原因。
ReviewReason string `json:"review_reason"`
}
type WithdrawForm struct {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"quyun/v2/app/errorx"
@@ -864,7 +865,8 @@ func (s *creator) ListPayoutAccounts(ctx context.Context, tenantID, userID int64
return nil, err
}
list, err := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.TenantID.Eq(tid)).Find()
tbl, q := models.PayoutAccountQuery.QueryContext(ctx)
list, err := q.Where(tbl.TenantID.Eq(tid)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
@@ -872,11 +874,15 @@ func (s *creator) ListPayoutAccounts(ctx context.Context, tenantID, userID int64
var data []creator_dto.PayoutAccount
for _, v := range list {
data = append(data, creator_dto.PayoutAccount{
ID: v.ID,
Type: v.Type,
Name: v.Name,
Account: v.Account,
Realname: v.Realname,
ID: v.ID,
Type: string(v.Type),
Name: v.Name,
Account: v.Account,
Realname: v.Realname,
Status: v.Status,
StatusDescription: v.Status.Description(),
ReviewedAt: s.formatTime(v.ReviewedAt),
ReviewReason: v.ReviewReason,
})
}
return data, nil
@@ -892,10 +898,11 @@ func (s *creator) AddPayoutAccount(ctx context.Context, tenantID, userID int64,
pa := &models.PayoutAccount{
TenantID: tid,
UserID: uid,
Type: form.Type,
Type: consts.PayoutAccountType(form.Type),
Name: form.Name,
Account: form.Account,
Realname: form.Realname,
Status: consts.PayoutAccountStatusPending,
}
if err := models.PayoutAccountQuery.WithContext(ctx).Create(pa); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
@@ -909,9 +916,8 @@ func (s *creator) RemovePayoutAccount(ctx context.Context, tenantID, userID, id
return err
}
_, err = models.PayoutAccountQuery.WithContext(ctx).
Where(models.PayoutAccountQuery.ID.Eq(id), models.PayoutAccountQuery.TenantID.Eq(tid)).
Delete()
tbl, q := models.PayoutAccountQuery.QueryContext(ctx)
_, err = q.Where(tbl.ID.Eq(id), tbl.TenantID.Eq(tid)).Delete()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
@@ -925,7 +931,7 @@ func (s *creator) Withdraw(ctx context.Context, tenantID, userID int64, form *cr
}
uid := userID
// Validate User Real-name Status
// 校验用户实名认证状态,未通过不允许提现。
user, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
@@ -939,19 +945,25 @@ func (s *creator) Withdraw(ctx context.Context, tenantID, userID int64, form *cr
return errorx.ErrBadRequest.WithMsg("金额无效")
}
// Validate Payout Account
account, err := models.PayoutAccountQuery.WithContext(ctx).
Where(models.PayoutAccountQuery.ID.Eq(form.AccountID), models.PayoutAccountQuery.TenantID.Eq(tid)).
First()
// 校验收款账户可用性与审核状态。
tbl, q := models.PayoutAccountQuery.QueryContext(ctx)
account, err := q.Where(tbl.ID.Eq(form.AccountID), tbl.TenantID.Eq(tid)).First()
if err != nil {
return errorx.ErrRecordNotFound.WithMsg("收款账户不存在")
}
if account.Status != consts.PayoutAccountStatusApproved {
reason := strings.TrimSpace(account.ReviewReason)
if account.Status == consts.PayoutAccountStatusRejected && reason != "" {
return errorx.ErrPreconditionFailed.WithMsg("收款账户审核未通过:" + reason)
}
return errorx.ErrPreconditionFailed.WithMsg("收款账户未审核通过")
}
// 将收款账户快照写入订单,便于超管审核与打款核对。
snapshotPayload, err := json.Marshal(fields.OrdersWithdrawalSnapshot{
Method: form.Method,
AccountID: account.ID,
AccountType: account.Type,
AccountType: string(account.Type),
AccountName: account.Name,
Account: account.Account,
AccountRealname: account.Realname,
@@ -1032,6 +1044,13 @@ func (s *creator) getTenantID(ctx context.Context, tenantID, userID int64) (int6
return t.ID, nil
}
func (s *creator) formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.RFC3339)
}
func (s *creator) validateContentAssets(
ctx context.Context,
tx *models.Query,

View File

@@ -247,6 +247,7 @@ func (s *CreatorTestSuite) Test_PayoutAccount() {
So(err, ShouldBeNil)
So(len(list), ShouldEqual, 1)
So(list[0].Account, ShouldEqual, "user@example.com")
So(list[0].Status, ShouldEqual, consts.PayoutAccountStatusPending)
// Remove
err = Creator.RemovePayoutAccount(ctx, tenantID, u.ID, list[0].ID)
@@ -285,10 +286,11 @@ func (s *CreatorTestSuite) Test_Withdraw() {
pa := &models.PayoutAccount{
TenantID: t.ID,
UserID: u.ID,
Type: "bank",
Type: consts.PayoutAccountTypeBank,
Name: "Bank",
Account: "123",
Realname: "Creator",
Status: consts.PayoutAccountStatusApproved,
}
models.PayoutAccountQuery.WithContext(ctx).Create(pa)
@@ -316,7 +318,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
var snap fields.OrdersWithdrawalSnapshot
So(json.Unmarshal(o.Snapshot.Data().Data, &snap), ShouldBeNil)
So(snap.AccountID, ShouldEqual, pa.ID)
So(snap.AccountType, ShouldEqual, pa.Type)
So(snap.AccountType, ShouldEqual, string(pa.Type))
So(snap.AccountName, ShouldEqual, pa.Name)
So(snap.Account, ShouldEqual, pa.Account)
So(snap.AccountRealname, ShouldEqual, pa.Realname)

View File

@@ -14,6 +14,7 @@ import (
super_dto "quyun/v2/app/http/super/v1/dto"
v1_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/database/fields"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
jwt_provider "quyun/v2/providers/jwt"
@@ -1150,7 +1151,10 @@ func (s *super) ListPayoutAccounts(ctx context.Context, filter *super_dto.SuperP
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)))
q = q.Where(tbl.Type.Eq(consts.PayoutAccountType(strings.TrimSpace(*filter.Type))))
}
if filter.Status != nil && *filter.Status != "" {
q = q.Where(tbl.Status.Eq(*filter.Status))
}
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
@@ -1273,18 +1277,23 @@ func (s *super) ListPayoutAccounts(ctx context.Context, filter *super_dto.SuperP
}
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),
ID: pa.ID,
TenantID: pa.TenantID,
TenantCode: tenantCode,
TenantName: tenantName,
UserID: pa.UserID,
Username: username,
Type: string(pa.Type),
Name: pa.Name,
Account: pa.Account,
Realname: pa.Realname,
Status: pa.Status,
StatusDescription: pa.Status.Description(),
ReviewedBy: pa.ReviewedBy,
ReviewedAt: s.formatTime(pa.ReviewedAt),
ReviewReason: pa.ReviewReason,
CreatedAt: s.formatTime(pa.CreatedAt),
UpdatedAt: s.formatTime(pa.UpdatedAt),
})
}
@@ -1322,6 +1331,84 @@ func (s *super) RemovePayoutAccount(ctx context.Context, operatorID, id int64) e
return nil
}
func (s *super) ReviewPayoutAccount(ctx context.Context, operatorID, id int64, form *super_dto.SuperPayoutAccountReviewForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if id == 0 {
return errorx.ErrBadRequest.WithMsg("结算账户ID不能为空")
}
if form == nil {
return errorx.ErrBadRequest.WithMsg("审核参数不能为空")
}
action := strings.ToLower(strings.TrimSpace(form.Action))
if action != "approve" && action != "reject" {
return errorx.ErrBadRequest.WithMsg("审核动作非法")
}
reason := strings.TrimSpace(form.Reason)
if action == "reject" && reason == "" {
return errorx.ErrBadRequest.WithMsg("驳回原因不能为空")
}
nextStatus := consts.PayoutAccountStatusApproved
if action == "reject" {
nextStatus = consts.PayoutAccountStatusRejected
}
var account *models.PayoutAccount
// 事务内校验状态并写入审核结果,避免并发重复审核。
err := models.Q.Transaction(func(tx *models.Query) error {
tbl, q := tx.PayoutAccount.QueryContext(ctx)
existing, 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 existing.Status != consts.PayoutAccountStatusPending {
return errorx.ErrStatusConflict.WithMsg("结算账户已完成审核")
}
now := time.Now()
updates := map[string]interface{}{
"status": nextStatus,
"reviewed_by": operatorID,
"reviewed_at": now,
"review_reason": reason,
"updated_at": now,
}
if action == "approve" {
updates["review_reason"] = ""
}
if _, err := q.Where(tbl.ID.Eq(existing.ID)).Updates(updates); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
account = existing
return nil
})
if err != nil {
return err
}
title := "结算账户审核结果"
detail := "结算账户审核通过"
if action == "reject" {
detail = "结算账户审核驳回"
if reason != "" {
detail += ",原因:" + reason
}
}
if Notification != nil && account != nil {
_ = Notification.Send(ctx, account.TenantID, account.UserID, string(consts.NotificationTypeAudit), title, detail)
}
if Audit != nil && account != nil {
Audit.Log(ctx, account.TenantID, operatorID, "review_payout_account", cast.ToString(account.ID), detail)
}
return nil
}
func (s *super) ListTenantJoinRequests(ctx context.Context, filter *super_dto.SuperTenantJoinRequestListFilter) (*requests.Pager, error) {
if filter == nil {
filter = &super_dto.SuperTenantJoinRequestListFilter{}
@@ -8008,6 +8095,26 @@ func (s *super) ApproveWithdrawal(ctx context.Context, operatorID, id int64) err
return errorx.ErrStatusConflict.WithMsg("订单状态不正确")
}
snapshot := o.Snapshot.Data()
if snapshot.Kind == "withdrawal" && len(snapshot.Data) > 0 {
var snap fields.OrdersWithdrawalSnapshot
if err := json.Unmarshal(snapshot.Data, &snap); err != nil {
return errorx.ErrInternalError.WithCause(err).WithMsg("解析提现快照失败")
}
if snap.AccountID > 0 {
account, err := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.ID.Eq(snap.AccountID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("收款账户不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
if account.Status != consts.PayoutAccountStatusApproved {
return errorx.ErrPreconditionFailed.WithMsg("收款账户未审核通过")
}
}
}
// Mark as Paid (Assumes external transfer done)
_, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).Updates(&models.Order{
Status: consts.OrderStatusPaid,

View File

@@ -733,3 +733,67 @@ func (s *SuperTestSuite) Test_OrderGovernance() {
})
})
}
func (s *SuperTestSuite) Test_PayoutAccountReview() {
Convey("PayoutAccountReview", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNamePayoutAccount, models.TableNameUser, models.TableNameTenant)
admin := &models.User{Username: "payout_admin"}
owner := &models.User{Username: "payout_owner"}
models.UserQuery.WithContext(ctx).Create(admin, owner)
tenant := &models.Tenant{
UserID: owner.ID,
Name: "Payout Tenant",
Code: "payout",
Status: consts.TenantStatusVerified,
}
models.TenantQuery.WithContext(ctx).Create(tenant)
account := &models.PayoutAccount{
TenantID: tenant.ID,
UserID: owner.ID,
Type: consts.PayoutAccountTypeBank,
Name: "Bank",
Account: "123",
Realname: "Owner",
Status: consts.PayoutAccountStatusPending,
}
models.PayoutAccountQuery.WithContext(ctx).Create(account)
Convey("should approve payout account", func() {
err := Super.ReviewPayoutAccount(ctx, admin.ID, account.ID, &super_dto.SuperPayoutAccountReviewForm{
Action: "approve",
})
So(err, ShouldBeNil)
reloaded, _ := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.ID.Eq(account.ID)).First()
So(reloaded.Status, ShouldEqual, consts.PayoutAccountStatusApproved)
So(reloaded.ReviewedBy, ShouldEqual, admin.ID)
So(reloaded.ReviewedAt.IsZero(), ShouldBeFalse)
})
Convey("should require reason when rejecting", func() {
account2 := &models.PayoutAccount{
TenantID: tenant.ID,
UserID: owner.ID,
Type: consts.PayoutAccountTypeAlipay,
Name: "Alipay",
Account: "user@example.com",
Realname: "Owner",
Status: consts.PayoutAccountStatusPending,
}
models.PayoutAccountQuery.WithContext(ctx).Create(account2)
err := Super.ReviewPayoutAccount(ctx, admin.ID, account2.ID, &super_dto.SuperPayoutAccountReviewForm{
Action: "reject",
})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrBadRequest.Code)
})
})
}

View File

@@ -52,6 +52,9 @@ field_type:
type: consts.CouponType
user_coupons:
status: consts.UserCouponStatus
payout_accounts:
type: consts.PayoutAccountType
status: consts.PayoutAccountStatus
notification_templates:
type: consts.NotificationType
field_relate:

View File

@@ -0,0 +1,33 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE payout_accounts
ADD COLUMN IF NOT EXISTS status VARCHAR(32) NOT NULL DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS reviewed_by BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS review_reason VARCHAR(255) NOT NULL DEFAULT '';
COMMENT ON COLUMN payout_accounts.status IS '结算账户审核状态;用途:控制提现账户可用性;默认 pending。';
COMMENT ON COLUMN payout_accounts.reviewed_by IS '结算账户审核操作者ID用途审计追踪默认 0 表示未审核。';
COMMENT ON COLUMN payout_accounts.reviewed_at IS '结算账户审核时间;用途:记录审核完成时间;未审核为空。';
COMMENT ON COLUMN payout_accounts.review_reason IS '结算账户审核说明;用途:驳回原因或备注;默认空字符串。';
-- 历史数据默认视为已审核通过,避免新增流程影响既有提现。
UPDATE payout_accounts
SET status = 'approved'
WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS payout_accounts_status_idx ON payout_accounts(status);
CREATE INDEX IF NOT EXISTS payout_accounts_reviewed_at_idx ON payout_accounts(reviewed_at);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS payout_accounts_reviewed_at_idx;
DROP INDEX IF EXISTS payout_accounts_status_idx;
ALTER TABLE payout_accounts
DROP COLUMN IF EXISTS review_reason,
DROP COLUMN IF EXISTS reviewed_at,
DROP COLUMN IF EXISTS reviewed_by,
DROP COLUMN IF EXISTS status;
-- +goose StatementEnd

View File

@@ -40,9 +40,9 @@ type Content struct {
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"`
Key string `gorm:"column:key;type:character varying(32);comment:Musical key/tone" json:"key"` // Musical key/tone
IsPinned bool `gorm:"column:is_pinned;type:boolean;comment:Whether content is pinned/featured" json:"is_pinned"` // Whether content is pinned/featured
Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"`
Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"`
ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,omitempty"`
Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"`
}
// Quick operations without importing query package

View File

@@ -46,12 +46,6 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
_contentQuery.DeletedAt = field.NewField(tableName, "deleted_at")
_contentQuery.Key = field.NewString(tableName, "key")
_contentQuery.IsPinned = field.NewBool(tableName, "is_pinned")
_contentQuery.Comments = contentQueryHasManyComments{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Comments", "Comment"),
}
_contentQuery.Author = contentQueryBelongsToAuthor{
db: db.Session(&gorm.Session{}),
@@ -64,6 +58,12 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
RelationField: field.NewRelation("ContentAssets", "ContentAsset"),
}
_contentQuery.Comments = contentQueryHasManyComments{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Comments", "Comment"),
}
_contentQuery.fillFieldMap()
return _contentQuery
@@ -94,12 +94,12 @@ type contentQuery struct {
DeletedAt field.Field
Key field.String // Musical key/tone
IsPinned field.Bool // Whether content is pinned/featured
Comments contentQueryHasManyComments
Author contentQueryBelongsToAuthor
Author contentQueryBelongsToAuthor
ContentAssets contentQueryHasManyContentAssets
Comments contentQueryHasManyComments
fieldMap map[string]field.Expr
}
@@ -195,104 +195,23 @@ func (c *contentQuery) fillFieldMap() {
func (c contentQuery) clone(db *gorm.DB) contentQuery {
c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool)
c.Comments.db = db.Session(&gorm.Session{Initialized: true})
c.Comments.db.Statement.ConnPool = db.Statement.ConnPool
c.Author.db = db.Session(&gorm.Session{Initialized: true})
c.Author.db.Statement.ConnPool = db.Statement.ConnPool
c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true})
c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool
c.Comments.db = db.Session(&gorm.Session{Initialized: true})
c.Comments.db.Statement.ConnPool = db.Statement.ConnPool
return c
}
func (c contentQuery) replaceDB(db *gorm.DB) contentQuery {
c.contentQueryDo.ReplaceDB(db)
c.Comments.db = db.Session(&gorm.Session{})
c.Author.db = db.Session(&gorm.Session{})
c.ContentAssets.db = db.Session(&gorm.Session{})
c.Comments.db = db.Session(&gorm.Session{})
return c
}
type contentQueryHasManyComments struct {
db *gorm.DB
field.RelationField
}
func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments {
a.db = a.db.WithContext(ctx)
return &a
}
func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments {
a.db = a.db.Session(session)
return &a
}
func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx {
return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())}
}
func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments {
a.db = a.db.Unscoped()
return &a
}
type contentQueryHasManyCommentsTx struct{ tx *gorm.Association }
func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) {
return result, a.tx.Find(&result)
}
func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Clear() error {
return a.tx.Clear()
}
func (a contentQueryHasManyCommentsTx) Count() int64 {
return a.tx.Count()
}
func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx {
a.tx = a.tx.Unscoped()
return &a
}
type contentQueryBelongsToAuthor struct {
db *gorm.DB
@@ -455,6 +374,87 @@ func (a contentQueryHasManyContentAssetsTx) Unscoped() *contentQueryHasManyConte
return &a
}
type contentQueryHasManyComments struct {
db *gorm.DB
field.RelationField
}
func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments {
a.db = a.db.WithContext(ctx)
return &a
}
func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments {
a.db = a.db.Session(session)
return &a
}
func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx {
return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())}
}
func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments {
a.db = a.db.Unscoped()
return &a
}
type contentQueryHasManyCommentsTx struct{ tx *gorm.Association }
func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) {
return result, a.tx.Find(&result)
}
func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a contentQueryHasManyCommentsTx) Clear() error {
return a.tx.Clear()
}
func (a contentQueryHasManyCommentsTx) Count() int64 {
return a.tx.Count()
}
func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx {
a.tx = a.tx.Unscoped()
return &a
}
type contentQueryDo struct{ gen.DO }
func (c contentQueryDo) Debug() *contentQueryDo {

View File

@@ -8,6 +8,8 @@ import (
"context"
"time"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen"
)
@@ -15,15 +17,19 @@ const TableNamePayoutAccount = "payout_accounts"
// PayoutAccount mapped from table <payout_accounts>
type PayoutAccount struct {
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Type string `gorm:"column:type;type:character varying(32);not null" json:"type"`
Name string `gorm:"column:name;type:character varying(128);not null" json:"name"`
Account string `gorm:"column:account;type:character varying(128);not null" json:"account"`
Realname string `gorm:"column:realname;type:character varying(128);not null" json:"realname"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"`
UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"`
Type consts.PayoutAccountType `gorm:"column:type;type:character varying(32);not null" json:"type"`
Name string `gorm:"column:name;type:character varying(128);not null" json:"name"`
Account string `gorm:"column:account;type:character varying(128);not null" json:"account"`
Realname string `gorm:"column:realname;type:character varying(128);not null" json:"realname"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"`
Status consts.PayoutAccountStatus `gorm:"column:status;type:character varying(32);not null;default:pending;comment:结算账户审核状态;用途:控制提现账户可用性;默认 pending。" json:"status"` // 结算账户审核状态;用途:控制提现账户可用性;默认 pending。
ReviewedBy int64 `gorm:"column:reviewed_by;type:bigint;not null;comment:结算账户审核操作者ID用途审计追踪默认 0 表示未审核。" json:"reviewed_by"` // 结算账户审核操作者ID用途审计追踪默认 0 表示未审核。
ReviewedAt time.Time `gorm:"column:reviewed_at;type:timestamp with time zone;comment:结算账户审核时间;用途:记录审核完成时间;未审核为空。" json:"reviewed_at"` // 结算账户审核时间;用途:记录审核完成时间;未审核为空。
ReviewReason string `gorm:"column:review_reason;type:character varying(255);not null;comment:结算账户审核说明;用途:驳回原因或备注;默认空字符串。" json:"review_reason"` // 结算账户审核说明;用途:驳回原因或备注;默认空字符串。
}
// Quick operations without importing query package

View File

@@ -28,12 +28,16 @@ func newPayoutAccount(db *gorm.DB, opts ...gen.DOOption) payoutAccountQuery {
_payoutAccountQuery.ID = field.NewInt64(tableName, "id")
_payoutAccountQuery.TenantID = field.NewInt64(tableName, "tenant_id")
_payoutAccountQuery.UserID = field.NewInt64(tableName, "user_id")
_payoutAccountQuery.Type = field.NewString(tableName, "type")
_payoutAccountQuery.Type = field.NewField(tableName, "type")
_payoutAccountQuery.Name = field.NewString(tableName, "name")
_payoutAccountQuery.Account = field.NewString(tableName, "account")
_payoutAccountQuery.Realname = field.NewString(tableName, "realname")
_payoutAccountQuery.CreatedAt = field.NewTime(tableName, "created_at")
_payoutAccountQuery.UpdatedAt = field.NewTime(tableName, "updated_at")
_payoutAccountQuery.Status = field.NewField(tableName, "status")
_payoutAccountQuery.ReviewedBy = field.NewInt64(tableName, "reviewed_by")
_payoutAccountQuery.ReviewedAt = field.NewTime(tableName, "reviewed_at")
_payoutAccountQuery.ReviewReason = field.NewString(tableName, "review_reason")
_payoutAccountQuery.fillFieldMap()
@@ -43,16 +47,20 @@ func newPayoutAccount(db *gorm.DB, opts ...gen.DOOption) payoutAccountQuery {
type payoutAccountQuery struct {
payoutAccountQueryDo payoutAccountQueryDo
ALL field.Asterisk
ID field.Int64
TenantID field.Int64
UserID field.Int64
Type field.String
Name field.String
Account field.String
Realname field.String
CreatedAt field.Time
UpdatedAt field.Time
ALL field.Asterisk
ID field.Int64
TenantID field.Int64
UserID field.Int64
Type field.Field
Name field.String
Account field.String
Realname field.String
CreatedAt field.Time
UpdatedAt field.Time
Status field.Field // 结算账户审核状态;用途:控制提现账户可用性;默认 pending。
ReviewedBy field.Int64 // 结算账户审核操作者ID用途审计追踪默认 0 表示未审核。
ReviewedAt field.Time // 结算账户审核时间;用途:记录审核完成时间;未审核为空。
ReviewReason field.String // 结算账户审核说明;用途:驳回原因或备注;默认空字符串。
fieldMap map[string]field.Expr
}
@@ -72,12 +80,16 @@ func (p *payoutAccountQuery) updateTableName(table string) *payoutAccountQuery {
p.ID = field.NewInt64(table, "id")
p.TenantID = field.NewInt64(table, "tenant_id")
p.UserID = field.NewInt64(table, "user_id")
p.Type = field.NewString(table, "type")
p.Type = field.NewField(table, "type")
p.Name = field.NewString(table, "name")
p.Account = field.NewString(table, "account")
p.Realname = field.NewString(table, "realname")
p.CreatedAt = field.NewTime(table, "created_at")
p.UpdatedAt = field.NewTime(table, "updated_at")
p.Status = field.NewField(table, "status")
p.ReviewedBy = field.NewInt64(table, "reviewed_by")
p.ReviewedAt = field.NewTime(table, "reviewed_at")
p.ReviewReason = field.NewString(table, "review_reason")
p.fillFieldMap()
@@ -110,7 +122,7 @@ func (p *payoutAccountQuery) GetFieldByName(fieldName string) (field.OrderExpr,
}
func (p *payoutAccountQuery) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 9)
p.fieldMap = make(map[string]field.Expr, 13)
p.fieldMap["id"] = p.ID
p.fieldMap["tenant_id"] = p.TenantID
p.fieldMap["user_id"] = p.UserID
@@ -120,6 +132,10 @@ func (p *payoutAccountQuery) fillFieldMap() {
p.fieldMap["realname"] = p.Realname
p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
p.fieldMap["status"] = p.Status
p.fieldMap["reviewed_by"] = p.ReviewedBy
p.fieldMap["reviewed_at"] = p.ReviewedAt
p.fieldMap["review_reason"] = p.ReviewReason
}
func (p payoutAccountQuery) clone(db *gorm.DB) payoutAccountQuery {

View File

@@ -1594,6 +1594,48 @@ const docTemplate = `{
}
}
},
"/super/v1/payout-accounts/{id}/review": {
"post": {
"description": "Review payout account across tenants",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Finance"
],
"summary": "Review payout account",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Payout account ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Review form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperPayoutAccountReviewForm"
}
}
],
"responses": {
"200": {
"description": "Reviewed",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/reports/export": {
"post": {
"description": "Export platform report data",
@@ -6278,6 +6320,19 @@ const docTemplate = `{
"OrderTypeWithdrawal"
]
},
"consts.PayoutAccountStatus": {
"type": "string",
"enum": [
"pending",
"approved",
"rejected"
],
"x-enum-varnames": [
"PayoutAccountStatusPending",
"PayoutAccountStatusApproved",
"PayoutAccountStatusRejected"
]
},
"consts.Role": {
"type": "string",
"enum": [
@@ -7432,6 +7487,26 @@ const docTemplate = `{
"description": "Realname 收款人姓名。",
"type": "string"
},
"review_reason": {
"description": "ReviewReason 审核说明/驳回原因。",
"type": "string"
},
"reviewed_at": {
"description": "ReviewedAt 审核时间RFC3339。",
"type": "string"
},
"status": {
"description": "Status 审核状态pending/approved/rejected。",
"allOf": [
{
"$ref": "#/definitions/consts.PayoutAccountStatus"
}
]
},
"status_description": {
"description": "StatusDescription 审核状态描述(用于展示)。",
"type": "string"
},
"type": {
"description": "Type 账户类型bank/alipay。",
"type": "string"
@@ -9094,6 +9169,30 @@ const docTemplate = `{
"description": "Realname 收款人姓名。",
"type": "string"
},
"review_reason": {
"description": "ReviewReason 审核说明/驳回原因。",
"type": "string"
},
"reviewed_at": {
"description": "ReviewedAt 审核时间RFC3339。",
"type": "string"
},
"reviewed_by": {
"description": "ReviewedBy 审核操作者ID。",
"type": "integer"
},
"status": {
"description": "Status 审核状态。",
"allOf": [
{
"$ref": "#/definitions/consts.PayoutAccountStatus"
}
]
},
"status_description": {
"description": "StatusDescription 审核状态描述(用于展示)。",
"type": "string"
},
"tenant_code": {
"description": "TenantCode 租户编码。",
"type": "string"
@@ -9124,6 +9223,26 @@ const docTemplate = `{
}
}
},
"dto.SuperPayoutAccountReviewForm": {
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"description": "Action 审核动作approve/reject。",
"type": "string",
"enum": [
"approve",
"reject"
]
},
"reason": {
"description": "Reason 审核说明(驳回时必填)。",
"type": "string"
}
}
},
"dto.SuperReportExportForm": {
"type": "object",
"properties": {

View File

@@ -1588,6 +1588,48 @@
}
}
},
"/super/v1/payout-accounts/{id}/review": {
"post": {
"description": "Review payout account across tenants",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Finance"
],
"summary": "Review payout account",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Payout account ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Review form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperPayoutAccountReviewForm"
}
}
],
"responses": {
"200": {
"description": "Reviewed",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/reports/export": {
"post": {
"description": "Export platform report data",
@@ -6272,6 +6314,19 @@
"OrderTypeWithdrawal"
]
},
"consts.PayoutAccountStatus": {
"type": "string",
"enum": [
"pending",
"approved",
"rejected"
],
"x-enum-varnames": [
"PayoutAccountStatusPending",
"PayoutAccountStatusApproved",
"PayoutAccountStatusRejected"
]
},
"consts.Role": {
"type": "string",
"enum": [
@@ -7426,6 +7481,26 @@
"description": "Realname 收款人姓名。",
"type": "string"
},
"review_reason": {
"description": "ReviewReason 审核说明/驳回原因。",
"type": "string"
},
"reviewed_at": {
"description": "ReviewedAt 审核时间RFC3339。",
"type": "string"
},
"status": {
"description": "Status 审核状态pending/approved/rejected。",
"allOf": [
{
"$ref": "#/definitions/consts.PayoutAccountStatus"
}
]
},
"status_description": {
"description": "StatusDescription 审核状态描述(用于展示)。",
"type": "string"
},
"type": {
"description": "Type 账户类型bank/alipay。",
"type": "string"
@@ -9088,6 +9163,30 @@
"description": "Realname 收款人姓名。",
"type": "string"
},
"review_reason": {
"description": "ReviewReason 审核说明/驳回原因。",
"type": "string"
},
"reviewed_at": {
"description": "ReviewedAt 审核时间RFC3339。",
"type": "string"
},
"reviewed_by": {
"description": "ReviewedBy 审核操作者ID。",
"type": "integer"
},
"status": {
"description": "Status 审核状态。",
"allOf": [
{
"$ref": "#/definitions/consts.PayoutAccountStatus"
}
]
},
"status_description": {
"description": "StatusDescription 审核状态描述(用于展示)。",
"type": "string"
},
"tenant_code": {
"description": "TenantCode 租户编码。",
"type": "string"
@@ -9118,6 +9217,26 @@
}
}
},
"dto.SuperPayoutAccountReviewForm": {
"type": "object",
"required": [
"action"
],
"properties": {
"action": {
"description": "Action 审核动作approve/reject。",
"type": "string",
"enum": [
"approve",
"reject"
]
},
"reason": {
"description": "Reason 审核说明(驳回时必填)。",
"type": "string"
}
}
},
"dto.SuperReportExportForm": {
"type": "object",
"properties": {

View File

@@ -118,6 +118,16 @@ definitions:
- OrderTypeContentPurchase
- OrderTypeRecharge
- OrderTypeWithdrawal
consts.PayoutAccountStatus:
enum:
- pending
- approved
- rejected
type: string
x-enum-varnames:
- PayoutAccountStatusPending
- PayoutAccountStatusApproved
- PayoutAccountStatusRejected
consts.Role:
enum:
- user
@@ -940,6 +950,19 @@ definitions:
realname:
description: Realname 收款人姓名。
type: string
review_reason:
description: ReviewReason 审核说明/驳回原因。
type: string
reviewed_at:
description: ReviewedAt 审核时间RFC3339
type: string
status:
allOf:
- $ref: '#/definitions/consts.PayoutAccountStatus'
description: Status 审核状态pending/approved/rejected
status_description:
description: StatusDescription 审核状态描述(用于展示)。
type: string
type:
description: Type 账户类型bank/alipay
type: string
@@ -2100,6 +2123,22 @@ definitions:
realname:
description: Realname 收款人姓名。
type: string
review_reason:
description: ReviewReason 审核说明/驳回原因。
type: string
reviewed_at:
description: ReviewedAt 审核时间RFC3339
type: string
reviewed_by:
description: ReviewedBy 审核操作者ID。
type: integer
status:
allOf:
- $ref: '#/definitions/consts.PayoutAccountStatus'
description: Status 审核状态。
status_description:
description: StatusDescription 审核状态描述(用于展示)。
type: string
tenant_code:
description: TenantCode 租户编码。
type: string
@@ -2122,6 +2161,20 @@ definitions:
description: Username 用户名。
type: string
type: object
dto.SuperPayoutAccountReviewForm:
properties:
action:
description: Action 审核动作approve/reject
enum:
- approve
- reject
type: string
reason:
description: Reason 审核说明(驳回时必填)。
type: string
required:
- action
type: object
dto.SuperReportExportForm:
properties:
end_at:
@@ -4266,6 +4319,34 @@ paths:
summary: Remove payout account
tags:
- Finance
/super/v1/payout-accounts/{id}/review:
post:
consumes:
- application/json
description: Review payout account across tenants
parameters:
- description: Payout account ID
format: int64
in: path
name: id
required: true
type: integer
- description: Review form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperPayoutAccountReviewForm'
produces:
- application/json
responses:
"200":
description: Reviewed
schema:
type: string
summary: Review payout account
tags:
- Finance
/super/v1/reports/export:
post:
consumes:

View File

@@ -0,0 +1,179 @@
// Code generated by go-enum DO NOT EDIT.
// Version: -
// Revision: -
// Build Date: -
// Built By: -
package consts
import (
"database/sql/driver"
"errors"
"fmt"
"strings"
)
const (
// PayoutAccountStatusPending is a PayoutAccountStatus of type pending.
PayoutAccountStatusPending PayoutAccountStatus = "pending"
// PayoutAccountStatusApproved is a PayoutAccountStatus of type approved.
PayoutAccountStatusApproved PayoutAccountStatus = "approved"
// PayoutAccountStatusRejected is a PayoutAccountStatus of type rejected.
PayoutAccountStatusRejected PayoutAccountStatus = "rejected"
)
var ErrInvalidPayoutAccountStatus = fmt.Errorf("not a valid PayoutAccountStatus, try [%s]", strings.Join(_PayoutAccountStatusNames, ", "))
var _PayoutAccountStatusNames = []string{
string(PayoutAccountStatusPending),
string(PayoutAccountStatusApproved),
string(PayoutAccountStatusRejected),
}
// PayoutAccountStatusNames returns a list of possible string values of PayoutAccountStatus.
func PayoutAccountStatusNames() []string {
tmp := make([]string, len(_PayoutAccountStatusNames))
copy(tmp, _PayoutAccountStatusNames)
return tmp
}
// PayoutAccountStatusValues returns a list of the values for PayoutAccountStatus
func PayoutAccountStatusValues() []PayoutAccountStatus {
return []PayoutAccountStatus{
PayoutAccountStatusPending,
PayoutAccountStatusApproved,
PayoutAccountStatusRejected,
}
}
// String implements the Stringer interface.
func (x PayoutAccountStatus) String() string {
return string(x)
}
// IsValid provides a quick way to determine if the typed value is
// part of the allowed enumerated values
func (x PayoutAccountStatus) IsValid() bool {
_, err := ParsePayoutAccountStatus(string(x))
return err == nil
}
var _PayoutAccountStatusValue = map[string]PayoutAccountStatus{
"pending": PayoutAccountStatusPending,
"approved": PayoutAccountStatusApproved,
"rejected": PayoutAccountStatusRejected,
}
// ParsePayoutAccountStatus attempts to convert a string to a PayoutAccountStatus.
func ParsePayoutAccountStatus(name string) (PayoutAccountStatus, error) {
if x, ok := _PayoutAccountStatusValue[name]; ok {
return x, nil
}
return PayoutAccountStatus(""), fmt.Errorf("%s is %w", name, ErrInvalidPayoutAccountStatus)
}
var errPayoutAccountStatusNilPtr = errors.New("value pointer is nil") // one per type for package clashes
// Scan implements the Scanner interface.
func (x *PayoutAccountStatus) Scan(value interface{}) (err error) {
if value == nil {
*x = PayoutAccountStatus("")
return
}
// A wider range of scannable types.
// driver.Value values at the top of the list for expediency
switch v := value.(type) {
case string:
*x, err = ParsePayoutAccountStatus(v)
case []byte:
*x, err = ParsePayoutAccountStatus(string(v))
case PayoutAccountStatus:
*x = v
case *PayoutAccountStatus:
if v == nil {
return errPayoutAccountStatusNilPtr
}
*x = *v
case *string:
if v == nil {
return errPayoutAccountStatusNilPtr
}
*x, err = ParsePayoutAccountStatus(*v)
default:
return errors.New("invalid type for PayoutAccountStatus")
}
return
}
// Value implements the driver Valuer interface.
func (x PayoutAccountStatus) Value() (driver.Value, error) {
return x.String(), nil
}
// Set implements the Golang flag.Value interface func.
func (x *PayoutAccountStatus) Set(val string) error {
v, err := ParsePayoutAccountStatus(val)
*x = v
return err
}
// Get implements the Golang flag.Getter interface func.
func (x *PayoutAccountStatus) Get() interface{} {
return *x
}
// Type implements the github.com/spf13/pFlag Value interface.
func (x *PayoutAccountStatus) Type() string {
return "PayoutAccountStatus"
}
type NullPayoutAccountStatus struct {
PayoutAccountStatus PayoutAccountStatus
Valid bool
}
func NewNullPayoutAccountStatus(val interface{}) (x NullPayoutAccountStatus) {
err := x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
_ = err // make any errcheck linters happy
return
}
// Scan implements the Scanner interface.
func (x *NullPayoutAccountStatus) Scan(value interface{}) (err error) {
if value == nil {
x.PayoutAccountStatus, x.Valid = PayoutAccountStatus(""), false
return
}
err = x.PayoutAccountStatus.Scan(value)
x.Valid = (err == nil)
return
}
// Value implements the driver Valuer interface.
func (x NullPayoutAccountStatus) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
// driver.Value accepts int64 for int values.
return string(x.PayoutAccountStatus), nil
}
type NullPayoutAccountStatusStr struct {
NullPayoutAccountStatus
}
func NewNullPayoutAccountStatusStr(val interface{}) (x NullPayoutAccountStatusStr) {
x.Scan(val) // yes, we ignore this error, it will just be an invalid value.
return
}
// Value implements the driver Valuer interface.
func (x NullPayoutAccountStatusStr) Value() (driver.Value, error) {
if !x.Valid {
return nil, nil
}
return x.PayoutAccountStatus.String(), nil
}

View File

@@ -0,0 +1,29 @@
package consts
import "quyun/v2/app/requests"
// swagger:enum PayoutAccountStatus
// ENUM( pending, approved, rejected )
type PayoutAccountStatus string
func (t PayoutAccountStatus) Description() string {
switch t {
case PayoutAccountStatusPending:
return "待审核"
case PayoutAccountStatusApproved:
return "已通过"
case PayoutAccountStatusRejected:
return "已驳回"
default:
return "未知状态"
}
}
func PayoutAccountStatusItems() []requests.KV {
values := PayoutAccountStatusValues()
items := make([]requests.KV, 0, len(values))
for _, v := range values {
items = append(items, requests.NewKV(string(v), v.Description()))
}
return items
}

View File

@@ -110,7 +110,7 @@ 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 } = {}) {
async listPayoutAccounts({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, type, status, created_at_from, created_at_to } = {}) {
const iso = (d) => {
if (!d) return undefined;
const date = d instanceof Date ? d : new Date(d);
@@ -127,6 +127,7 @@ export const CreatorService = {
user_id,
username,
type,
status,
created_at_from: iso(created_at_from),
created_at_to: iso(created_at_to)
};
@@ -143,6 +144,16 @@ export const CreatorService = {
if (!accountID) throw new Error('accountID is required');
return requestJson(`/super/v1/payout-accounts/${accountID}`, { method: 'DELETE' });
},
async reviewPayoutAccount(accountID, { action, reason } = {}) {
if (!accountID) throw new Error('accountID is required');
return requestJson(`/super/v1/payout-accounts/${accountID}/review`, {
method: 'POST',
body: {
action,
reason
}
});
},
async createInvite(tenantID, { max_uses, expires_at, remark } = {}) {
if (!tenantID) throw new Error('tenantID is required');
return requestJson(`/super/v1/tenants/${tenantID}/invites`, {

View File

@@ -74,6 +74,7 @@ const payoutTenantName = ref('');
const payoutUserID = ref(null);
const payoutUsername = ref('');
const payoutType = ref('');
const payoutStatus = ref('');
const payoutCreatedAtFrom = ref(null);
const payoutCreatedAtTo = ref(null);
@@ -84,6 +85,18 @@ const joinStatusOptions = [
{ label: '已驳回', value: 'rejected' }
];
const payoutStatusOptions = [
{ label: '全部', value: '' },
{ label: '待审核', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已驳回', value: 'rejected' }
];
const payoutReviewOptions = [
{ label: '通过', value: 'approve' },
{ label: '驳回', value: 'reject' }
];
const reviewDialogVisible = ref(false);
const reviewSubmitting = ref(false);
const reviewAction = ref('approve');
@@ -100,6 +113,12 @@ const payoutRemoveDialogVisible = ref(false);
const payoutRemoveSubmitting = ref(false);
const payoutRemoveTarget = ref(null);
const payoutReviewDialogVisible = ref(false);
const payoutReviewSubmitting = ref(false);
const payoutReviewAction = ref('approve');
const payoutReviewReason = ref('');
const payoutReviewTarget = ref(null);
const inviteDialogVisible = ref(false);
const inviteSubmitting = ref(false);
const inviteTenantID = ref(null);
@@ -140,6 +159,19 @@ function getJoinStatusSeverity(value) {
}
}
function getPayoutStatusSeverity(value) {
switch (value) {
case 'pending':
return 'warn';
case 'approved':
return 'success';
case 'rejected':
return 'danger';
default:
return 'secondary';
}
}
async function ensureStatusOptionsLoaded() {
if (statusOptions.value.length > 0) return;
statusOptionsLoading.value = true;
@@ -240,6 +272,7 @@ async function loadPayoutAccounts() {
user_id: payoutUserID.value || undefined,
username: payoutUsername.value,
type: payoutType.value || undefined,
status: payoutStatus.value || undefined,
created_at_from: payoutCreatedAtFrom.value || undefined,
created_at_to: payoutCreatedAtTo.value || undefined
});
@@ -345,6 +378,7 @@ function onPayoutReset() {
payoutUserID.value = null;
payoutUsername.value = '';
payoutType.value = '';
payoutStatus.value = '';
payoutCreatedAtFrom.value = null;
payoutCreatedAtTo.value = null;
payoutAccountsPage.value = 1;
@@ -466,6 +500,35 @@ async function confirmRemovePayoutAccount() {
}
}
function openPayoutReviewDialog(row, action) {
payoutReviewTarget.value = row;
payoutReviewAction.value = action || 'approve';
payoutReviewReason.value = '';
payoutReviewDialogVisible.value = true;
}
async function confirmPayoutReview() {
const targetID = payoutReviewTarget.value?.id;
if (!targetID) return;
const reason = payoutReviewReason.value.trim();
if (payoutReviewAction.value === 'reject' && !reason) {
toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回时需填写原因', life: 3000 });
return;
}
payoutReviewSubmitting.value = true;
try {
await CreatorService.reviewPayoutAccount(targetID, { action: payoutReviewAction.value, reason });
toast.add({ severity: 'success', summary: '审核完成', detail: `账户ID: ${targetID}`, life: 3000 });
payoutReviewDialogVisible.value = false;
await loadPayoutAccounts();
} catch (error) {
toast.add({ severity: 'error', summary: '审核失败', detail: error?.message || '无法审核结算账户', life: 4000 });
} finally {
payoutReviewSubmitting.value = false;
}
}
function openInviteDialog(row) {
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
inviteMaxUses.value = 1;
@@ -826,6 +889,9 @@ onMounted(() => {
<SearchField label="类型">
<InputText v-model="payoutType" placeholder="bank/alipay" class="w-full" @keyup.enter="onPayoutSearch" />
</SearchField>
<SearchField label="状态">
<Select v-model="payoutStatus" :options="payoutStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
</SearchField>
<SearchField label="创建时间 From">
<DatePicker v-model="payoutCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
</SearchField>
@@ -873,6 +939,15 @@ onMounted(() => {
<Column field="name" header="账户名称" style="min-width: 14rem" />
<Column field="account" header="账号" style="min-width: 14rem" />
<Column field="realname" header="收款人" style="min-width: 12rem" />
<Column header="状态" style="min-width: 12rem">
<template #body="{ data }">
<div class="flex flex-col">
<Tag :value="data?.status_description || data?.status || '-'" :severity="getPayoutStatusSeverity(data?.status)" />
<span v-if="data?.review_reason" class="text-xs text-muted-color mt-1 truncate max-w-[220px]">原因{{ data.review_reason }}</span>
<span v-if="data?.reviewed_at" class="text-xs text-muted-color">审核时间{{ formatDate(data.reviewed_at) }}</span>
</div>
</template>
</Column>
<Column field="created_at" header="创建时间" style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.created_at) }}
@@ -880,7 +955,11 @@ onMounted(() => {
</Column>
<Column header="操作" style="min-width: 8rem">
<template #body="{ data }">
<Button label="删除" icon="pi pi-trash" severity="danger" text size="small" class="p-0" @click="openPayoutRemoveDialog(data)" />
<div class="flex flex-col gap-1">
<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)" />
</div>
</template>
</Column>
</DataTable>
@@ -1001,6 +1080,30 @@ onMounted(() => {
</template>
</Dialog>
<Dialog v-model:visible="payoutReviewDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">结算账户审核</span>
<span class="text-muted-color">账户ID: {{ payoutReviewTarget?.id ?? '-' }}</span>
</div>
</template>
<div class="flex flex-col gap-4">
<div class="text-sm text-muted-color">审核结算账户信息请确认处理动作与备注</div>
<div>
<label class="block font-medium mb-2">审核动作</label>
<Select v-model="payoutReviewAction" :options="payoutReviewOptions" optionLabel="label" optionValue="value" class="w-full" />
</div>
<div>
<label class="block font-medium mb-2">审核说明</label>
<InputText v-model="payoutReviewReason" placeholder="驳回时建议填写原因" class="w-full" />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="payoutReviewDialogVisible = false" :disabled="payoutReviewSubmitting" />
<Button label="确认审核" icon="pi pi-check" severity="success" @click="confirmPayoutReview" :loading="payoutReviewSubmitting" :disabled="payoutReviewSubmitting || (payoutReviewAction === 'reject' && !payoutReviewReason.trim())" />
</template>
</Dialog>
<Dialog v-model:visible="inviteDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">