暂存一下,防止丢失

This commit is contained in:
2026-07-24 16:07:18 +08:00
parent 5d6e23f1a5
commit a18ed8bc8d
180 changed files with 13597 additions and 1986 deletions

View File

@@ -0,0 +1,59 @@
package wallet
import (
"context"
"github.com/bytedance/sonic"
"gorm.io/gorm"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// CreditEventConsumer 校验已投递的代理主钱包入账事件具有对应权威资金流水。
type CreditEventConsumer struct {
db *gorm.DB
}
// NewCreditEventConsumer 创建代理主钱包入账事件消费者。
func NewCreditEventConsumer(db *gorm.DB) *CreditEventConsumer {
return &CreditEventConsumer{db: db}
}
// Consume 校验事件载荷及不可变流水,拒绝确认未知或损坏事件。
func (c *CreditEventConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
if c == nil || c.db == nil {
return errors.New(errors.CodeInternalError, "代理主钱包入账事件消费者未配置")
}
if envelope.EventType != constants.OutboxEventTypeAgentMainWalletCredited || envelope.PayloadVersion != constants.AgentMainWalletCreditedPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "代理主钱包入账事件类型或版本不受支持")
}
var event walletapp.CreditedEvent
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
return errors.Wrap(errors.CodeInvalidParam, err, "代理主钱包入账事件载荷无法解析")
}
validRecharge := event.ReferenceType == constants.ReferenceTypeTopup && event.TransactionType == constants.AgentTransactionTypeRecharge
validAdjustment := event.ReferenceType == constants.ReferenceTypeManualAdjustment && event.TransactionType == constants.AgentTransactionTypeAdjustment
if event.EventID != envelope.EventID || event.WalletID == 0 || event.ReferenceID == 0 || event.Amount <= 0 || (!validRecharge && !validAdjustment) {
return errors.New(errors.CodeInvalidParam, "代理主钱包入账事件载荷不完整")
}
var transaction model.AgentWalletTransaction
err := c.db.WithContext(ctx).Unscoped().
Where("agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
event.WalletID, event.ReferenceType, event.ReferenceID, event.TransactionType, constants.TransactionStatusSuccess).
First(&transaction).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeInternalError, "代理主钱包入账事件缺少权威资金流水")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包入账流水失败")
}
if transaction.Amount != event.Amount || transaction.BalanceBefore != event.BalanceBefore || transaction.BalanceAfter != event.BalanceAfter {
return errors.New(errors.CodeInternalError, "代理主钱包入账事件与权威资金流水不一致")
}
return nil
}

View File

