feat: add payout account review flow
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "quyun/v2/app/requests"
|
import (
|
||||||
|
"quyun/v2/app/requests"
|
||||||
|
"quyun/v2/pkg/consts"
|
||||||
|
)
|
||||||
|
|
||||||
// SuperPayoutAccountListFilter 超管结算账户列表过滤条件。
|
// SuperPayoutAccountListFilter 超管结算账户列表过滤条件。
|
||||||
type SuperPayoutAccountListFilter struct {
|
type SuperPayoutAccountListFilter struct {
|
||||||
@@ -17,6 +20,8 @@ type SuperPayoutAccountListFilter struct {
|
|||||||
Username *string `query:"username"`
|
Username *string `query:"username"`
|
||||||
// Type 账户类型过滤(bank/alipay)。
|
// Type 账户类型过滤(bank/alipay)。
|
||||||
Type *string `query:"type"`
|
Type *string `query:"type"`
|
||||||
|
// Status 审核状态过滤(pending/approved/rejected)。
|
||||||
|
Status *consts.PayoutAccountStatus `query:"status"`
|
||||||
// CreatedAtFrom 创建时间起始(RFC3339)。
|
// CreatedAtFrom 创建时间起始(RFC3339)。
|
||||||
CreatedAtFrom *string `query:"created_at_from"`
|
CreatedAtFrom *string `query:"created_at_from"`
|
||||||
// CreatedAtTo 创建时间结束(RFC3339)。
|
// CreatedAtTo 创建时间结束(RFC3339)。
|
||||||
@@ -45,8 +50,25 @@ type SuperPayoutAccountItem struct {
|
|||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
// Realname 收款人姓名。
|
// Realname 收款人姓名。
|
||||||
Realname string `json:"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 创建时间(RFC3339)。
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
// UpdatedAt 更新时间(RFC3339)。
|
// UpdatedAt 更新时间(RFC3339)。
|
||||||
UpdatedAt string `json:"updated_at"`
|
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 {
|
func (c *payoutAccounts) Remove(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||||
return services.Super.RemovePayoutAccount(ctx, user.ID, id)
|
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,
|
r.payoutAccounts.List,
|
||||||
Query[dto.SuperPayoutAccountListFilter]("filter"),
|
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
|
// Register routes for controller: reports
|
||||||
r.log.Debugf("Registering route: Get /super/v1/reports/overview -> reports.Overview")
|
r.log.Debugf("Registering route: Get /super/v1/reports/overview -> reports.Overview")
|
||||||
router.Get("/super/v1/reports/overview"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/reports/overview"[len(r.Path()):], DataFunc1(
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "quyun/v2/app/requests"
|
import (
|
||||||
|
"quyun/v2/app/requests"
|
||||||
|
"quyun/v2/pkg/consts"
|
||||||
|
)
|
||||||
|
|
||||||
type ApplyForm struct {
|
type ApplyForm struct {
|
||||||
// Name 频道/创作者名称。
|
// Name 频道/创作者名称。
|
||||||
@@ -206,6 +209,14 @@ type PayoutAccount struct {
|
|||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
// Realname 收款人姓名。
|
// Realname 收款人姓名。
|
||||||
Realname string `json:"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 {
|
type WithdrawForm struct {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
@@ -864,7 +865,8 @@ func (s *creator) ListPayoutAccounts(ctx context.Context, tenantID, userID int64
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
@@ -873,10 +875,14 @@ func (s *creator) ListPayoutAccounts(ctx context.Context, tenantID, userID int64
|
|||||||
for _, v := range list {
|
for _, v := range list {
|
||||||
data = append(data, creator_dto.PayoutAccount{
|
data = append(data, creator_dto.PayoutAccount{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
Type: v.Type,
|
Type: string(v.Type),
|
||||||
Name: v.Name,
|
Name: v.Name,
|
||||||
Account: v.Account,
|
Account: v.Account,
|
||||||
Realname: v.Realname,
|
Realname: v.Realname,
|
||||||
|
Status: v.Status,
|
||||||
|
StatusDescription: v.Status.Description(),
|
||||||
|
ReviewedAt: s.formatTime(v.ReviewedAt),
|
||||||
|
ReviewReason: v.ReviewReason,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
@@ -892,10 +898,11 @@ func (s *creator) AddPayoutAccount(ctx context.Context, tenantID, userID int64,
|
|||||||
pa := &models.PayoutAccount{
|
pa := &models.PayoutAccount{
|
||||||
TenantID: tid,
|
TenantID: tid,
|
||||||
UserID: uid,
|
UserID: uid,
|
||||||
Type: form.Type,
|
Type: consts.PayoutAccountType(form.Type),
|
||||||
Name: form.Name,
|
Name: form.Name,
|
||||||
Account: form.Account,
|
Account: form.Account,
|
||||||
Realname: form.Realname,
|
Realname: form.Realname,
|
||||||
|
Status: consts.PayoutAccountStatusPending,
|
||||||
}
|
}
|
||||||
if err := models.PayoutAccountQuery.WithContext(ctx).Create(pa); err != nil {
|
if err := models.PayoutAccountQuery.WithContext(ctx).Create(pa); err != nil {
|
||||||
return errorx.ErrDatabaseError.WithCause(err)
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
@@ -909,9 +916,8 @@ func (s *creator) RemovePayoutAccount(ctx context.Context, tenantID, userID, id
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = models.PayoutAccountQuery.WithContext(ctx).
|
tbl, q := models.PayoutAccountQuery.QueryContext(ctx)
|
||||||
Where(models.PayoutAccountQuery.ID.Eq(id), models.PayoutAccountQuery.TenantID.Eq(tid)).
|
_, err = q.Where(tbl.ID.Eq(id), tbl.TenantID.Eq(tid)).Delete()
|
||||||
Delete()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.ErrDatabaseError.WithCause(err)
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
@@ -925,7 +931,7 @@ func (s *creator) Withdraw(ctx context.Context, tenantID, userID int64, form *cr
|
|||||||
}
|
}
|
||||||
uid := userID
|
uid := userID
|
||||||
|
|
||||||
// Validate User Real-name Status
|
// 校验用户实名认证状态,未通过不允许提现。
|
||||||
user, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First()
|
user, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.ErrDatabaseError.WithCause(err)
|
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("金额无效")
|
return errorx.ErrBadRequest.WithMsg("金额无效")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Payout Account
|
// 校验收款账户可用性与审核状态。
|
||||||
account, err := models.PayoutAccountQuery.WithContext(ctx).
|
tbl, q := models.PayoutAccountQuery.QueryContext(ctx)
|
||||||
Where(models.PayoutAccountQuery.ID.Eq(form.AccountID), models.PayoutAccountQuery.TenantID.Eq(tid)).
|
account, err := q.Where(tbl.ID.Eq(form.AccountID), tbl.TenantID.Eq(tid)).First()
|
||||||
First()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.ErrRecordNotFound.WithMsg("收款账户不存在")
|
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{
|
snapshotPayload, err := json.Marshal(fields.OrdersWithdrawalSnapshot{
|
||||||
Method: form.Method,
|
Method: form.Method,
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
AccountType: account.Type,
|
AccountType: string(account.Type),
|
||||||
AccountName: account.Name,
|
AccountName: account.Name,
|
||||||
Account: account.Account,
|
Account: account.Account,
|
||||||
AccountRealname: account.Realname,
|
AccountRealname: account.Realname,
|
||||||
@@ -1032,6 +1044,13 @@ func (s *creator) getTenantID(ctx context.Context, tenantID, userID int64) (int6
|
|||||||
return t.ID, nil
|
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(
|
func (s *creator) validateContentAssets(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx *models.Query,
|
tx *models.Query,
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ func (s *CreatorTestSuite) Test_PayoutAccount() {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(len(list), ShouldEqual, 1)
|
So(len(list), ShouldEqual, 1)
|
||||||
So(list[0].Account, ShouldEqual, "user@example.com")
|
So(list[0].Account, ShouldEqual, "user@example.com")
|
||||||
|
So(list[0].Status, ShouldEqual, consts.PayoutAccountStatusPending)
|
||||||
|
|
||||||
// Remove
|
// Remove
|
||||||
err = Creator.RemovePayoutAccount(ctx, tenantID, u.ID, list[0].ID)
|
err = Creator.RemovePayoutAccount(ctx, tenantID, u.ID, list[0].ID)
|
||||||
@@ -285,10 +286,11 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
|||||||
pa := &models.PayoutAccount{
|
pa := &models.PayoutAccount{
|
||||||
TenantID: t.ID,
|
TenantID: t.ID,
|
||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
Type: "bank",
|
Type: consts.PayoutAccountTypeBank,
|
||||||
Name: "Bank",
|
Name: "Bank",
|
||||||
Account: "123",
|
Account: "123",
|
||||||
Realname: "Creator",
|
Realname: "Creator",
|
||||||
|
Status: consts.PayoutAccountStatusApproved,
|
||||||
}
|
}
|
||||||
models.PayoutAccountQuery.WithContext(ctx).Create(pa)
|
models.PayoutAccountQuery.WithContext(ctx).Create(pa)
|
||||||
|
|
||||||
@@ -316,7 +318,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
|||||||
var snap fields.OrdersWithdrawalSnapshot
|
var snap fields.OrdersWithdrawalSnapshot
|
||||||
So(json.Unmarshal(o.Snapshot.Data().Data, &snap), ShouldBeNil)
|
So(json.Unmarshal(o.Snapshot.Data().Data, &snap), ShouldBeNil)
|
||||||
So(snap.AccountID, ShouldEqual, pa.ID)
|
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.AccountName, ShouldEqual, pa.Name)
|
||||||
So(snap.Account, ShouldEqual, pa.Account)
|
So(snap.Account, ShouldEqual, pa.Account)
|
||||||
So(snap.AccountRealname, ShouldEqual, pa.Realname)
|
So(snap.AccountRealname, ShouldEqual, pa.Realname)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
super_dto "quyun/v2/app/http/super/v1/dto"
|
super_dto "quyun/v2/app/http/super/v1/dto"
|
||||||
v1_dto "quyun/v2/app/http/v1/dto"
|
v1_dto "quyun/v2/app/http/v1/dto"
|
||||||
"quyun/v2/app/requests"
|
"quyun/v2/app/requests"
|
||||||
|
"quyun/v2/database/fields"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
jwt_provider "quyun/v2/providers/jwt"
|
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))
|
q = q.Where(tbl.UserID.Eq(*filter.UserID))
|
||||||
}
|
}
|
||||||
if filter.Type != nil && strings.TrimSpace(*filter.Type) != "" {
|
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)
|
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,
|
TenantName: tenantName,
|
||||||
UserID: pa.UserID,
|
UserID: pa.UserID,
|
||||||
Username: username,
|
Username: username,
|
||||||
Type: pa.Type,
|
Type: string(pa.Type),
|
||||||
Name: pa.Name,
|
Name: pa.Name,
|
||||||
Account: pa.Account,
|
Account: pa.Account,
|
||||||
Realname: pa.Realname,
|
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),
|
CreatedAt: s.formatTime(pa.CreatedAt),
|
||||||
UpdatedAt: s.formatTime(pa.UpdatedAt),
|
UpdatedAt: s.formatTime(pa.UpdatedAt),
|
||||||
})
|
})
|
||||||
@@ -1322,6 +1331,84 @@ func (s *super) RemovePayoutAccount(ctx context.Context, operatorID, id int64) e
|
|||||||
return nil
|
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) {
|
func (s *super) ListTenantJoinRequests(ctx context.Context, filter *super_dto.SuperTenantJoinRequestListFilter) (*requests.Pager, error) {
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
filter = &super_dto.SuperTenantJoinRequestListFilter{}
|
filter = &super_dto.SuperTenantJoinRequestListFilter{}
|
||||||
@@ -8008,6 +8095,26 @@ func (s *super) ApproveWithdrawal(ctx context.Context, operatorID, id int64) err
|
|||||||
return errorx.ErrStatusConflict.WithMsg("订单状态不正确")
|
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)
|
// Mark as Paid (Assumes external transfer done)
|
||||||
_, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).Updates(&models.Order{
|
_, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).Updates(&models.Order{
|
||||||
Status: consts.OrderStatusPaid,
|
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
|
type: consts.CouponType
|
||||||
user_coupons:
|
user_coupons:
|
||||||
status: consts.UserCouponStatus
|
status: consts.UserCouponStatus
|
||||||
|
payout_accounts:
|
||||||
|
type: consts.PayoutAccountType
|
||||||
|
status: consts.PayoutAccountStatus
|
||||||
notification_templates:
|
notification_templates:
|
||||||
type: consts.NotificationType
|
type: consts.NotificationType
|
||||||
field_relate:
|
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"`
|
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
|
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
|
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"`
|
Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"`
|
||||||
ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,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
|
// 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.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||||
_contentQuery.Key = field.NewString(tableName, "key")
|
_contentQuery.Key = field.NewString(tableName, "key")
|
||||||
_contentQuery.IsPinned = field.NewBool(tableName, "is_pinned")
|
_contentQuery.IsPinned = field.NewBool(tableName, "is_pinned")
|
||||||
_contentQuery.Comments = contentQueryHasManyComments{
|
|
||||||
db: db.Session(&gorm.Session{}),
|
|
||||||
|
|
||||||
RelationField: field.NewRelation("Comments", "Comment"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_contentQuery.Author = contentQueryBelongsToAuthor{
|
_contentQuery.Author = contentQueryBelongsToAuthor{
|
||||||
db: db.Session(&gorm.Session{}),
|
db: db.Session(&gorm.Session{}),
|
||||||
|
|
||||||
@@ -64,6 +58,12 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery {
|
|||||||
RelationField: field.NewRelation("ContentAssets", "ContentAsset"),
|
RelationField: field.NewRelation("ContentAssets", "ContentAsset"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_contentQuery.Comments = contentQueryHasManyComments{
|
||||||
|
db: db.Session(&gorm.Session{}),
|
||||||
|
|
||||||
|
RelationField: field.NewRelation("Comments", "Comment"),
|
||||||
|
}
|
||||||
|
|
||||||
_contentQuery.fillFieldMap()
|
_contentQuery.fillFieldMap()
|
||||||
|
|
||||||
return _contentQuery
|
return _contentQuery
|
||||||
@@ -94,12 +94,12 @@ type contentQuery struct {
|
|||||||
DeletedAt field.Field
|
DeletedAt field.Field
|
||||||
Key field.String // Musical key/tone
|
Key field.String // Musical key/tone
|
||||||
IsPinned field.Bool // Whether content is pinned/featured
|
IsPinned field.Bool // Whether content is pinned/featured
|
||||||
Comments contentQueryHasManyComments
|
|
||||||
|
|
||||||
Author contentQueryBelongsToAuthor
|
Author contentQueryBelongsToAuthor
|
||||||
|
|
||||||
ContentAssets contentQueryHasManyContentAssets
|
ContentAssets contentQueryHasManyContentAssets
|
||||||
|
|
||||||
|
Comments contentQueryHasManyComments
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,104 +195,23 @@ func (c *contentQuery) fillFieldMap() {
|
|||||||
|
|
||||||
func (c contentQuery) clone(db *gorm.DB) contentQuery {
|
func (c contentQuery) clone(db *gorm.DB) contentQuery {
|
||||||
c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool)
|
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 = db.Session(&gorm.Session{Initialized: true})
|
||||||
c.Author.db.Statement.ConnPool = db.Statement.ConnPool
|
c.Author.db.Statement.ConnPool = db.Statement.ConnPool
|
||||||
c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true})
|
c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true})
|
||||||
c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool
|
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
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c contentQuery) replaceDB(db *gorm.DB) contentQuery {
|
func (c contentQuery) replaceDB(db *gorm.DB) contentQuery {
|
||||||
c.contentQueryDo.ReplaceDB(db)
|
c.contentQueryDo.ReplaceDB(db)
|
||||||
c.Comments.db = db.Session(&gorm.Session{})
|
|
||||||
c.Author.db = db.Session(&gorm.Session{})
|
c.Author.db = db.Session(&gorm.Session{})
|
||||||
c.ContentAssets.db = db.Session(&gorm.Session{})
|
c.ContentAssets.db = db.Session(&gorm.Session{})
|
||||||
|
c.Comments.db = db.Session(&gorm.Session{})
|
||||||
return c
|
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 {
|
type contentQueryBelongsToAuthor struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
|
||||||
@@ -455,6 +374,87 @@ func (a contentQueryHasManyContentAssetsTx) Unscoped() *contentQueryHasManyConte
|
|||||||
return &a
|
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 }
|
type contentQueryDo struct{ gen.DO }
|
||||||
|
|
||||||
func (c contentQueryDo) Debug() *contentQueryDo {
|
func (c contentQueryDo) Debug() *contentQueryDo {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"quyun/v2/pkg/consts"
|
||||||
|
|
||||||
"go.ipao.vip/gen"
|
"go.ipao.vip/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,12 +20,16 @@ type PayoutAccount struct {
|
|||||||
ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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
|
// 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.ID = field.NewInt64(tableName, "id")
|
||||||
_payoutAccountQuery.TenantID = field.NewInt64(tableName, "tenant_id")
|
_payoutAccountQuery.TenantID = field.NewInt64(tableName, "tenant_id")
|
||||||
_payoutAccountQuery.UserID = field.NewInt64(tableName, "user_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.Name = field.NewString(tableName, "name")
|
||||||
_payoutAccountQuery.Account = field.NewString(tableName, "account")
|
_payoutAccountQuery.Account = field.NewString(tableName, "account")
|
||||||
_payoutAccountQuery.Realname = field.NewString(tableName, "realname")
|
_payoutAccountQuery.Realname = field.NewString(tableName, "realname")
|
||||||
_payoutAccountQuery.CreatedAt = field.NewTime(tableName, "created_at")
|
_payoutAccountQuery.CreatedAt = field.NewTime(tableName, "created_at")
|
||||||
_payoutAccountQuery.UpdatedAt = field.NewTime(tableName, "updated_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()
|
_payoutAccountQuery.fillFieldMap()
|
||||||
|
|
||||||
@@ -47,12 +51,16 @@ type payoutAccountQuery struct {
|
|||||||
ID field.Int64
|
ID field.Int64
|
||||||
TenantID field.Int64
|
TenantID field.Int64
|
||||||
UserID field.Int64
|
UserID field.Int64
|
||||||
Type field.String
|
Type field.Field
|
||||||
Name field.String
|
Name field.String
|
||||||
Account field.String
|
Account field.String
|
||||||
Realname field.String
|
Realname field.String
|
||||||
CreatedAt field.Time
|
CreatedAt field.Time
|
||||||
UpdatedAt 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
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@@ -72,12 +80,16 @@ func (p *payoutAccountQuery) updateTableName(table string) *payoutAccountQuery {
|
|||||||
p.ID = field.NewInt64(table, "id")
|
p.ID = field.NewInt64(table, "id")
|
||||||
p.TenantID = field.NewInt64(table, "tenant_id")
|
p.TenantID = field.NewInt64(table, "tenant_id")
|
||||||
p.UserID = field.NewInt64(table, "user_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.Name = field.NewString(table, "name")
|
||||||
p.Account = field.NewString(table, "account")
|
p.Account = field.NewString(table, "account")
|
||||||
p.Realname = field.NewString(table, "realname")
|
p.Realname = field.NewString(table, "realname")
|
||||||
p.CreatedAt = field.NewTime(table, "created_at")
|
p.CreatedAt = field.NewTime(table, "created_at")
|
||||||
p.UpdatedAt = field.NewTime(table, "updated_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()
|
p.fillFieldMap()
|
||||||
|
|
||||||
@@ -110,7 +122,7 @@ func (p *payoutAccountQuery) GetFieldByName(fieldName string) (field.OrderExpr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *payoutAccountQuery) fillFieldMap() {
|
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["id"] = p.ID
|
||||||
p.fieldMap["tenant_id"] = p.TenantID
|
p.fieldMap["tenant_id"] = p.TenantID
|
||||||
p.fieldMap["user_id"] = p.UserID
|
p.fieldMap["user_id"] = p.UserID
|
||||||
@@ -120,6 +132,10 @@ func (p *payoutAccountQuery) fillFieldMap() {
|
|||||||
p.fieldMap["realname"] = p.Realname
|
p.fieldMap["realname"] = p.Realname
|
||||||
p.fieldMap["created_at"] = p.CreatedAt
|
p.fieldMap["created_at"] = p.CreatedAt
|
||||||
p.fieldMap["updated_at"] = p.UpdatedAt
|
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 {
|
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": {
|
"/super/v1/reports/export": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Export platform report data",
|
"description": "Export platform report data",
|
||||||
@@ -6278,6 +6320,19 @@ const docTemplate = `{
|
|||||||
"OrderTypeWithdrawal"
|
"OrderTypeWithdrawal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.PayoutAccountStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"approved",
|
||||||
|
"rejected"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"PayoutAccountStatusPending",
|
||||||
|
"PayoutAccountStatusApproved",
|
||||||
|
"PayoutAccountStatusRejected"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.Role": {
|
"consts.Role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -7432,6 +7487,26 @@ const docTemplate = `{
|
|||||||
"description": "Realname 收款人姓名。",
|
"description": "Realname 收款人姓名。",
|
||||||
"type": "string"
|
"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": {
|
"type": {
|
||||||
"description": "Type 账户类型(bank/alipay)。",
|
"description": "Type 账户类型(bank/alipay)。",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -9094,6 +9169,30 @@ const docTemplate = `{
|
|||||||
"description": "Realname 收款人姓名。",
|
"description": "Realname 收款人姓名。",
|
||||||
"type": "string"
|
"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": {
|
"tenant_code": {
|
||||||
"description": "TenantCode 租户编码。",
|
"description": "TenantCode 租户编码。",
|
||||||
"type": "string"
|
"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": {
|
"dto.SuperReportExportForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"/super/v1/reports/export": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Export platform report data",
|
"description": "Export platform report data",
|
||||||
@@ -6272,6 +6314,19 @@
|
|||||||
"OrderTypeWithdrawal"
|
"OrderTypeWithdrawal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.PayoutAccountStatus": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"pending",
|
||||||
|
"approved",
|
||||||
|
"rejected"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"PayoutAccountStatusPending",
|
||||||
|
"PayoutAccountStatusApproved",
|
||||||
|
"PayoutAccountStatusRejected"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.Role": {
|
"consts.Role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -7426,6 +7481,26 @@
|
|||||||
"description": "Realname 收款人姓名。",
|
"description": "Realname 收款人姓名。",
|
||||||
"type": "string"
|
"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": {
|
"type": {
|
||||||
"description": "Type 账户类型(bank/alipay)。",
|
"description": "Type 账户类型(bank/alipay)。",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -9088,6 +9163,30 @@
|
|||||||
"description": "Realname 收款人姓名。",
|
"description": "Realname 收款人姓名。",
|
||||||
"type": "string"
|
"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": {
|
"tenant_code": {
|
||||||
"description": "TenantCode 租户编码。",
|
"description": "TenantCode 租户编码。",
|
||||||
"type": "string"
|
"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": {
|
"dto.SuperReportExportForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ definitions:
|
|||||||
- OrderTypeContentPurchase
|
- OrderTypeContentPurchase
|
||||||
- OrderTypeRecharge
|
- OrderTypeRecharge
|
||||||
- OrderTypeWithdrawal
|
- OrderTypeWithdrawal
|
||||||
|
consts.PayoutAccountStatus:
|
||||||
|
enum:
|
||||||
|
- pending
|
||||||
|
- approved
|
||||||
|
- rejected
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- PayoutAccountStatusPending
|
||||||
|
- PayoutAccountStatusApproved
|
||||||
|
- PayoutAccountStatusRejected
|
||||||
consts.Role:
|
consts.Role:
|
||||||
enum:
|
enum:
|
||||||
- user
|
- user
|
||||||
@@ -940,6 +950,19 @@ definitions:
|
|||||||
realname:
|
realname:
|
||||||
description: Realname 收款人姓名。
|
description: Realname 收款人姓名。
|
||||||
type: string
|
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:
|
type:
|
||||||
description: Type 账户类型(bank/alipay)。
|
description: Type 账户类型(bank/alipay)。
|
||||||
type: string
|
type: string
|
||||||
@@ -2100,6 +2123,22 @@ definitions:
|
|||||||
realname:
|
realname:
|
||||||
description: Realname 收款人姓名。
|
description: Realname 收款人姓名。
|
||||||
type: string
|
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:
|
tenant_code:
|
||||||
description: TenantCode 租户编码。
|
description: TenantCode 租户编码。
|
||||||
type: string
|
type: string
|
||||||
@@ -2122,6 +2161,20 @@ definitions:
|
|||||||
description: Username 用户名。
|
description: Username 用户名。
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
dto.SuperReportExportForm:
|
||||||
properties:
|
properties:
|
||||||
end_at:
|
end_at:
|
||||||
@@ -4266,6 +4319,34 @@ paths:
|
|||||||
summary: Remove payout account
|
summary: Remove payout account
|
||||||
tags:
|
tags:
|
||||||
- Finance
|
- 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:
|
/super/v1/reports/export:
|
||||||
post:
|
post:
|
||||||
consumes:
|
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 }
|
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) => {
|
const iso = (d) => {
|
||||||
if (!d) return undefined;
|
if (!d) return undefined;
|
||||||
const date = d instanceof Date ? d : new Date(d);
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
@@ -127,6 +127,7 @@ export const CreatorService = {
|
|||||||
user_id,
|
user_id,
|
||||||
username,
|
username,
|
||||||
type,
|
type,
|
||||||
|
status,
|
||||||
created_at_from: iso(created_at_from),
|
created_at_from: iso(created_at_from),
|
||||||
created_at_to: iso(created_at_to)
|
created_at_to: iso(created_at_to)
|
||||||
};
|
};
|
||||||
@@ -143,6 +144,16 @@ export const CreatorService = {
|
|||||||
if (!accountID) throw new Error('accountID is required');
|
if (!accountID) throw new Error('accountID is required');
|
||||||
return requestJson(`/super/v1/payout-accounts/${accountID}`, { method: 'DELETE' });
|
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 } = {}) {
|
async createInvite(tenantID, { max_uses, expires_at, remark } = {}) {
|
||||||
if (!tenantID) throw new Error('tenantID is required');
|
if (!tenantID) throw new Error('tenantID is required');
|
||||||
return requestJson(`/super/v1/tenants/${tenantID}/invites`, {
|
return requestJson(`/super/v1/tenants/${tenantID}/invites`, {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const payoutTenantName = ref('');
|
|||||||
const payoutUserID = ref(null);
|
const payoutUserID = ref(null);
|
||||||
const payoutUsername = ref('');
|
const payoutUsername = ref('');
|
||||||
const payoutType = ref('');
|
const payoutType = ref('');
|
||||||
|
const payoutStatus = ref('');
|
||||||
const payoutCreatedAtFrom = ref(null);
|
const payoutCreatedAtFrom = ref(null);
|
||||||
const payoutCreatedAtTo = ref(null);
|
const payoutCreatedAtTo = ref(null);
|
||||||
|
|
||||||
@@ -84,6 +85,18 @@ const joinStatusOptions = [
|
|||||||
{ label: '已驳回', value: 'rejected' }
|
{ 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 reviewDialogVisible = ref(false);
|
||||||
const reviewSubmitting = ref(false);
|
const reviewSubmitting = ref(false);
|
||||||
const reviewAction = ref('approve');
|
const reviewAction = ref('approve');
|
||||||
@@ -100,6 +113,12 @@ const payoutRemoveDialogVisible = ref(false);
|
|||||||
const payoutRemoveSubmitting = ref(false);
|
const payoutRemoveSubmitting = ref(false);
|
||||||
const payoutRemoveTarget = ref(null);
|
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 inviteDialogVisible = ref(false);
|
||||||
const inviteSubmitting = ref(false);
|
const inviteSubmitting = ref(false);
|
||||||
const inviteTenantID = ref(null);
|
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() {
|
async function ensureStatusOptionsLoaded() {
|
||||||
if (statusOptions.value.length > 0) return;
|
if (statusOptions.value.length > 0) return;
|
||||||
statusOptionsLoading.value = true;
|
statusOptionsLoading.value = true;
|
||||||
@@ -240,6 +272,7 @@ async function loadPayoutAccounts() {
|
|||||||
user_id: payoutUserID.value || undefined,
|
user_id: payoutUserID.value || undefined,
|
||||||
username: payoutUsername.value,
|
username: payoutUsername.value,
|
||||||
type: payoutType.value || undefined,
|
type: payoutType.value || undefined,
|
||||||
|
status: payoutStatus.value || undefined,
|
||||||
created_at_from: payoutCreatedAtFrom.value || undefined,
|
created_at_from: payoutCreatedAtFrom.value || undefined,
|
||||||
created_at_to: payoutCreatedAtTo.value || undefined
|
created_at_to: payoutCreatedAtTo.value || undefined
|
||||||
});
|
});
|
||||||
@@ -345,6 +378,7 @@ function onPayoutReset() {
|
|||||||
payoutUserID.value = null;
|
payoutUserID.value = null;
|
||||||
payoutUsername.value = '';
|
payoutUsername.value = '';
|
||||||
payoutType.value = '';
|
payoutType.value = '';
|
||||||
|
payoutStatus.value = '';
|
||||||
payoutCreatedAtFrom.value = null;
|
payoutCreatedAtFrom.value = null;
|
||||||
payoutCreatedAtTo.value = null;
|
payoutCreatedAtTo.value = null;
|
||||||
payoutAccountsPage.value = 1;
|
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) {
|
function openInviteDialog(row) {
|
||||||
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
|
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
|
||||||
inviteMaxUses.value = 1;
|
inviteMaxUses.value = 1;
|
||||||
@@ -826,6 +889,9 @@ onMounted(() => {
|
|||||||
<SearchField label="类型">
|
<SearchField label="类型">
|
||||||
<InputText v-model="payoutType" placeholder="bank/alipay" class="w-full" @keyup.enter="onPayoutSearch" />
|
<InputText v-model="payoutType" placeholder="bank/alipay" class="w-full" @keyup.enter="onPayoutSearch" />
|
||||||
</SearchField>
|
</SearchField>
|
||||||
|
<SearchField label="状态">
|
||||||
|
<Select v-model="payoutStatus" :options="payoutStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
<SearchField label="创建时间 From">
|
<SearchField label="创建时间 From">
|
||||||
<DatePicker v-model="payoutCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
<DatePicker v-model="payoutCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
</SearchField>
|
</SearchField>
|
||||||
@@ -873,6 +939,15 @@ onMounted(() => {
|
|||||||
<Column field="name" header="账户名称" style="min-width: 14rem" />
|
<Column field="name" header="账户名称" style="min-width: 14rem" />
|
||||||
<Column field="account" header="账号" style="min-width: 14rem" />
|
<Column field="account" header="账号" style="min-width: 14rem" />
|
||||||
<Column field="realname" header="收款人" style="min-width: 12rem" />
|
<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">
|
<Column field="created_at" header="创建时间" style="min-width: 14rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatDate(data.created_at) }}
|
{{ formatDate(data.created_at) }}
|
||||||
@@ -880,7 +955,11 @@ onMounted(() => {
|
|||||||
</Column>
|
</Column>
|
||||||
<Column header="操作" style="min-width: 8rem">
|
<Column header="操作" style="min-width: 8rem">
|
||||||
<template #body="{ data }">
|
<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>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
@@ -1001,6 +1080,30 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</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' }">
|
<Dialog v-model:visible="inviteDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user