feat: add withdrawal snapshot for review
This commit is contained in:
@@ -2,6 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -939,13 +940,26 @@ func (s *creator) Withdraw(ctx context.Context, tenantID, userID int64, form *cr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate Payout Account
|
// Validate Payout Account
|
||||||
_, err = models.PayoutAccountQuery.WithContext(ctx).
|
account, err := models.PayoutAccountQuery.WithContext(ctx).
|
||||||
Where(models.PayoutAccountQuery.ID.Eq(form.AccountID), models.PayoutAccountQuery.TenantID.Eq(tid)).
|
Where(models.PayoutAccountQuery.ID.Eq(form.AccountID), models.PayoutAccountQuery.TenantID.Eq(tid)).
|
||||||
First()
|
First()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.ErrRecordNotFound.WithMsg("收款账户不存在")
|
return errorx.ErrRecordNotFound.WithMsg("收款账户不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将收款账户快照写入订单,便于超管审核与打款核对。
|
||||||
|
snapshotPayload, err := json.Marshal(fields.OrdersWithdrawalSnapshot{
|
||||||
|
Method: form.Method,
|
||||||
|
AccountID: account.ID,
|
||||||
|
AccountType: account.Type,
|
||||||
|
AccountName: account.Name,
|
||||||
|
Account: account.Account,
|
||||||
|
AccountRealname: account.Realname,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errorx.ErrInternalError.WithCause(err).WithMsg("构建提现快照失败")
|
||||||
|
}
|
||||||
|
|
||||||
return models.Q.Transaction(func(tx *models.Query) error {
|
return models.Q.Transaction(func(tx *models.Query) error {
|
||||||
// 1. Deduct Balance
|
// 1. Deduct Balance
|
||||||
info, err := tx.User.WithContext(ctx).
|
info, err := tx.User.WithContext(ctx).
|
||||||
@@ -968,7 +982,10 @@ func (s *creator) Withdraw(ctx context.Context, tenantID, userID int64, form *cr
|
|||||||
AmountOriginal: amount,
|
AmountOriginal: amount,
|
||||||
AmountPaid: amount, // Actually Amount Withdrawn
|
AmountPaid: amount, // Actually Amount Withdrawn
|
||||||
IdempotencyKey: uuid.NewString(),
|
IdempotencyKey: uuid.NewString(),
|
||||||
Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Can store account details here
|
Snapshot: types.NewJSONType(fields.OrdersSnapshot{
|
||||||
|
Kind: "withdrawal",
|
||||||
|
Data: snapshotPayload,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
if err := tx.Order.WithContext(ctx).Create(order); err != nil {
|
if err := tx.Order.WithContext(ctx).Create(order); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"quyun/v2/app/commands/testx"
|
"quyun/v2/app/commands/testx"
|
||||||
creator_dto "quyun/v2/app/http/v1/dto"
|
creator_dto "quyun/v2/app/http/v1/dto"
|
||||||
"quyun/v2/database"
|
"quyun/v2/database"
|
||||||
|
"quyun/v2/database/fields"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
|
|
||||||
@@ -293,6 +295,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
|||||||
Convey("should withdraw successfully", func() {
|
Convey("should withdraw successfully", func() {
|
||||||
form := &creator_dto.WithdrawForm{
|
form := &creator_dto.WithdrawForm{
|
||||||
Amount: 20.00,
|
Amount: 20.00,
|
||||||
|
Method: "external",
|
||||||
AccountID: pa.ID,
|
AccountID: pa.ID,
|
||||||
}
|
}
|
||||||
err := Creator.Withdraw(ctx, tenantID, u.ID, form)
|
err := Creator.Withdraw(ctx, tenantID, u.ID, form)
|
||||||
@@ -308,6 +311,16 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
|||||||
First()
|
First()
|
||||||
So(o, ShouldNotBeNil)
|
So(o, ShouldNotBeNil)
|
||||||
So(o.AmountPaid, ShouldEqual, 2000)
|
So(o.AmountPaid, ShouldEqual, 2000)
|
||||||
|
So(o.Snapshot.Data().Kind, ShouldEqual, "withdrawal")
|
||||||
|
|
||||||
|
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.AccountName, ShouldEqual, pa.Name)
|
||||||
|
So(snap.Account, ShouldEqual, pa.Account)
|
||||||
|
So(snap.AccountRealname, ShouldEqual, pa.Realname)
|
||||||
|
So(snap.Method, ShouldEqual, "external")
|
||||||
|
|
||||||
// Verify Ledger
|
// Verify Ledger
|
||||||
l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First()
|
l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First()
|
||||||
|
|||||||
@@ -4170,6 +4170,7 @@ func (s *super) toSuperOrderItem(o *models.Order, tenant *models.Tenant, buyer *
|
|||||||
AmountOriginal: o.AmountOriginal,
|
AmountOriginal: o.AmountOriginal,
|
||||||
AmountDiscount: o.AmountDiscount,
|
AmountDiscount: o.AmountDiscount,
|
||||||
AmountPaid: o.AmountPaid,
|
AmountPaid: o.AmountPaid,
|
||||||
|
Snapshot: o.Snapshot.Data(),
|
||||||
CreatedAt: o.CreatedAt.Format(time.RFC3339),
|
CreatedAt: o.CreatedAt.Format(time.RFC3339),
|
||||||
UpdatedAt: o.UpdatedAt.Format(time.RFC3339),
|
UpdatedAt: o.UpdatedAt.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,3 +78,19 @@ type OrdersContentPurchaseSnapshot struct {
|
|||||||
// PurchasePricingNotes 价格计算补充说明(可选,便于排查争议)。
|
// PurchasePricingNotes 价格计算补充说明(可选,便于排查争议)。
|
||||||
PurchasePricingNotes string `json:"purchase_pricing_notes,omitempty"`
|
PurchasePricingNotes string `json:"purchase_pricing_notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OrdersWithdrawalSnapshot 为“创作者提现订单”的快照信息(用于打款核对与审计追溯)。
|
||||||
|
type OrdersWithdrawalSnapshot struct {
|
||||||
|
// Method 提现方式(wallet/external)。
|
||||||
|
Method string `json:"method"`
|
||||||
|
// AccountID 收款账户ID(来源 payout_accounts)。
|
||||||
|
AccountID int64 `json:"account_id"`
|
||||||
|
// AccountType 收款账户类型(bank/alipay)。
|
||||||
|
AccountType string `json:"account_type"`
|
||||||
|
// AccountName 收款账户名称/开户行。
|
||||||
|
AccountName string `json:"account_name"`
|
||||||
|
// Account 收款账号。
|
||||||
|
Account string `json:"account"`
|
||||||
|
// AccountRealname 收款人姓名。
|
||||||
|
AccountRealname string `json:"account_realname"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
### 2.9 创作者与成员审核 `/superadmin/creators`
|
### 2.9 创作者与成员审核 `/superadmin/creators`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除。
|
- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除。
|
||||||
- 缺口:结算账户审批流(若需要区分通过/驳回状态),创作者维度提现审核与财务联动入口。
|
- 缺口:结算账户审批流(若需要区分通过/驳回状态)。
|
||||||
|
|
||||||
### 2.10 优惠券 `/superadmin/coupons`
|
### 2.10 优惠券 `/superadmin/coupons`
|
||||||
- 状态:**已完成**
|
- 状态:**已完成**
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
### 2.11 财务与钱包 `/superadmin/finance`
|
### 2.11 财务与钱包 `/superadmin/finance`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
- 已有:提现列表与审批/驳回。
|
- 已有:提现列表与审批/驳回、收款账户快照展示。
|
||||||
- 缺口:钱包流水、充值与退款异常排查、资金汇总报表。
|
- 缺口:钱包流水、充值与退款异常排查、资金汇总报表。
|
||||||
|
|
||||||
### 2.12 报表与导出 `/superadmin/reports`
|
### 2.12 报表与导出 `/superadmin/reports`
|
||||||
@@ -88,9 +88,8 @@
|
|||||||
## 3) `/super/v1` 接口覆盖度概览
|
## 3) `/super/v1` 接口覆盖度概览
|
||||||
|
|
||||||
- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。
|
- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。
|
||||||
- **缺失/待补**:创作者提现审核。
|
- **缺失/待补**:暂无与提现审核相关缺口。
|
||||||
|
|
||||||
## 4) 建议的下一步(按优先级)
|
## 4) 建议的下一步(按优先级)
|
||||||
|
|
||||||
1. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。
|
1. **内容/财务治理补齐**:评论/举报治理、钱包流水与异常排查能力。
|
||||||
2. **内容/财务治理补齐**:评论/举报治理、钱包流水与异常排查能力。
|
|
||||||
|
|||||||
@@ -80,6 +80,27 @@ function formatCny(amountInCents) {
|
|||||||
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveWithdrawalSnapshot(order) {
|
||||||
|
const snapshot = order?.snapshot;
|
||||||
|
if (!snapshot || typeof snapshot !== 'object') return null;
|
||||||
|
const kind = snapshot.kind || snapshot.Kind;
|
||||||
|
const data = snapshot.data ?? snapshot.Data;
|
||||||
|
if (kind !== 'withdrawal') return null;
|
||||||
|
if (!data || typeof data !== 'object') return null;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWithdrawMethod(method) {
|
||||||
|
switch (method) {
|
||||||
|
case 'wallet':
|
||||||
|
return '钱包';
|
||||||
|
case 'external':
|
||||||
|
return '外部打款';
|
||||||
|
default:
|
||||||
|
return method || '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusSeverity(value) {
|
function getStatusSeverity(value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 'paid':
|
case 'paid':
|
||||||
@@ -342,6 +363,18 @@ watch(
|
|||||||
<span v-else class="text-muted-color">{{ data.buyer?.id ?? '-' }}</span>
|
<span v-else class="text-muted-color">{{ data.buyer?.id ?? '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column header="收款账户" style="min-width: 16rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div v-if="resolveWithdrawalSnapshot(data)" class="flex flex-col">
|
||||||
|
<span class="font-medium">{{ resolveWithdrawalSnapshot(data).account_realname || '-' }}</span>
|
||||||
|
<span class="text-xs text-muted-color">
|
||||||
|
{{ formatWithdrawMethod(resolveWithdrawalSnapshot(data).method) }} · {{ resolveWithdrawalSnapshot(data).account_type || '-' }} ·
|
||||||
|
{{ resolveWithdrawalSnapshot(data).account || '-' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-muted-color">-</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
<Column field="amount_paid" header="金额" sortable style="min-width: 10rem">
|
<Column field="amount_paid" header="金额" sortable style="min-width: 10rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatCny(data.amount_paid) }}
|
{{ formatCny(data.amount_paid) }}
|
||||||
|
|||||||
Reference in New Issue
Block a user