@@ -0,0 +1,37 @@
package wallet
import (
"context"
"strconv"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
// CreditEventWriter 将代理主钱包正向入账事实写入公共 Outbox。
type CreditEventWriter struct {
outbox *outbox.Repository
}
// NewCreditEventWriter 创建代理主钱包入账 Outbox Writer。
func NewCreditEventWriter(repository *outbox.Repository) *CreditEventWriter {
return &CreditEventWriter{outbox: repository}
}
// Append 在调用方业务事务中追加代理主钱包入账事件。
func (w *CreditEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.CreditedEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "代理主钱包入账 Outbox Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentMainWalletCredited,
PayloadVersion: constants.AgentMainWalletCreditedPayloadVersionV1,
AggregateType: "agent_wallet", AggregateID: strconv.FormatUint(uint64(event.WalletID), 10),
ResourceType: event.ReferenceType, ResourceID: strconv.FormatUint(uint64(event.ReferenceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
return err
}

View File

@@ -0,0 +1,62 @@
package wallet
import (
"context"
"github.com/bytedance/sonic"
"gorm.io/gorm"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// DebitEventConsumer 校验已投递的代理主钱包扣款事件具有对应权威资金流水。
// 后续余额预警等消费者在此稳定接缝上扩展,不需要回读或改写订单扣款事务。
type DebitEventConsumer struct {
db *gorm.DB
}
// NewDebitEventConsumer 创建代理主钱包扣款事件消费者。
func NewDebitEventConsumer(db *gorm.DB) *DebitEventConsumer {
return &DebitEventConsumer{db: db}
}
// Consume 校验事件载荷及不可变流水,保证未知或损坏事件不会被静默确认。
func (c *DebitEventConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
if c == nil || c.db == nil {
return errors.New(errors.CodeInternalError, "代理主钱包扣款事件消费者未配置")
}
if envelope.EventType != constants.OutboxEventTypeAgentMainWalletDebited ||
envelope.PayloadVersion != constants.AgentMainWalletDebitedPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "代理主钱包扣款事件类型或版本不受支持")
}
var event walletapp.DebitedEvent
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
return errors.Wrap(errors.CodeInvalidParam, err, "代理主钱包扣款事件载荷无法解析")
}
if event.EventID != envelope.EventID || event.WalletID == 0 || event.ReferenceID == 0 ||
event.ReferenceType != constants.ReferenceTypeOrder || event.TransactionType != constants.AgentTransactionTypeDeduct ||
event.Amount <= 0 {
return errors.New(errors.CodeInvalidParam, "代理主钱包扣款事件载荷不完整")
}
var transaction model.AgentWalletTransaction
err := c.db.WithContext(ctx).Unscoped().
Where("agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
event.WalletID, event.ReferenceType, event.ReferenceID, constants.AgentTransactionTypeDeduct, constants.TransactionStatusSuccess).
First(&transaction).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeInternalError, "代理主钱包扣款事件缺少权威资金流水")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包扣款流水失败")
}
if transaction.Amount != -event.Amount || transaction.BalanceBefore != event.BalanceBefore ||
transaction.BalanceAfter != event.BalanceAfter {
return errors.New(errors.CodeInternalError, "代理主钱包扣款事件与权威资金流水不一致")
}
return nil
}

View File

