Files
break c64f3d8b80
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
全局审计完成
2026-08-07 11:02:52 +08:00

1520 lines
70 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package audit
import (
"context"
"sort"
"strconv"
"strings"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
retentionquery "github.com/break/junhong_cmp_fiber/internal/query/retention"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// FinanceFilter 定义资金调查时间线的稳定业务筛选。
type FinanceFilter struct {
ShopID uint
WalletID uint
OrderID uint
OrderNo string
PaymentID uint
PaymentNo string
RefundID uint
RefundNo string
RechargeID uint
RechargeNo string
ApprovalInstanceID uint
ThirdPartyTradeNo string
ActorKind string
ActorID string
CorrelationID string
CreatedFrom *time.Time
CreatedTo *time.Time
Page int
PageSize int
}
// FinanceTimelinePage 是资金多源投影的稳定分页结果。
type FinanceTimelinePage struct {
Total int64 `json:"total" description:"关联资金事实总数"`
Page int `json:"page" description:"当前页码"`
PageSize int `json:"page_size" description:"每页数量"`
Items []FinanceTimelineNode `json:"items" description:"按发生时间稳定倒序的资金事实节点"`
Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"`
}
// FinanceTimelineNode 是明确事实来源和金额权威的资金时间线节点。
type FinanceTimelineNode struct {
RecordSource string `json:"record_source" enum:"audit_event,domain_ledger_ref,agent_wallet_transaction,asset_wallet_transaction,agent_wallet_reservation,order,payment,refund,agent_recharge,recharge_order,commission_record,commission_withdrawal,approval_instance" description:"资金事实来源稳定编码"`
NodeID string `json:"node_id" description:"该事实来源内的稳定节点ID"`
OccurredAt time.Time `json:"occurred_at" description:"资金事实发生时间"`
Code string `json:"code" description:"来源内稳定业务动作或状态编码"`
Title string `json:"title" description:"code对应的中文展示名称"`
Result string `json:"result" description:"来源内原始结果或状态编码"`
ResultName string `json:"result_name" description:"result对应的中文展示名称"`
Amount *int64 `json:"amount" description:"本节点金额,单位分;为空表示该节点不承载金额"`
BalanceBefore *int64 `json:"balance_before" description:"变更前余额,单位分"`
BalanceAfter *int64 `json:"balance_after" description:"变更后余额,单位分"`
Currency string `json:"currency" description:"币种编码人民币为CNY"`
ShopID *uint `json:"shop_id" description:"关联店铺ID"`
Wallet *FinanceWalletRef `json:"wallet" description:"关联钱包稳定引用"`
AmountAuthority FinanceAmountAuthority `json:"amount_authority" description:"金额是否权威及权威字段来源"`
Facts map[string]any `json:"facts" description:"该事实来源的安全结构化业务字段"`
InvestigationRefs InvestigationRefs `json:"investigation_refs" description:"可继续跳转的稳定调查引用"`
}
// FinanceWalletRef 是资金节点关联的钱包稳定引用。
type FinanceWalletRef struct {
ResourceType string `json:"resource_type" enum:"agent_wallet,asset_wallet" description:"钱包资源类型"`
WalletID uint `json:"wallet_id" description:"钱包内部稳定ID可作为finance/timeline的wallet_id"`
}
// FinanceAmountAuthority 说明当前金额是否为业务权威及其字段来源。
type FinanceAmountAuthority struct {
Authoritative bool `json:"authoritative" description:"当前amount或余额是否来自业务权威表"`
Table string `json:"table" description:"权威金额所在业务表;非权威节点可为空"`
Field string `json:"field" description:"权威金额所在字段;非权威节点可为空"`
ConflictRule string `json:"conflict_rule" description:"多来源冲突时的取值规则说明"`
}
type financeRefs struct {
seeded bool
shops map[uint]struct{}
agentWallets map[uint]struct{}
assetWallets map[uint]struct{}
agentTxs map[uint]struct{}
assetTxs map[uint]struct{}
reservations map[uint]struct{}
orders map[uint]struct{}
orderNos map[string]struct{}
payments map[uint]struct{}
paymentNos map[string]struct{}
refunds map[uint]struct{}
refundNos map[string]struct{}
agentRecharges map[uint]struct{}
rechargeOrders map[uint]struct{}
rechargeNos map[string]struct{}
approvals map[uint]struct{}
commissions map[uint]struct{}
withdrawals map[uint]struct{}
tradeNos map[string]struct{}
}
// FinanceTimeline 查询资金审计与业务账本的只读组合时间线。
func (q *Query) FinanceTimeline(ctx context.Context, filter FinanceFilter) (*FinanceTimelinePage, error) {
if err := q.authorize(ctx); err != nil {
return nil, err
}
retention, err := retentionquery.Load(ctx, q.db, retentionquery.SourceAudit)
if err != nil {
return nil, err
}
filter.CreatedFrom, filter.CreatedTo, err = retentionquery.NormalizeRange(retention, filter.CreatedFrom, filter.CreatedTo)
if err != nil {
return nil, err
}
if !validFinanceFilter(filter) {
return nil, errors.New(errors.CodeInvalidParam)
}
filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize)
refs := newFinanceRefs(filter)
if err := q.seedFinanceActorAndCorrelation(ctx, filter, refs); err != nil {
return nil, err
}
if err := q.expandFinanceRefs(ctx, refs); err != nil {
return nil, err
}
auditRows, auditTotal, err := q.loadFinanceAuditRows(ctx, filter, refs)
if err != nil {
return nil, err
}
if err := q.addAuditFinanceRefs(ctx, auditRows, refs); err != nil {
return nil, err
}
if err := q.expandFinanceRefs(ctx, refs); err != nil {
return nil, err
}
limit := filter.Page * filter.PageSize
nodes, total, err := q.loadFinanceNodes(ctx, filter, refs, auditRows, auditTotal, limit)
if err != nil {
return nil, err
}
sort.Slice(nodes, func(i, j int) bool {
if nodes[i].OccurredAt.Equal(nodes[j].OccurredAt) {
if nodes[i].RecordSource == nodes[j].RecordSource {
return financeNodeIDAfter(nodes[i].NodeID, nodes[j].NodeID)
}
return nodes[i].RecordSource > nodes[j].RecordSource
}
return nodes[i].OccurredAt.After(nodes[j].OccurredAt)
})
start := (filter.Page - 1) * filter.PageSize
if start > len(nodes) {
start = len(nodes)
}
end := start + filter.PageSize
if end > len(nodes) {
end = len(nodes)
}
items := nodes[start:end]
if items == nil {
items = []FinanceTimelineNode{}
}
return &FinanceTimelinePage{Total: total, Page: filter.Page, PageSize: filter.PageSize, Items: items, Retention: retention}, nil
}
// validFinanceFilter 拒绝无条件全表扫描及不完整的操作者筛选。
func validFinanceFilter(filter FinanceFilter) bool {
if filter.Page < 0 || filter.PageSize < 0 || filter.PageSize > constants.MaxPageSize {
return false
}
if filter.CreatedFrom != nil && filter.CreatedTo != nil && !filter.CreatedFrom.Before(*filter.CreatedTo) {
return false
}
if filter.ActorKind != "" && !validActorKind(filter.ActorKind) {
return false
}
if (filter.ActorKind == "") != (filter.ActorID == "") {
return false
}
hasStableCondition := filter.ShopID != 0 || filter.WalletID != 0 || filter.OrderID != 0 || filter.OrderNo != "" ||
filter.PaymentID != 0 || filter.PaymentNo != "" || filter.RefundID != 0 || filter.RefundNo != "" ||
filter.RechargeID != 0 || filter.RechargeNo != "" || filter.ApprovalInstanceID != 0 ||
filter.ThirdPartyTradeNo != "" || filter.ActorID != "" || filter.CorrelationID != ""
return hasStableCondition || (filter.CreatedFrom != nil && filter.CreatedTo != nil)
}
// newFinanceRefs 将调用方提供的稳定条件初始化为关联解析种子。
func newFinanceRefs(filter FinanceFilter) *financeRefs {
refs := &financeRefs{
shops: make(map[uint]struct{}), agentWallets: make(map[uint]struct{}), assetWallets: make(map[uint]struct{}),
agentTxs: make(map[uint]struct{}), assetTxs: make(map[uint]struct{}), reservations: make(map[uint]struct{}),
orders: make(map[uint]struct{}), orderNos: make(map[string]struct{}), payments: make(map[uint]struct{}),
paymentNos: make(map[string]struct{}), refunds: make(map[uint]struct{}), refundNos: make(map[string]struct{}),
agentRecharges: make(map[uint]struct{}), rechargeOrders: make(map[uint]struct{}), rechargeNos: make(map[string]struct{}),
approvals: make(map[uint]struct{}), commissions: make(map[uint]struct{}), withdrawals: make(map[uint]struct{}),
tradeNos: make(map[string]struct{}),
}
addUint(refs.shops, filter.ShopID)
addUint(refs.agentWallets, filter.WalletID)
addUint(refs.assetWallets, filter.WalletID)
addUint(refs.orders, filter.OrderID)
addString(refs.orderNos, filter.OrderNo)
addUint(refs.payments, filter.PaymentID)
addString(refs.paymentNos, filter.PaymentNo)
addUint(refs.refunds, filter.RefundID)
addString(refs.refundNos, filter.RefundNo)
addUint(refs.agentRecharges, filter.RechargeID)
addUint(refs.rechargeOrders, filter.RechargeID)
addString(refs.rechargeNos, filter.RechargeNo)
addUint(refs.approvals, filter.ApprovalInstanceID)
addString(refs.tradeNos, filter.ThirdPartyTradeNo)
refs.seeded = filter.ShopID != 0 || filter.WalletID != 0 || filter.OrderID != 0 || filter.OrderNo != "" ||
filter.PaymentID != 0 || filter.PaymentNo != "" || filter.RefundID != 0 || filter.RefundNo != "" ||
filter.RechargeID != 0 || filter.RechargeNo != "" || filter.ApprovalInstanceID != 0 || filter.ThirdPartyTradeNo != ""
return refs
}
// seedFinanceActorAndCorrelation 使用明确持久化字段解析操作者和业务链路,不按时间邻近猜测。
func (q *Query) seedFinanceActorAndCorrelation(ctx context.Context, filter FinanceFilter, refs *financeRefs) error {
if filter.CorrelationID != "" {
rows := []model.ApprovalInstance{}
if err := q.db.WithContext(ctx).Where("correlation_id = ?", filter.CorrelationID).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联审批链路失败")
}
for _, row := range rows {
addUint(refs.approvals, row.ID)
}
}
if filter.WalletID != 0 {
if err := q.seedFinanceWalletReferences(ctx, filter, refs); err != nil {
return err
}
}
if filter.ActorKind != constants.AuditActorAccount {
return nil
}
actorID64, err := strconv.ParseUint(filter.ActorID, 10, 64)
if err != nil {
return nil
}
actorID := uint(actorID64)
orderRows := []model.Order{}
orderQuery := applyFinanceTime(q.db.WithContext(ctx).Where("operator_account_id = ?", actorID), filter, "updated_at")
if err := orderQuery.Limit(filter.Page * filter.PageSize).Find(&orderRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析操作者关联订单失败")
}
for _, row := range orderRows {
addUint(refs.orders, row.ID)
}
agentRows := []model.AgentRechargeRecord{}
agentQuery := applyFinanceTime(q.db.WithContext(ctx).Where("user_id = ?", actorID), filter, "updated_at")
if err := agentQuery.Limit(filter.Page * filter.PageSize).Find(&agentRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析操作者关联代理充值失败")
}
for _, row := range agentRows {
addUint(refs.agentRecharges, row.ID)
}
rechargeRows := []model.RechargeOrder{}
rechargeQuery := applyFinanceTime(q.db.WithContext(ctx).Where("user_id = ?", actorID), filter, "updated_at")
if err := rechargeQuery.Limit(filter.Page * filter.PageSize).Find(&rechargeRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析操作者关联资产充值失败")
}
for _, row := range rechargeRows {
addUint(refs.rechargeOrders, row.ID)
}
approvalRows := []model.ApprovalInstance{}
approvalQuery := applyFinanceTime(q.db.WithContext(ctx).Where("submitter_account_id = ?", actorID), filter, "status_changed_at")
if err := approvalQuery.Limit(filter.Page * filter.PageSize).Find(&approvalRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析操作者关联审批失败")
}
for _, row := range approvalRows {
addUint(refs.approvals, row.ID)
}
return nil
}
// seedFinanceWalletReferences 从当前分页窗口的唯一流水引用解析业务单据。
func (q *Query) seedFinanceWalletReferences(ctx context.Context, filter FinanceFilter, refs *financeRefs) error {
limit := filter.Page * filter.PageSize
agentRows := []model.AgentWalletTransaction{}
agentQuery := applyFinanceTime(q.db.WithContext(ctx).Where("agent_wallet_id = ?", filter.WalletID), filter, "created_at")
if err := agentQuery.Order("created_at DESC, id DESC").Limit(limit).Find(&agentRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析代理钱包关联业务失败")
}
for _, row := range agentRows {
addUint(refs.agentTxs, row.ID)
if row.ReferenceType == nil || row.ReferenceID == nil {
continue
}
switch *row.ReferenceType {
case constants.ReferenceTypeOrder:
addUint(refs.orders, *row.ReferenceID)
case constants.ReferenceTypeRefund:
addUint(refs.refunds, *row.ReferenceID)
case constants.ReferenceTypeTopup:
addUint(refs.agentRecharges, *row.ReferenceID)
case constants.ReferenceTypeCommission:
addUint(refs.commissions, *row.ReferenceID)
case constants.ReferenceTypeWithdrawal:
addUint(refs.withdrawals, *row.ReferenceID)
}
}
assetRows := []model.AssetWalletTransaction{}
assetQuery := applyFinanceTime(q.db.WithContext(ctx).Where("asset_wallet_id = ?", filter.WalletID), filter, "created_at")
if err := assetQuery.Order("created_at DESC, id DESC").Limit(limit).Find(&assetRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资产钱包关联业务失败")
}
for _, row := range assetRows {
addUint(refs.assetTxs, row.ID)
if row.ReferenceType == nil || row.ReferenceNo == nil {
continue
}
switch *row.ReferenceType {
case constants.ReferenceTypeOrder:
addString(refs.orderNos, *row.ReferenceNo)
case constants.ReferenceTypeRefund:
addString(refs.refundNos, *row.ReferenceNo)
case constants.ReferenceTypeTopup:
addString(refs.rechargeNos, *row.ReferenceNo)
case constants.ReferenceTypeRecharge:
addString(refs.paymentNos, *row.ReferenceNo)
}
}
reservationRows := []model.AgentWalletReservation{}
reservationQuery := applyFinanceTime(q.db.WithContext(ctx).Where("agent_wallet_id = ?", filter.WalletID), filter, "updated_at")
if err := reservationQuery.Order("updated_at DESC, id DESC").Limit(limit).Find(&reservationRows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析代理钱包预占关联业务失败")
}
for _, row := range reservationRows {
addUint(refs.reservations, row.ID)
if row.ReferenceType == constants.ReferenceTypeOrder {
addUint(refs.orders, row.ReferenceID)
}
}
return nil
}
// expandFinanceRefs 以固定轮次展开支付、订单、退款、充值和审批的确定性关系。
func (q *Query) expandFinanceRefs(ctx context.Context, refs *financeRefs) error {
for range 2 {
if err := q.expandFinanceRecords(ctx, refs); err != nil {
return err
}
}
return nil
}
// expandFinanceRecords 批量读取各业务表,避免按时间线节点逐条回查。
func (q *Query) expandFinanceRecords(ctx context.Context, refs *financeRefs) error {
if err := q.expandOrders(ctx, refs); err != nil {
return err
}
if err := q.expandPayments(ctx, refs); err != nil {
return err
}
if err := q.expandRefunds(ctx, refs); err != nil {
return err
}
if err := q.expandRecharges(ctx, refs); err != nil {
return err
}
return q.expandApprovals(ctx, refs)
}
// expandOrders 解析订单编号、店铺和后续支付所需的内部 ID。
func (q *Query) expandOrders(ctx context.Context, refs *financeRefs) error {
conditions, args := make([]string, 0, 2), make([]any, 0, 2)
if len(refs.orders) > 0 {
conditions, args = append(conditions, "id IN ?"), append(args, uintKeys(refs.orders))
}
if len(refs.orderNos) > 0 {
conditions, args = append(conditions, "order_no IN ?"), append(args, stringKeys(refs.orderNos))
}
if len(conditions) == 0 {
return nil
}
rows := []model.Order{}
if err := q.db.WithContext(ctx).Where(strings.Join(conditions, " OR "), args...).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联订单失败")
}
for _, row := range rows {
addUint(refs.orders, row.ID)
addString(refs.orderNos, row.OrderNo)
}
return nil
}
// expandPayments 解析支付单、渠道交易号及其明确业务单类型。
func (q *Query) expandPayments(ctx context.Context, refs *financeRefs) error {
conditions, args := make([]string, 0, 5), make([]any, 0, 5)
if len(refs.payments) > 0 {
conditions, args = append(conditions, "id IN ?"), append(args, uintKeys(refs.payments))
}
if len(refs.paymentNos) > 0 {
conditions, args = append(conditions, "payment_no IN ?"), append(args, stringKeys(refs.paymentNos))
}
if len(refs.tradeNos) > 0 {
conditions, args = append(conditions, "third_party_trade_no IN ?"), append(args, stringKeys(refs.tradeNos))
}
if len(refs.orders) > 0 {
conditions, args = append(conditions, "order_type = ? AND order_id IN ?"), append(args, model.PaymentOrderTypePackage, uintKeys(refs.orders))
}
if len(refs.agentRecharges) > 0 {
conditions, args = append(conditions, "order_type = ? AND order_id IN ?"), append(args, model.PaymentOrderTypeAgentRecharge, uintKeys(refs.agentRecharges))
}
if len(refs.rechargeOrders) > 0 {
conditions, args = append(conditions, "order_type = ? AND order_id IN ?"), append(args, model.PaymentOrderTypeRecharge, uintKeys(refs.rechargeOrders))
}
if len(conditions) == 0 {
return nil
}
rows := []model.Payment{}
if err := q.db.WithContext(ctx).Where("("+strings.Join(conditions, ") OR (")+")", args...).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联支付失败")
}
for _, row := range rows {
addUint(refs.payments, row.ID)
addString(refs.paymentNos, row.PaymentNo)
addString(refs.tradeNos, row.ThirdPartyTradeNo)
switch row.OrderType {
case model.PaymentOrderTypePackage:
addUint(refs.orders, row.OrderID)
case model.PaymentOrderTypeAgentRecharge:
addUint(refs.agentRecharges, row.OrderID)
case model.PaymentOrderTypeRecharge:
addUint(refs.rechargeOrders, row.OrderID)
}
}
return nil
}
// expandRefunds 解析退款与订单、审批的稳定关系。
func (q *Query) expandRefunds(ctx context.Context, refs *financeRefs) error {
conditions, args := make([]string, 0, 4), make([]any, 0, 4)
if len(refs.refunds) > 0 {
conditions, args = append(conditions, "id IN ?"), append(args, uintKeys(refs.refunds))
}
if len(refs.refundNos) > 0 {
conditions, args = append(conditions, "refund_no IN ?"), append(args, stringKeys(refs.refundNos))
}
if len(refs.orders) > 0 {
conditions, args = append(conditions, "order_id IN ?"), append(args, uintKeys(refs.orders))
}
if len(refs.approvals) > 0 {
conditions, args = append(conditions, "approval_instance_id IN ?"), append(args, uintKeys(refs.approvals))
}
if len(conditions) == 0 {
return nil
}
rows := []model.RefundRequest{}
if err := q.db.WithContext(ctx).Where(strings.Join(conditions, " OR "), args...).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联退款失败")
}
for _, row := range rows {
addUint(refs.refunds, row.ID)
addString(refs.refundNos, row.RefundNo)
addUint(refs.orders, row.OrderID)
if row.ApprovalInstanceID != nil {
addUint(refs.approvals, *row.ApprovalInstanceID)
}
}
return nil
}
// expandRecharges 同时解析代理充值和个人资产充值两类既有业务单。
func (q *Query) expandRecharges(ctx context.Context, refs *financeRefs) error {
agentConditions, agentArgs := make([]string, 0, 4), make([]any, 0, 4)
if len(refs.agentRecharges) > 0 {
agentConditions, agentArgs = append(agentConditions, "id IN ?"), append(agentArgs, uintKeys(refs.agentRecharges))
}
if len(refs.rechargeNos) > 0 {
agentConditions, agentArgs = append(agentConditions, "recharge_no IN ?"), append(agentArgs, stringKeys(refs.rechargeNos))
}
if len(refs.tradeNos) > 0 {
agentConditions, agentArgs = append(agentConditions, "payment_transaction_id IN ?"), append(agentArgs, stringKeys(refs.tradeNos))
}
if len(refs.approvals) > 0 {
agentConditions, agentArgs = append(agentConditions, "approval_instance_id IN ?"), append(agentArgs, uintKeys(refs.approvals))
}
if len(agentConditions) > 0 {
rows := []model.AgentRechargeRecord{}
if err := q.db.WithContext(ctx).Where(strings.Join(agentConditions, " OR "), agentArgs...).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联代理充值失败")
}
for _, row := range rows {
addUint(refs.agentRecharges, row.ID)
addString(refs.rechargeNos, row.RechargeNo)
if row.ApprovalInstanceID != nil {
addUint(refs.approvals, *row.ApprovalInstanceID)
}
if row.PaymentTransactionID != nil {
addString(refs.tradeNos, *row.PaymentTransactionID)
}
}
}
personalConditions, personalArgs := make([]string, 0, 2), make([]any, 0, 2)
if len(refs.rechargeOrders) > 0 {
personalConditions, personalArgs = append(personalConditions, "id IN ?"), append(personalArgs, uintKeys(refs.rechargeOrders))
}
if len(refs.rechargeNos) > 0 {
personalConditions, personalArgs = append(personalConditions, "recharge_order_no IN ?"), append(personalArgs, stringKeys(refs.rechargeNos))
}
if len(personalConditions) == 0 {
return nil
}
rows := []model.RechargeOrder{}
if err := q.db.WithContext(ctx).Where(strings.Join(personalConditions, " OR "), personalArgs...).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联资产充值失败")
}
for _, row := range rows {
addUint(refs.rechargeOrders, row.ID)
addString(refs.rechargeNos, row.RechargeOrderNo)
}
return nil
}
// expandApprovals 只按审批业务类型关联退款或线下代理充值。
func (q *Query) expandApprovals(ctx context.Context, refs *financeRefs) error {
conditions, args := make([]string, 0, 3), make([]any, 0, 3)
if len(refs.approvals) > 0 {
conditions, args = append(conditions, "id IN ?"), append(args, uintKeys(refs.approvals))
}
if len(refs.refunds) > 0 {
conditions, args = append(conditions, "business_type = ? AND business_id IN ?"), append(args, constants.ApprovalBusinessTypeRefund, uintKeys(refs.refunds))
}
if len(refs.agentRecharges) > 0 {
conditions, args = append(conditions, "business_type = ? AND business_id IN ?"), append(args, constants.ApprovalBusinessTypeOfflineRecharge, uintKeys(refs.agentRecharges))
}
if len(conditions) == 0 {
return nil
}
rows := []model.ApprovalInstance{}
if err := q.db.WithContext(ctx).Where("("+strings.Join(conditions, ") OR (")+")", args...).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金关联审批失败")
}
for _, row := range rows {
addUint(refs.approvals, row.ID)
switch row.BusinessType {
case constants.ApprovalBusinessTypeRefund:
addUint(refs.refunds, row.BusinessID)
case constants.ApprovalBusinessTypeOfflineRecharge:
addUint(refs.agentRecharges, row.BusinessID)
}
}
return nil
}
// loadFinanceAuditRows 只读取具有资金资源的审计事件,并保留操作者权威。
func (q *Query) loadFinanceAuditRows(ctx context.Context, filter FinanceFilter, refs *financeRefs) ([]model.AuditEvent, int64, error) {
resourceTypes := []string{
constants.AuditResourceOrder, constants.AuditResourcePayment, constants.AuditResourceRefund,
constants.AuditResourceAgentRecharge, constants.AuditResourceRechargeOrder,
constants.AuditResourceAgentWallet, constants.AuditResourceAgentWalletTransaction,
constants.AuditResourceAgentWalletReservation, constants.AuditResourceAssetWallet,
constants.AuditResourceAssetWalletTransaction, constants.AuditResourceCommissionRecord,
constants.AuditResourceCommissionWithdrawal, constants.AuditResourceApprovalInstance,
}
query := q.db.WithContext(ctx).Model(&model.AuditEvent{}).
Where("EXISTS (?)", q.db.Table("tb_audit_event_resource AS finance_resource").Select("1").
Where("finance_resource.audit_event_id = tb_audit_event.id").
Where("finance_resource.resource_type IN ?", resourceTypes))
query = applyFinanceTime(query, filter, "occurred_at")
if filter.ActorKind != "" {
query = query.Where("actor_kind = ? AND actor_id = ?", filter.ActorKind, filter.ActorID)
}
if filter.CorrelationID != "" {
query = query.Where("correlation_id = ?", filter.CorrelationID)
}
if refs.seeded {
predicate, args := financeResourcePredicate("matched_resource", refs)
if predicate == "" {
return []model.AuditEvent{}, 0, nil
}
query = query.Where("EXISTS (?)", q.db.Table("tb_audit_event_resource AS matched_resource").Select("1").
Where("matched_resource.audit_event_id = tb_audit_event.id").Where(predicate, args...))
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "统计资金关联审计事件失败")
}
rows := []model.AuditEvent{}
if err := query.Order("occurred_at DESC, event_id DESC").Limit(filter.Page * filter.PageSize).Find(&rows).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询资金关联审计事件失败")
}
return rows, total, nil
}
// addAuditFinanceRefs 从事件资源读取稳定业务 ID不解析摘要或中文描述。
func (q *Query) addAuditFinanceRefs(ctx context.Context, events []model.AuditEvent, refs *financeRefs) error {
if len(events) == 0 {
return nil
}
ids := make([]uint, 0, len(events))
for _, event := range events {
ids = append(ids, event.ID)
}
rows := []model.AuditEventResource{}
if err := q.db.WithContext(ctx).Where("audit_event_id IN ?", ids).Find(&rows).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解析资金审计资源失败")
}
for _, row := range rows {
id, ok := parseResourceUint(row.ResourceID)
switch row.ResourceType {
case constants.AuditResourceShop:
// 店铺只接受显式筛选,避免单笔业务链路扩散成整个店铺资金历史。
case constants.AuditResourceOrder:
if ok {
addUint(refs.orders, id)
}
addString(refs.orderNos, row.ResourceKey)
case constants.AuditResourcePayment:
if ok {
addUint(refs.payments, id)
}
addString(refs.paymentNos, row.ResourceKey)
case constants.AuditResourceRefund:
if ok {
addUint(refs.refunds, id)
}
addString(refs.refundNos, row.ResourceKey)
case constants.AuditResourceAgentRecharge:
if ok {
addUint(refs.agentRecharges, id)
}
addString(refs.rechargeNos, row.ResourceKey)
case constants.AuditResourceRechargeOrder:
if ok {
addUint(refs.rechargeOrders, id)
}
addString(refs.rechargeNos, row.ResourceKey)
case constants.AuditResourceAgentWallet:
// 钱包只接受显式筛选,关联业务使用唯一流水资源继续解析。
case constants.AuditResourceAssetWallet:
// 钱包只接受显式筛选,关联业务使用唯一流水资源继续解析。
case constants.AuditResourceAgentWalletTransaction:
if ok {
addUint(refs.agentTxs, id)
}
case constants.AuditResourceAssetWalletTransaction:
if ok {
addUint(refs.assetTxs, id)
}
case constants.AuditResourceAgentWalletReservation:
if ok {
addUint(refs.reservations, id)
}
case constants.AuditResourceApprovalInstance:
if ok {
addUint(refs.approvals, id)
}
case constants.AuditResourceCommissionRecord:
if ok {
addUint(refs.commissions, id)
}
case constants.AuditResourceCommissionWithdrawal:
if ok {
addUint(refs.withdrawals, id)
}
}
}
return nil
}
// financeResourcePredicate 生成仅包含已注册资金资源的参数化匹配条件。
func financeResourcePredicate(alias string, refs *financeRefs) (string, []any) {
type resourceMatch struct {
resourceType string
ids map[uint]struct{}
keys map[string]struct{}
}
matches := []resourceMatch{
{constants.AuditResourceShop, refs.shops, nil},
{constants.AuditResourceOrder, refs.orders, refs.orderNos},
{constants.AuditResourcePayment, refs.payments, refs.paymentNos},
{constants.AuditResourceRefund, refs.refunds, refs.refundNos},
{constants.AuditResourceAgentRecharge, refs.agentRecharges, refs.rechargeNos},
{constants.AuditResourceRechargeOrder, refs.rechargeOrders, refs.rechargeNos},
{constants.AuditResourceAgentWallet, refs.agentWallets, nil},
{constants.AuditResourceAssetWallet, refs.assetWallets, nil},
{constants.AuditResourceAgentWalletTransaction, refs.agentTxs, nil},
{constants.AuditResourceAssetWalletTransaction, refs.assetTxs, nil},
{constants.AuditResourceAgentWalletReservation, refs.reservations, nil},
{constants.AuditResourceApprovalInstance, refs.approvals, nil},
{constants.AuditResourceCommissionRecord, refs.commissions, nil},
{constants.AuditResourceCommissionWithdrawal, refs.withdrawals, nil},
}
conditions, args := make([]string, 0, len(matches)*2), make([]any, 0, len(matches)*2)
for _, match := range matches {
if len(match.ids) > 0 {
conditions = append(conditions, "("+alias+".resource_type = ? AND "+alias+".resource_id IN ?)")
args = append(args, match.resourceType, stringUintKeys(match.ids))
}
if len(match.keys) > 0 {
conditions = append(conditions, "("+alias+".resource_type = ? AND "+alias+".resource_key IN ?)")
args = append(args, match.resourceType, stringKeys(match.keys))
}
}
return strings.Join(conditions, " OR "), args
}
// loadFinanceNodes 以固定查询数读取各事实源后统一分页,不产生节点级 N+1。
func (q *Query) loadFinanceNodes(ctx context.Context, filter FinanceFilter, refs *financeRefs, auditRows []model.AuditEvent, auditTotal int64, limit int) ([]FinanceTimelineNode, int64, error) {
nodes, err := q.financeAuditNodes(ctx, auditRows)
if err != nil {
return nil, 0, err
}
total := auditTotal
loaders := []func() ([]FinanceTimelineNode, int64, error){
func() ([]FinanceTimelineNode, int64, error) {
return q.loadAgentWalletFinance(ctx, filter, refs, limit)
},
func() ([]FinanceTimelineNode, int64, error) {
return q.loadAssetWalletFinance(ctx, filter, refs, limit)
},
func() ([]FinanceTimelineNode, int64, error) {
return q.loadReservationFinance(ctx, filter, refs, limit)
},
func() ([]FinanceTimelineNode, int64, error) { return q.loadOrderFinance(ctx, filter, refs, limit) },
func() ([]FinanceTimelineNode, int64, error) { return q.loadPaymentFinance(ctx, filter, refs, limit) },
func() ([]FinanceTimelineNode, int64, error) { return q.loadRefundFinance(ctx, filter, refs, limit) },
func() ([]FinanceTimelineNode, int64, error) {
return q.loadAgentRechargeFinance(ctx, filter, refs, limit)
},
func() ([]FinanceTimelineNode, int64, error) {
return q.loadRechargeOrderFinance(ctx, filter, refs, limit)
},
func() ([]FinanceTimelineNode, int64, error) { return q.loadCommissionFinance(ctx, filter, refs, limit) },
func() ([]FinanceTimelineNode, int64, error) { return q.loadWithdrawalFinance(ctx, filter, refs, limit) },
func() ([]FinanceTimelineNode, int64, error) { return q.loadApprovalFinance(ctx, filter, refs, limit) },
}
for _, load := range loaders {
items, count, loadErr := load()
if loadErr != nil {
return nil, 0, loadErr
}
nodes = append(nodes, items...)
total += count
}
return nodes, total, nil
}
// financeAuditNodes 投影操作者事实,明确其金额不是资金权威。
func (q *Query) financeAuditNodes(ctx context.Context, rows []model.AuditEvent) ([]FinanceTimelineNode, error) {
events, err := q.project(ctx, rows)
if err != nil {
return nil, err
}
integrationRows := []model.IntegrationLog{}
ids := make([]uint, 0, len(rows))
for _, row := range rows {
ids = append(ids, row.ID)
}
if len(ids) > 0 {
if err := q.db.WithContext(ctx).Where("audit_event_id IN ?", ids).Find(&integrationRows).Error; err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资金审计外部交互引用失败")
}
}
integrationByAudit := integrationRefsByAuditID(integrationRows)
nodes := make([]FinanceTimelineNode, 0, len(events))
for index, event := range events {
refs := event.InvestigationRefs
refs.IntegrationRefs = uniqueIntegrationRefs(append(refs.IntegrationRefs, integrationByAudit[rows[index].ID]...))
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceAuditEvent, NodeID: event.EventID, OccurredAt: event.OccurredAt,
Code: event.ActionCode, Title: event.ActionName, Result: event.Result, ResultName: auditResultName(event.Result),
AmountAuthority: nonAuthoritativeAuditAmount(),
Facts: map[string]any{"summary": event.Summary, "metadata": event.Metadata, "risk_level": event.RiskLevel},
InvestigationRefs: refs,
})
}
return nodes, nil
}
// loadAgentWalletFinance 读取代理钱包金额与余额权威流水。
func (q *Query) loadAgentWalletFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.AgentWalletTransaction{})
query = applyFinanceTime(query, filter, "created_at")
conditions, args := make([]string, 0, 8), make([]any, 0, 8)
appendUintCondition(&conditions, &args, "id", refs.agentTxs)
appendUintCondition(&conditions, &args, "agent_wallet_id", refs.agentWallets)
appendUintCondition(&conditions, &args, "shop_id", refs.shops)
appendReferenceIDCondition(&conditions, &args, constants.ReferenceTypeOrder, refs.orders)
appendReferenceIDCondition(&conditions, &args, constants.ReferenceTypeRefund, refs.refunds)
appendReferenceIDCondition(&conditions, &args, constants.ReferenceTypeTopup, refs.agentRecharges)
appendReferenceIDCondition(&conditions, &args, constants.ReferenceTypeCommission, refs.commissions)
appendReferenceIDCondition(&conditions, &args, constants.ReferenceTypeWithdrawal, refs.withdrawals)
appendAccountActorCondition(&conditions, &args, filter, "user_id", "creator")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.AgentWalletTransaction](query, "created_at DESC, id DESC", limit, "代理钱包流水")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount, before, after := row.Amount, row.BalanceBefore, row.BalanceAfter
resourceID := strconv.FormatUint(uint64(row.ID), 10)
refsView := ledgerRefs(constants.AuditResourceAgentWalletTransaction, resourceID, resourceID, row.UserID)
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceAgentWallet, row.AgentWalletID, ""))
refsView.ResourceRefs = append(refsView.ResourceRefs, agentTransactionBusinessRef(row)...)
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceAgentWalletTransaction, NodeID: resourceID, OccurredAt: row.CreatedAt,
Code: row.TransactionType, Title: "代理钱包" + constants.GetAgentTransactionTypeName(row.TransactionType),
Result: strconv.Itoa(row.Status), ResultName: constants.GetTransactionStatusName(row.Status),
Amount: &amount, BalanceBefore: &before, BalanceAfter: &after, Currency: "CNY", ShopID: &row.ShopID,
Wallet: &FinanceWalletRef{ResourceType: constants.AuditResourceAgentWallet, WalletID: row.AgentWalletID},
AmountAuthority: authoritativeAmount("tb_agent_wallet_transaction", "amount,balance_before,balance_after"),
Facts: map[string]any{"reference_type": row.ReferenceType, "reference_id": row.ReferenceID, "transaction_subtype": row.TransactionSubtype, "asset_type": row.AssetType, "asset_id": row.AssetID, "asset_identifier": row.AssetIdentifier},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadAssetWalletFinance 读取卡或设备钱包金额与余额权威流水。
func (q *Query) loadAssetWalletFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.AssetWalletTransaction{})
query = applyFinanceTime(query, filter, "created_at")
conditions, args := make([]string, 0, 6), make([]any, 0, 6)
appendUintCondition(&conditions, &args, "id", refs.assetTxs)
appendUintCondition(&conditions, &args, "asset_wallet_id", refs.assetWallets)
appendUintCondition(&conditions, &args, "shop_id_tag", refs.shops)
appendReferenceNoCondition(&conditions, &args, constants.ReferenceTypeOrder, refs.orderNos)
appendReferenceNoCondition(&conditions, &args, constants.ReferenceTypeRefund, refs.refundNos)
appendReferenceNoCondition(&conditions, &args, constants.ReferenceTypeTopup, refs.rechargeNos)
appendReferenceNoCondition(&conditions, &args, constants.ReferenceTypeRecharge, refs.paymentNos)
appendAccountActorCondition(&conditions, &args, filter, "user_id", "creator")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.AssetWalletTransaction](query, "created_at DESC, id DESC", limit, "资产钱包流水")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount, before, after := row.Amount, row.BalanceBefore, row.BalanceAfter
resourceID := strconv.FormatUint(uint64(row.ID), 10)
refsView := ledgerRefs(constants.AuditResourceAssetWalletTransaction, resourceID, resourceID, row.UserID)
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceAssetWallet, row.AssetWalletID, ""))
refsView.ResourceRefs = append(refsView.ResourceRefs, assetTransactionBusinessRef(row)...)
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceAssetWalletTransaction, NodeID: resourceID, OccurredAt: row.CreatedAt,
Code: row.TransactionType, Title: "资产钱包" + assetTransactionTypeName(row.TransactionType),
Result: strconv.Itoa(row.Status), ResultName: constants.GetTransactionStatusName(row.Status),
Amount: &amount, BalanceBefore: &before, BalanceAfter: &after, Currency: "CNY", ShopID: &row.ShopIDTag,
Wallet: &FinanceWalletRef{ResourceType: constants.AuditResourceAssetWallet, WalletID: row.AssetWalletID},
AmountAuthority: authoritativeAmount("tb_asset_wallet_transaction", "amount,balance_before,balance_after"),
Facts: map[string]any{"reference_type": row.ReferenceType, "reference_no": row.ReferenceNo, "resource_type": row.ResourceType, "resource_id": row.ResourceID},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadReservationFinance 读取代理主钱包预占及唯一终态。
func (q *Query) loadReservationFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.AgentWalletReservation{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 4), make([]any, 0, 4)
appendUintCondition(&conditions, &args, "id", refs.reservations)
appendUintCondition(&conditions, &args, "agent_wallet_id", refs.agentWallets)
appendUintCondition(&conditions, &args, "shop_id", refs.shops)
appendReservationReferenceCondition(&conditions, &args, constants.ReferenceTypeOrder, refs.orders)
appendAccountActorCondition(&conditions, &args, filter, "creator")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.AgentWalletReservation](query, "updated_at DESC, id DESC", limit, "代理钱包预占")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.Amount
resourceID := strconv.FormatUint(uint64(row.ID), 10)
refsView := ledgerRefs(constants.AuditResourceAgentWalletReservation, resourceID, row.ReferenceType+":"+strconv.FormatUint(uint64(row.ReferenceID), 10), row.Creator)
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceAgentWallet, row.AgentWalletID, ""))
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceAgentWalletReservation, NodeID: resourceID, OccurredAt: row.UpdatedAt,
Code: row.ReferenceType, Title: "代理钱包资金预占", Result: strconv.Itoa(row.Status), ResultName: reservationStatusName(row.Status),
Amount: &amount, Currency: "CNY", ShopID: &row.ShopID,
Wallet: &FinanceWalletRef{ResourceType: constants.AuditResourceAgentWallet, WalletID: row.AgentWalletID},
AmountAuthority: authoritativeAmount("tb_agent_wallet_reservation", "amount"),
Facts: map[string]any{"reference_type": row.ReferenceType, "reference_id": row.ReferenceID, "completed_at": row.CompletedAt},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadOrderFinance 读取订单金额和当前支付事实。
func (q *Query) loadOrderFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.Order{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 5), make([]any, 0, 5)
appendUintCondition(&conditions, &args, "id", refs.orders)
appendStringCondition(&conditions, &args, "order_no", refs.orderNos)
if len(refs.shops) > 0 {
conditions = append(conditions, "(buyer_type = ? AND buyer_id IN ?) OR seller_shop_id IN ?")
args = append(args, model.BuyerTypeAgent, uintKeys(refs.shops), uintKeys(refs.shops))
}
appendAccountActorCondition(&conditions, &args, filter, "operator_account_id")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.Order](query, "updated_at DESC, id DESC", limit, "订单资金事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.TotalAmount
shopID := orderShopID(row)
refsView := ledgerRefs(constants.AuditResourceOrder, strconv.FormatUint(uint64(row.ID), 10), row.OrderNo, pointerUintValue(row.OperatorAccountID))
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceOrder, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: row.PaymentMethod, Title: "订单 " + row.OrderNo, Result: strconv.Itoa(row.PaymentStatus), ResultName: constants.GetOrderPaymentStatusName(row.PaymentStatus),
Amount: &amount, Currency: "CNY", ShopID: shopID,
AmountAuthority: authoritativeAmount("tb_order", "total_amount"),
Facts: map[string]any{"order_no": row.OrderNo, "actual_paid_amount": row.ActualPaidAmount, "payment_method": row.PaymentMethod, "buyer_type": row.BuyerType, "buyer_id": row.BuyerID, "commission_status": row.CommissionStatus, "commission_result": row.CommissionResult},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadPaymentFinance 读取支付金额、渠道交易号和当前支付状态。
func (q *Query) loadPaymentFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.Payment{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 6), make([]any, 0, 6)
appendUintCondition(&conditions, &args, "id", refs.payments)
appendStringCondition(&conditions, &args, "payment_no", refs.paymentNos)
appendStringCondition(&conditions, &args, "third_party_trade_no", refs.tradeNos)
appendTypedOrderCondition(&conditions, &args, model.PaymentOrderTypePackage, refs.orders)
appendTypedOrderCondition(&conditions, &args, model.PaymentOrderTypeAgentRecharge, refs.agentRecharges)
appendTypedOrderCondition(&conditions, &args, model.PaymentOrderTypeRecharge, refs.rechargeOrders)
if filter.ShopID != 0 {
conditions = append(conditions, `(order_type = ? AND EXISTS (SELECT 1 FROM tb_order o WHERE o.id = tb_payment.order_id AND o.deleted_at IS NULL AND ((o.buyer_type = ? AND o.buyer_id = ?) OR o.seller_shop_id = ?))) OR (order_type = ? AND EXISTS (SELECT 1 FROM tb_agent_recharge_record ar WHERE ar.id = tb_payment.order_id AND ar.deleted_at IS NULL AND ar.shop_id = ?)) OR (order_type = ? AND EXISTS (SELECT 1 FROM tb_recharge_order ro WHERE ro.id = tb_payment.order_id AND ro.deleted_at IS NULL AND ro.shop_id_tag = ?))`)
args = append(args, model.PaymentOrderTypePackage, model.BuyerTypeAgent, filter.ShopID, filter.ShopID, model.PaymentOrderTypeAgentRecharge, filter.ShopID, model.PaymentOrderTypeRecharge, filter.ShopID)
}
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.Payment](query, "updated_at DESC, id DESC", limit, "支付资金事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.Amount
refsView := ledgerRefs(constants.AuditResourcePayment, strconv.FormatUint(uint64(row.ID), 10), row.PaymentNo, 0)
refsView.ResourceRefs = append(refsView.ResourceRefs, paymentBusinessRefs(row)...)
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourcePayment, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: row.PaymentMethod, Title: "支付单 " + row.PaymentNo, Result: strconv.Itoa(row.Status), ResultName: constants.GetPaymentRecordStatusName(row.Status),
Amount: &amount, Currency: "CNY", AmountAuthority: authoritativeAmount("tb_payment", "amount"),
Facts: map[string]any{"payment_no": row.PaymentNo, "order_type": row.OrderType, "order_id": row.OrderID, "third_party_trade_no": row.ThirdPartyTradeNo, "paid_at": row.PaidAt},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadRefundFinance 优先展示已批准金额,否则展示申请金额并声明实际字段。
func (q *Query) loadRefundFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.RefundRequest{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 6), make([]any, 0, 6)
appendUintCondition(&conditions, &args, "id", refs.refunds)
appendStringCondition(&conditions, &args, "refund_no", refs.refundNos)
appendUintCondition(&conditions, &args, "order_id", refs.orders)
appendUintCondition(&conditions, &args, "approval_instance_id", refs.approvals)
appendUintCondition(&conditions, &args, "shop_id", refs.shops)
appendAccountActorCondition(&conditions, &args, filter, "processor_id")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.RefundRequest](query, "updated_at DESC, id DESC", limit, "退款资金事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount, field := row.RequestedRefundAmount, "requested_refund_amount"
if row.ApprovedRefundAmount != nil {
amount, field = *row.ApprovedRefundAmount, "approved_refund_amount"
}
refsView := ledgerRefs(constants.AuditResourceRefund, strconv.FormatUint(uint64(row.ID), 10), row.RefundNo, pointerUintValue(row.ProcessorID))
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceOrder, row.OrderID, row.OrderNo))
if row.ApprovalInstanceID != nil {
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceApprovalInstance, *row.ApprovalInstanceID, ""))
}
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceRefund, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: "refund", Title: "退款单 " + row.RefundNo, Result: strconv.Itoa(row.Status), ResultName: constants.GetRefundStatusName(row.Status),
Amount: &amount, Currency: "CNY", ShopID: row.ShopID, AmountAuthority: authoritativeAmount("tb_refund_request", field),
Facts: map[string]any{"refund_no": row.RefundNo, "order_id": row.OrderID, "order_no": row.OrderNo, "actual_received_amount": row.ActualReceivedAmount, "requested_refund_amount": row.RequestedRefundAmount, "approved_refund_amount": row.ApprovedRefundAmount, "approval_instance_id": row.ApprovalInstanceID},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadAgentRechargeFinance 读取代理充值金额、支付和审批事实。
func (q *Query) loadAgentRechargeFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.AgentRechargeRecord{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 7), make([]any, 0, 7)
appendUintCondition(&conditions, &args, "id", refs.agentRecharges)
appendStringCondition(&conditions, &args, "recharge_no", refs.rechargeNos)
appendStringCondition(&conditions, &args, "payment_transaction_id", refs.tradeNos)
appendUintCondition(&conditions, &args, "agent_wallet_id", refs.agentWallets)
appendUintCondition(&conditions, &args, "approval_instance_id", refs.approvals)
appendUintCondition(&conditions, &args, "shop_id", refs.shops)
appendAccountActorCondition(&conditions, &args, filter, "user_id")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.AgentRechargeRecord](query, "updated_at DESC, id DESC", limit, "代理充值事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.Amount
refsView := ledgerRefs(constants.AuditResourceAgentRecharge, strconv.FormatUint(uint64(row.ID), 10), row.RechargeNo, row.UserID)
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceAgentWallet, row.AgentWalletID, ""))
if row.ApprovalInstanceID != nil {
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceApprovalInstance, *row.ApprovalInstanceID, ""))
}
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceAgentRecharge, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: row.PaymentMethod, Title: "代理充值单 " + row.RechargeNo, Result: strconv.Itoa(row.Status), ResultName: constants.GetRechargeStatusName(row.Status),
Amount: &amount, Currency: "CNY", ShopID: &row.ShopID,
Wallet: &FinanceWalletRef{ResourceType: constants.AuditResourceAgentWallet, WalletID: row.AgentWalletID},
AmountAuthority: authoritativeAmount("tb_agent_recharge_record", "amount"),
Facts: map[string]any{"recharge_no": row.RechargeNo, "payment_method": row.PaymentMethod, "payment_transaction_id": row.PaymentTransactionID, "approval_instance_id": row.ApprovalInstanceID, "paid_at": row.PaidAt, "completed_at": row.CompletedAt},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadRechargeOrderFinance 读取个人资产充值金额及自动购包状态。
func (q *Query) loadRechargeOrderFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.RechargeOrder{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 5), make([]any, 0, 5)
appendUintCondition(&conditions, &args, "id", refs.rechargeOrders)
appendStringCondition(&conditions, &args, "recharge_order_no", refs.rechargeNos)
appendUintCondition(&conditions, &args, "asset_wallet_id", refs.assetWallets)
appendUintCondition(&conditions, &args, "shop_id_tag", refs.shops)
appendAccountActorCondition(&conditions, &args, filter, "user_id")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.RechargeOrder](query, "updated_at DESC, id DESC", limit, "资产充值事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.Amount
refsView := ledgerRefs(constants.AuditResourceRechargeOrder, strconv.FormatUint(uint64(row.ID), 10), row.RechargeOrderNo, row.UserID)
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceAssetWallet, row.AssetWalletID, ""))
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceRechargeOrder, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: row.OperatorType, Title: "资产充值单 " + row.RechargeOrderNo, Result: strconv.Itoa(row.Status), ResultName: rechargeOrderStatusName(row.Status),
Amount: &amount, Currency: "CNY", ShopID: &row.ShopIDTag,
Wallet: &FinanceWalletRef{ResourceType: constants.AuditResourceAssetWallet, WalletID: row.AssetWalletID},
AmountAuthority: authoritativeAmount("tb_recharge_order", "amount"),
Facts: map[string]any{"recharge_order_no": row.RechargeOrderNo, "resource_type": row.ResourceType, "resource_id": row.ResourceID, "paid_at": row.PaidAt, "auto_purchase_status": row.AutoPurchaseStatus},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadCommissionFinance 读取佣金金额、状态和入账后余额。
func (q *Query) loadCommissionFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.CommissionRecord{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 4), make([]any, 0, 4)
appendUintCondition(&conditions, &args, "id", refs.commissions)
appendUintCondition(&conditions, &args, "order_id", refs.orders)
appendUintCondition(&conditions, &args, "shop_id", refs.shops)
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.CommissionRecord](query, "updated_at DESC, id DESC", limit, "佣金事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.Amount
refsView := ledgerRefs(constants.AuditResourceCommissionRecord, strconv.FormatUint(uint64(row.ID), 10), strconv.FormatUint(uint64(row.ID), 10), 0)
refsView.ResourceRefs = append(refsView.ResourceRefs, financeResourceRef(constants.AuditResourceOrder, row.OrderID, ""))
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceCommissionRecord, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: row.CommissionSource, Title: "佣金记录", Result: strconv.Itoa(row.Status), ResultName: constants.GetCommissionRecordStatusName(row.Status),
Amount: &amount, BalanceAfter: int64Pointer(row.BalanceAfter), Currency: "CNY", ShopID: &row.ShopID,
AmountAuthority: authoritativeAmount("tb_commission_record", "amount,balance_after"),
Facts: map[string]any{"order_id": row.OrderID, "commission_source": row.CommissionSource, "released_at": row.ReleasedAt},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadWithdrawalFinance 读取提现申请金额、手续费和实际到账金额。
func (q *Query) loadWithdrawalFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{})
query = applyFinanceTime(query, filter, "updated_at")
conditions, args := make([]string, 0, 4), make([]any, 0, 4)
appendUintCondition(&conditions, &args, "id", refs.withdrawals)
appendUintCondition(&conditions, &args, "shop_id", refs.shops)
appendAccountActorCondition(&conditions, &args, filter, "applicant_id", "processor_id")
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.CommissionWithdrawalRequest](query, "updated_at DESC, id DESC", limit, "佣金提现事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
amount := row.Amount
refsView := ledgerRefs(constants.AuditResourceCommissionWithdrawal, strconv.FormatUint(uint64(row.ID), 10), row.WithdrawalNo, row.ApplicantID)
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceCommissionWithdrawal, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.UpdatedAt,
Code: row.WithdrawalMethod, Title: "佣金提现单 " + row.WithdrawalNo, Result: strconv.Itoa(row.Status), ResultName: constants.GetWithdrawalStatusName(row.Status),
Amount: &amount, Currency: "CNY", ShopID: &row.ShopID,
AmountAuthority: authoritativeAmount("tb_commission_withdrawal_request", "amount"),
Facts: map[string]any{"fee": row.Fee, "actual_amount": row.ActualAmount, "payment_type": row.PaymentType, "processed_at": row.ProcessedAt, "paid_at": row.PaidAt},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
// loadApprovalFinance 读取审批状态,明确审批表不提供金额权威。
func (q *Query) loadApprovalFinance(ctx context.Context, filter FinanceFilter, refs *financeRefs, limit int) ([]FinanceTimelineNode, int64, error) {
query := q.db.WithContext(ctx).Model(&model.ApprovalInstance{})
query = applyFinanceTime(query, filter, "status_changed_at")
conditions, args := make([]string, 0, 5), make([]any, 0, 5)
appendUintCondition(&conditions, &args, "id", refs.approvals)
appendApprovalBusinessCondition(&conditions, &args, constants.ApprovalBusinessTypeRefund, refs.refunds)
appendApprovalBusinessCondition(&conditions, &args, constants.ApprovalBusinessTypeOfflineRecharge, refs.agentRecharges)
if filter.ShopID != 0 {
conditions = append(conditions, `(business_type = ? AND EXISTS (SELECT 1 FROM tb_refund_request r WHERE r.id = tb_approval_instance.business_id AND r.deleted_at IS NULL AND r.shop_id = ?)) OR (business_type = ? AND EXISTS (SELECT 1 FROM tb_agent_recharge_record ar WHERE ar.id = tb_approval_instance.business_id AND ar.deleted_at IS NULL AND ar.shop_id = ?))`)
args = append(args, constants.ApprovalBusinessTypeRefund, filter.ShopID, constants.ApprovalBusinessTypeOfflineRecharge, filter.ShopID)
}
appendAccountActorCondition(&conditions, &args, filter, "submitter_account_id")
if filter.CorrelationID != "" {
conditions, args = append(conditions, "correlation_id = ?"), append(args, filter.CorrelationID)
}
query = applyFinanceRelationship(query, filter, conditions, args)
rows, total, err := loadFinanceRows[model.ApprovalInstance](query, "status_changed_at DESC, id DESC", limit, "审批事实")
if err != nil {
return nil, 0, err
}
nodes := make([]FinanceTimelineNode, 0, len(rows))
for _, row := range rows {
refsView := ledgerRefs(constants.AuditResourceApprovalInstance, strconv.FormatUint(uint64(row.ID), 10), strconv.FormatUint(uint64(row.ID), 10), row.SubmitterAccountID)
refsView.CorrelationID = stringPointer(row.CorrelationID)
refsView.ResourceRefs = append(refsView.ResourceRefs, approvalBusinessRefs(row)...)
nodes = append(nodes, FinanceTimelineNode{
RecordSource: constants.AuditRecordSourceApprovalInstance, NodeID: strconv.FormatUint(uint64(row.ID), 10), OccurredAt: row.StatusChangedAt,
Code: row.BusinessType, Title: "审批实例", Result: strconv.Itoa(row.Status), ResultName: constants.GetApprovalStatusName(row.Status),
AmountAuthority: FinanceAmountAuthority{Authoritative: false, Table: "tb_approval_instance", ConflictRule: "审批表只对审批状态负责,不提供金额权威"},
Facts: map[string]any{"business_type": row.BusinessType, "business_id": row.BusinessID, "provider": row.Provider, "external_ref": row.ExternalRef, "correlation_id": row.CorrelationID},
InvestigationRefs: refsView,
})
}
return nodes, total, nil
}
func loadFinanceRows[T any](query *gorm.DB, order string, limit int, sourceName string) ([]T, int64, error) {
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "统计"+sourceName+"失败")
}
rows := make([]T, 0, limit)
if err := query.Order(order).Limit(limit).Find(&rows).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeDatabaseError, err, "查询"+sourceName+"失败")
}
return rows, total, nil
}
func applyFinanceTime(query *gorm.DB, filter FinanceFilter, column string) *gorm.DB {
if filter.CreatedFrom != nil {
query = query.Where(column+" >= ?", filter.CreatedFrom.UTC())
}
if filter.CreatedTo != nil {
query = query.Where(column+" < ?", filter.CreatedTo.UTC())
}
return query
}
func applyFinanceRelationship(query *gorm.DB, filter FinanceFilter, conditions []string, args []any) *gorm.DB {
if !financeRelationshipRequired(filter) {
return query
}
if len(conditions) == 0 {
return query.Where("1 = 0")
}
return query.Where("("+strings.Join(conditions, ") OR (")+")", args...)
}
func financeRelationshipRequired(filter FinanceFilter) bool {
return filter.ShopID != 0 || filter.WalletID != 0 || filter.OrderID != 0 || filter.OrderNo != "" ||
filter.PaymentID != 0 || filter.PaymentNo != "" || filter.RefundID != 0 || filter.RefundNo != "" ||
filter.RechargeID != 0 || filter.RechargeNo != "" || filter.ApprovalInstanceID != 0 ||
filter.ThirdPartyTradeNo != "" || filter.ActorKind != "" || filter.CorrelationID != ""
}
func appendUintCondition(conditions *[]string, args *[]any, column string, values map[uint]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, column+" IN ?")
*args = append(*args, uintKeys(values))
}
func appendStringCondition(conditions *[]string, args *[]any, column string, values map[string]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, column+" IN ?")
*args = append(*args, stringKeys(values))
}
func appendReferenceIDCondition(conditions *[]string, args *[]any, referenceType string, values map[uint]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, "reference_type = ? AND reference_id IN ?")
*args = append(*args, referenceType, uintKeys(values))
}
func appendReferenceNoCondition(conditions *[]string, args *[]any, referenceType string, values map[string]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, "reference_type = ? AND reference_no IN ?")
*args = append(*args, referenceType, stringKeys(values))
}
func appendReservationReferenceCondition(conditions *[]string, args *[]any, referenceType string, values map[uint]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, "reference_type = ? AND reference_id IN ?")
*args = append(*args, referenceType, uintKeys(values))
}
func appendTypedOrderCondition(conditions *[]string, args *[]any, orderType string, values map[uint]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, "order_type = ? AND order_id IN ?")
*args = append(*args, orderType, uintKeys(values))
}
func appendApprovalBusinessCondition(conditions *[]string, args *[]any, businessType string, values map[uint]struct{}) {
if len(values) == 0 {
return
}
*conditions = append(*conditions, "business_type = ? AND business_id IN ?")
*args = append(*args, businessType, uintKeys(values))
}
func appendAccountActorCondition(conditions *[]string, args *[]any, filter FinanceFilter, columns ...string) {
if filter.ActorKind != constants.AuditActorAccount || filter.ActorID == "" {
return
}
actorID, err := strconv.ParseUint(filter.ActorID, 10, 64)
if err != nil {
return
}
for _, column := range columns {
*conditions = append(*conditions, column+" = ?")
*args = append(*args, uint(actorID))
}
}
func ledgerRefs(resourceType, resourceID, resourceKey string, actorID uint) InvestigationRefs {
refs := InvestigationRefs{
ResourceRefs: []InvestigationResourceRef{{ResourceType: resourceType, ResourceID: stringPointer(resourceID), ResourceKey: resourceKey, DisplayName: resourceKey}},
IntegrationRefs: []IntegrationRef{},
}
if actorID != 0 {
refs.ActorRef = &ActorRef{Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(actorID), 10)}
}
return refs
}
func financeResourceRef(resourceType string, resourceID uint, resourceKey string) InvestigationResourceRef {
id := strconv.FormatUint(uint64(resourceID), 10)
if resourceKey == "" {
resourceKey = id
}
return InvestigationResourceRef{ResourceType: resourceType, ResourceID: &id, ResourceKey: resourceKey, DisplayName: resourceKey}
}
// agentTransactionBusinessRef 将显式 reference_type 转为业务资源引用。
func agentTransactionBusinessRef(row model.AgentWalletTransaction) []InvestigationResourceRef {
if row.ReferenceType == nil || row.ReferenceID == nil {
return nil
}
resourceType := ""
switch *row.ReferenceType {
case constants.ReferenceTypeOrder:
resourceType = constants.AuditResourceOrder
case constants.ReferenceTypeRefund:
resourceType = constants.AuditResourceRefund
case constants.ReferenceTypeTopup:
resourceType = constants.AuditResourceAgentRecharge
case constants.ReferenceTypeCommission:
resourceType = constants.AuditResourceCommissionRecord
case constants.ReferenceTypeWithdrawal:
resourceType = constants.AuditResourceCommissionWithdrawal
}
if resourceType == "" {
return nil
}
return []InvestigationResourceRef{financeResourceRef(resourceType, *row.ReferenceID, "")}
}
// assetTransactionBusinessRef 保留只有业务编号而没有内部 ID 的历史引用。
func assetTransactionBusinessRef(row model.AssetWalletTransaction) []InvestigationResourceRef {
if row.ReferenceType == nil || row.ReferenceNo == nil || *row.ReferenceNo == "" {
return nil
}
resourceType := ""
switch *row.ReferenceType {
case constants.ReferenceTypeOrder:
resourceType = constants.AuditResourceOrder
case constants.ReferenceTypeRefund:
resourceType = constants.AuditResourceRefund
case constants.ReferenceTypeRecharge:
resourceType = constants.AuditResourcePayment
}
if resourceType == "" {
return nil
}
return []InvestigationResourceRef{{ResourceType: resourceType, ResourceKey: *row.ReferenceNo, DisplayName: *row.ReferenceNo}}
}
// paymentBusinessRefs 按支付单声明的 order_type 定位业务单类型。
func paymentBusinessRefs(row model.Payment) []InvestigationResourceRef {
resourceType := ""
switch row.OrderType {
case model.PaymentOrderTypePackage:
resourceType = constants.AuditResourceOrder
case model.PaymentOrderTypeAgentRecharge:
resourceType = constants.AuditResourceAgentRecharge
case model.PaymentOrderTypeRecharge:
resourceType = constants.AuditResourceRechargeOrder
}
if resourceType == "" {
return nil
}
return []InvestigationResourceRef{financeResourceRef(resourceType, row.OrderID, "")}
}
// approvalBusinessRefs 按审批实例声明的业务类型生成稳定跳转。
func approvalBusinessRefs(row model.ApprovalInstance) []InvestigationResourceRef {
resourceType := ""
switch row.BusinessType {
case constants.ApprovalBusinessTypeRefund:
resourceType = constants.AuditResourceRefund
case constants.ApprovalBusinessTypeOfflineRecharge:
resourceType = constants.AuditResourceAgentRecharge
}
if resourceType == "" {
return nil
}
return []InvestigationResourceRef{financeResourceRef(resourceType, row.BusinessID, "")}
}
func authoritativeAmount(table, field string) FinanceAmountAuthority {
return FinanceAmountAuthority{
Authoritative: true, Table: table, Field: field,
ConflictRule: "金额冲突时以该业务表字段为准,不修改历史 Audit Event",
}
}
func nonAuthoritativeAuditAmount() FinanceAmountAuthority {
return FinanceAmountAuthority{
Authoritative: false, Table: "tb_audit_event",
ConflictRule: "Audit Event 只解释操作者与动作,金额以钱包流水及对应业务表为准",
}
}
func auditResultName(result string) string {
switch result {
case constants.AuditResultSuccess:
return "成功"
case constants.AuditResultFailed:
return "失败"
case constants.AuditResultDenied:
return "拒绝"
case constants.AuditResultPartial:
return "部分成功"
case constants.AuditResultUnknown:
return "结果未知"
default:
return "未知"
}
}
func reservationStatusName(status int) string {
switch status {
case constants.AgentWalletReservationStatusFrozen:
return "已冻结"
case constants.AgentWalletReservationStatusReleased:
return "已释放"
case constants.AgentWalletReservationStatusCompleted:
return "已完成扣除"
default:
return "未知"
}
}
func assetTransactionTypeName(transactionType string) string {
switch transactionType {
case constants.AssetTransactionTypeRecharge:
return "充值"
case constants.AssetTransactionTypeDeduct:
return "扣款"
case constants.AssetTransactionTypeRefund:
return "退款"
case constants.AssetTransactionTypeExchange:
return "换货迁移"
default:
return "未知变动"
}
}
func rechargeOrderStatusName(status int) string {
switch status {
case model.RechargeOrderStatusPending:
return "待支付"
case model.RechargeOrderStatusPaid:
return "已支付"
case model.RechargeOrderStatusClosed:
return "已关闭"
case model.RechargeOrderStatusRefunded:
return "已退款"
default:
return "未知"
}
}
func orderShopID(row model.Order) *uint {
if row.BuyerType == model.BuyerTypeAgent {
id := row.BuyerID
return &id
}
return row.SellerShopID
}
func pointerUintValue(value *uint) uint {
if value == nil {
return 0
}
return *value
}
func int64Pointer(value int64) *int64 {
return &value
}
func parseResourceUint(value *string) (uint, bool) {
if value == nil || *value == "" {
return 0, false
}
parsed, err := strconv.ParseUint(*value, 10, 64)
if err != nil {
return 0, false
}
return uint(parsed), true
}
func addUint(values map[uint]struct{}, value uint) {
if value != 0 {
values[value] = struct{}{}
}
}
func addString(values map[string]struct{}, value string) {
value = strings.TrimSpace(value)
if value != "" {
values[value] = struct{}{}
}
}
func uintKeys(values map[uint]struct{}) []uint {
keys := make([]uint, 0, len(values))
for value := range values {
keys = append(keys, value)
}
return keys
}
func stringUintKeys(values map[uint]struct{}) []string {
keys := make([]string, 0, len(values))
for value := range values {
keys = append(keys, strconv.FormatUint(uint64(value), 10))
}
return keys
}
func stringKeys(values map[string]struct{}) []string {
keys := make([]string, 0, len(values))
for value := range values {
keys = append(keys, value)
}
return keys
}
func financeNodeIDAfter(left, right string) bool {
leftID, leftErr := strconv.ParseUint(left, 10, 64)
rightID, rightErr := strconv.ParseUint(right, 10, 64)
if leftErr == nil && rightErr == nil {
return leftID > rightID
}
return left > right
}