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)
}
@@ -873,10 +875,14 @@ func (s *creator) ListPayoutAccounts(ctx context.Context, tenantID, userID int64
for _, v := range list {
data = append(data, creator_dto.PayoutAccount{
ID: v.ID,
Type: v.Type,
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)
@@ -1279,10 +1283,15 @@ func (s *super) ListPayoutAccounts(ctx context.Context, filter *super_dto.SuperP
TenantName: tenantName,
UserID: pa.UserID,
Username: username,
Type: pa.Type,
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
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"
)
@@ -18,12 +20,16 @@ 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"`
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()
@@ -47,12 +51,16 @@ type payoutAccountQuery struct {
ID field.Int64
TenantID field.Int64
UserID field.Int64
Type field.String
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">