feat: add payout account review flow
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
179
backend/pkg/consts/payout_account.gen.go
Normal file
179
backend/pkg/consts/payout_account.gen.go
Normal 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
|
||||
}
|
||||
29
backend/pkg/consts/payout_account.go
Normal file
29
backend/pkg/consts/payout_account.go
Normal 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
|
||||
}
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user