@@ -0,0 +1,38 @@
// Package wallet 提供代理主钱包持久化与可靠事件 Adapter。
package wallet
import (
"context"
"strconv"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
// DebitEventWriter 将代理主钱包扣款事实写入公共 Outbox。
type DebitEventWriter struct {
outbox *outbox.Repository
}
// NewDebitEventWriter 创建代理主钱包扣款 Outbox Writer。
func NewDebitEventWriter(repository *outbox.Repository) *DebitEventWriter {
return &DebitEventWriter{outbox: repository}
}
// Append 在调用方业务事务中追加代理主钱包扣款事件。
func (w *DebitEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.DebitedEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "代理主钱包扣款 Outbox Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentMainWalletDebited,
PayloadVersion: constants.AgentMainWalletDebitedPayloadVersionV1,
AggregateType: "agent_wallet", AggregateID: strconv.FormatUint(uint64(event.WalletID), 10),
ResourceType: event.ReferenceType, ResourceID: strconv.FormatUint(uint64(event.ReferenceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
return err
}

View File

@@ -0,0 +1,98 @@
package wallet
import (
"context"
"math"
"github.com/bytedance/sonic"
"gorm.io/gorm"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// RefundEventConsumer 校验已投递的代理主钱包退款事件具有对应权威资金流水。
type RefundEventConsumer struct {
db *gorm.DB
}
// NewRefundEventConsumer 创建代理主钱包退款事件消费者。
func NewRefundEventConsumer(db *gorm.DB) *RefundEventConsumer {
return &RefundEventConsumer{db: db}
}
// Consume 复核退款流水及正常订单的原扣款流水,拒绝确认未知或损坏事件。
func (c *RefundEventConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
if c == nil || c.db == nil {
return errors.New(errors.CodeInternalError, "代理主钱包退款事件消费者未配置")
}
if envelope.EventType != constants.OutboxEventTypeAgentMainWalletRefunded ||
envelope.PayloadVersion != constants.AgentMainWalletRefundedPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "代理主钱包退款事件类型或版本不受支持")
}
var event walletapp.RefundedEvent
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
return errors.Wrap(errors.CodeInvalidParam, err, "代理主钱包退款事件载荷无法解析")
}
if event.EventID != envelope.EventID || event.WalletID == 0 || event.ShopID == 0 ||
event.OrderID == 0 || event.RefundID == 0 || event.Amount <= 0 || event.Version <= 0 ||
event.OriginalDeductAmount <= 0 || event.Amount > event.OriginalDeductAmount {
return errors.New(errors.CodeInvalidParam, "代理主钱包退款事件载荷不完整")
}
if event.Legacy == (event.OriginalDebitTransactionID > 0) {
return errors.New(errors.CodeInvalidParam, "代理主钱包退款事件原扣款标识不一致")
}
var refund model.AgentWalletTransaction
err := c.db.WithContext(ctx).Unscoped().
Where("agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
event.WalletID, constants.ReferenceTypeRefund, event.RefundID, constants.AgentTransactionTypeRefund, constants.TransactionStatusSuccess).
First(&refund).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeInternalError, "代理主钱包退款事件缺少权威资金流水")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包退款流水失败")
}
if refund.ShopID != event.ShopID || refund.Amount != event.Amount ||
refund.BalanceBefore != event.BalanceBefore || refund.BalanceAfter != event.BalanceAfter ||
!sameRefundUint(refund.RelatedShopID, event.RelatedShopID) ||
!sameRefundString(refund.TransactionSubtype, event.TransactionSubtype) ||
refund.AssetType != event.AssetType || refund.AssetID != event.AssetID || refund.AssetIdentifier != event.AssetIdentifier {
return errors.New(errors.CodeInternalError, "代理主钱包退款事件与权威退款流水不一致")
}
if event.Legacy {
return nil
}
var debit model.AgentWalletTransaction
err = c.db.WithContext(ctx).Unscoped().
Where("id = ? AND agent_wallet_id = ? AND reference_type = ? AND reference_id = ? AND transaction_type = ? AND status = ?",
event.OriginalDebitTransactionID, event.WalletID, constants.ReferenceTypeOrder, event.OrderID,
constants.AgentTransactionTypeDeduct, constants.TransactionStatusSuccess).
First(&debit).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeInternalError, "代理主钱包退款事件缺少原扣款流水")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包原扣款流水失败")
}
if debit.Amount >= 0 || debit.Amount == math.MinInt64 || event.OriginalDeductAmount != -debit.Amount ||
!sameRefundUint(debit.RelatedShopID, event.RelatedShopID) ||
!sameRefundString(debit.TransactionSubtype, event.TransactionSubtype) ||
debit.AssetType != event.AssetType || debit.AssetID != event.AssetID || debit.AssetIdentifier != event.AssetIdentifier {
return errors.New(errors.CodeInternalError, "代理主钱包退款事件与原扣款流水不一致")
}
return nil
}
func sameRefundUint(left, right *uint) bool {
return (left == nil && right == nil) || (left != nil && right != nil && *left == *right)
}
func sameRefundString(left, right *string) bool {
return (left == nil && right == nil) || (left != nil && right != nil && *left == *right)
}

View File

@@ -0,0 +1,37 @@
package wallet
import (
"context"
"strconv"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
// RefundEventWriter 将代理主钱包退款事实写入公共 Outbox。
type RefundEventWriter struct {
outbox *outbox.Repository
}
// NewRefundEventWriter 创建代理主钱包退款 Outbox Writer。
func NewRefundEventWriter(repository *outbox.Repository) *RefundEventWriter {
return &RefundEventWriter{outbox: repository}
}
// Append 在调用方业务事务中追加代理主钱包退款事件。
func (w *RefundEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.RefundedEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "代理主钱包退款 Outbox Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentMainWalletRefunded,
PayloadVersion: constants.AgentMainWalletRefundedPayloadVersionV1,
AggregateType: "agent_wallet", AggregateID: strconv.FormatUint(uint64(event.WalletID), 10),
ResourceType: constants.ReferenceTypeRefund, ResourceID: strconv.FormatUint(uint64(event.RefundID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
return err
}

View File

@@ -0,0 +1,64 @@
package wallet
import (
"context"
"github.com/bytedance/sonic"
"gorm.io/gorm"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// ReservationEventConsumer 校验预占事件与本地权威事实一致。
type ReservationEventConsumer struct {
db *gorm.DB
}
// NewReservationEventConsumer 创建代理主钱包预占事件消费者。
func NewReservationEventConsumer(db *gorm.DB) *ReservationEventConsumer {
return &ReservationEventConsumer{db: db}
}
// Consume 只确认具有匹配权威预占事实的事件。
func (c *ReservationEventConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
if c == nil || c.db == nil {
return errors.New(errors.CodeInternalError, "代理主钱包预占事件消费者未配置")
}
if envelope.EventType != constants.OutboxEventTypeAgentMainWalletReservationChanged ||
envelope.PayloadVersion != constants.AgentMainWalletReservationPayloadVersionV1 {
return errors.New(errors.CodeInvalidParam, "代理主钱包预占事件类型或版本不受支持")
}
var event walletapp.ReservationEvent
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
return errors.Wrap(errors.CodeInvalidParam, err, "代理主钱包预占事件载荷无法解析")
}
if event.EventID != envelope.EventID || event.ReservationID == 0 || event.WalletID == 0 ||
event.Amount <= 0 || event.ReferenceType == "" || event.ReferenceID == 0 {
return errors.New(errors.CodeInvalidParam, "代理主钱包预占事件载荷不完整")
}
var reservation model.AgentWalletReservation
if err := c.db.WithContext(ctx).Where("id = ?", event.ReservationID).First(&reservation).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeInternalError, "代理主钱包预占事件缺少权威事实")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理主钱包预占事实失败")
}
if reservation.AgentWalletID != event.WalletID || reservation.Amount != event.Amount ||
reservation.ReferenceType != event.ReferenceType || reservation.ReferenceID != event.ReferenceID ||
!reservationStatusContainsEvent(reservation.Status, event.Status) {
return errors.New(errors.CodeInternalError, "代理主钱包预占事件与权威事实不一致")
}
return nil
}
func reservationStatusContainsEvent(current, event int) bool {
if current == event {
return true
}
return event == constants.AgentWalletReservationStatusFrozen &&
(current == constants.AgentWalletReservationStatusReleased || current == constants.AgentWalletReservationStatusCompleted)
}

View File

@@ -0,0 +1,37 @@
package wallet
import (
"context"
"strconv"
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"gorm.io/gorm"
)
// ReservationEventWriter 将代理主钱包预占状态写入公共 Outbox。
type ReservationEventWriter struct {
outbox *outbox.Repository
}
// NewReservationEventWriter 创建代理主钱包预占 Outbox Writer。
func NewReservationEventWriter(repository *outbox.Repository) *ReservationEventWriter {
return &ReservationEventWriter{outbox: repository}
}
// Append 在调用方事务中追加预占状态事件。
func (w *ReservationEventWriter) Append(ctx context.Context, tx *gorm.DB, event walletapp.ReservationEvent) error {
if w == nil || w.outbox == nil {
return errors.New(errors.CodeInternalError, "代理主钱包预占 Outbox Writer 未配置")
}
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentMainWalletReservationChanged,
PayloadVersion: constants.AgentMainWalletReservationPayloadVersionV1,
AggregateType: "agent_wallet_reservation", AggregateID: strconv.FormatUint(uint64(event.ReservationID), 10),
ResourceType: event.ReferenceType, ResourceID: strconv.FormatUint(uint64(event.ReferenceID), 10),
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
})
return err
}