v0.1
This commit is contained in:
140
internal/domains/finance/entities/alipay_order.go
Normal file
140
internal/domains/finance/entities/alipay_order.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AlipayOrderStatus 支付宝订单状态枚举
|
||||
type AlipayOrderStatus string
|
||||
|
||||
const (
|
||||
AlipayOrderStatusPending AlipayOrderStatus = "pending" // 待支付
|
||||
AlipayOrderStatusSuccess AlipayOrderStatus = "success" // 支付成功
|
||||
AlipayOrderStatusFailed AlipayOrderStatus = "failed" // 支付失败
|
||||
AlipayOrderStatusCancelled AlipayOrderStatus = "cancelled" // 已取消
|
||||
AlipayOrderStatusClosed AlipayOrderStatus = "closed" // 已关闭
|
||||
)
|
||||
|
||||
const (
|
||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||
)
|
||||
|
||||
// AlipayOrder 支付宝订单详情实体
|
||||
type AlipayOrder struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"支付宝订单唯一标识"`
|
||||
RechargeID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"recharge_id" comment:"关联充值记录ID"`
|
||||
OutTradeNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"out_trade_no" comment:"商户订单号"`
|
||||
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"支付宝交易号"`
|
||||
|
||||
// 订单信息
|
||||
Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"`
|
||||
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"支付平台:app/h5/pc"`
|
||||
Status AlipayOrderStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"订单状态"`
|
||||
|
||||
// 支付宝返回信息
|
||||
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家支付宝用户ID"`
|
||||
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家支付宝用户ID"`
|
||||
PayAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
|
||||
ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"`
|
||||
|
||||
// 回调信息
|
||||
NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"`
|
||||
ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"`
|
||||
|
||||
// 错误信息
|
||||
ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (AlipayOrder) TableName() string {
|
||||
return "alipay_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (a *AlipayOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待支付状态
|
||||
func (a *AlipayOrder) IsPending() bool {
|
||||
return a.Status == AlipayOrderStatusPending
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否为支付成功状态
|
||||
func (a *AlipayOrder) IsSuccess() bool {
|
||||
return a.Status == AlipayOrderStatusSuccess
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (a *AlipayOrder) IsFailed() bool {
|
||||
return a.Status == AlipayOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (a *AlipayOrder) IsCancelled() bool {
|
||||
return a.Status == AlipayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (a *AlipayOrder) IsClosed() bool {
|
||||
return a.Status == AlipayOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为支付成功
|
||||
func (a *AlipayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
a.Status = AlipayOrderStatusSuccess
|
||||
a.TradeNo = &tradeNo
|
||||
a.BuyerID = buyerID
|
||||
a.SellerID = sellerID
|
||||
a.PayAmount = payAmount
|
||||
a.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
a.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (a *AlipayOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
a.Status = AlipayOrderStatusFailed
|
||||
a.ErrorCode = errorCode
|
||||
a.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (a *AlipayOrder) MarkCancelled() {
|
||||
a.Status = AlipayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (a *AlipayOrder) MarkClosed() {
|
||||
a.Status = AlipayOrderStatusClosed
|
||||
}
|
||||
|
||||
// NewAlipayOrder 工厂方法 - 创建支付宝订单
|
||||
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
|
||||
return &AlipayOrder{
|
||||
ID: uuid.New().String(),
|
||||
RechargeID: rechargeID,
|
||||
OutTradeNo: outTradeNo,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
Status: AlipayOrderStatusPending,
|
||||
}
|
||||
}
|
||||
177
internal/domains/finance/entities/recharge_record.go
Normal file
177
internal/domains/finance/entities/recharge_record.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RechargeType 充值类型枚举
|
||||
type RechargeType string
|
||||
|
||||
const (
|
||||
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
|
||||
RechargeTypeTransfer RechargeType = "transfer" // 对公转账
|
||||
RechargeTypeGift RechargeType = "gift" // 赠送
|
||||
)
|
||||
|
||||
// RechargeStatus 充值状态枚举
|
||||
type RechargeStatus string
|
||||
|
||||
const (
|
||||
RechargeStatusPending RechargeStatus = "pending" // 待处理
|
||||
RechargeStatusSuccess RechargeStatus = "success" // 成功
|
||||
RechargeStatusFailed RechargeStatus = "failed" // 失败
|
||||
RechargeStatusCancelled RechargeStatus = "cancelled" // 已取消
|
||||
)
|
||||
|
||||
// RechargeRecord 充值记录实体
|
||||
// 记录用户的各种充值操作,包括支付宝充值、对公转账、赠送等
|
||||
type RechargeRecord struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"充值记录唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"充值用户ID"`
|
||||
|
||||
// 充值信息
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"充值金额"`
|
||||
RechargeType RechargeType `gorm:"type:varchar(20);not null;index" json:"recharge_type" comment:"充值类型"`
|
||||
Status RechargeStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"充值状态"`
|
||||
|
||||
// 订单号字段(根据充值类型使用不同字段)
|
||||
AlipayOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"alipay_order_id,omitempty" comment:"支付宝订单号"`
|
||||
TransferOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"transfer_order_id,omitempty" comment:"转账订单号"`
|
||||
|
||||
// 通用字段
|
||||
Notes string `gorm:"type:varchar(500)" json:"notes,omitempty" comment:"备注信息"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (RechargeRecord) TableName() string {
|
||||
return "recharge_records"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (r *RechargeRecord) BeforeCreate(tx *gorm.DB) error {
|
||||
if r.ID == "" {
|
||||
r.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待处理状态
|
||||
func (r *RechargeRecord) IsPending() bool {
|
||||
return r.Status == RechargeStatusPending
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否为成功状态
|
||||
func (r *RechargeRecord) IsSuccess() bool {
|
||||
return r.Status == RechargeStatusSuccess
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为失败状态
|
||||
func (r *RechargeRecord) IsFailed() bool {
|
||||
return r.Status == RechargeStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (r *RechargeRecord) IsCancelled() bool {
|
||||
return r.Status == RechargeStatusCancelled
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为成功
|
||||
func (r *RechargeRecord) MarkSuccess() {
|
||||
r.Status = RechargeStatusSuccess
|
||||
}
|
||||
|
||||
// MarkFailed 标记为失败
|
||||
func (r *RechargeRecord) MarkFailed() {
|
||||
r.Status = RechargeStatusFailed
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (r *RechargeRecord) MarkCancelled() {
|
||||
r.Status = RechargeStatusCancelled
|
||||
}
|
||||
|
||||
// ValidatePaymentMethod 验证支付方式:支付宝订单号和转账订单号只能有一个存在
|
||||
func (r *RechargeRecord) ValidatePaymentMethod() error {
|
||||
hasAlipay := r.AlipayOrderID != nil && *r.AlipayOrderID != ""
|
||||
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
|
||||
|
||||
if hasAlipay && hasTransfer {
|
||||
return errors.New("支付宝订单号和转账订单号不能同时存在")
|
||||
}
|
||||
|
||||
if !hasAlipay && !hasTransfer {
|
||||
return errors.New("必须提供支付宝订单号或转账订单号")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrderID 获取订单号(根据充值类型返回对应的订单号)
|
||||
func (r *RechargeRecord) GetOrderID() string {
|
||||
switch r.RechargeType {
|
||||
case RechargeTypeAlipay:
|
||||
if r.AlipayOrderID != nil {
|
||||
return *r.AlipayOrderID
|
||||
}
|
||||
case RechargeTypeTransfer:
|
||||
if r.TransferOrderID != nil {
|
||||
return *r.TransferOrderID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetAlipayOrderID 设置支付宝订单号
|
||||
func (r *RechargeRecord) SetAlipayOrderID(orderID string) {
|
||||
r.AlipayOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetTransferOrderID 设置转账订单号
|
||||
func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
||||
r.TransferOrderID = &orderID
|
||||
}
|
||||
|
||||
// NewAlipayRechargeRecord 工厂方法 - 创建支付宝充值记录
|
||||
func NewAlipayRechargeRecord(userID string, amount decimal.Decimal, alipayOrderID string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeAlipay,
|
||||
Status: RechargeStatusPending,
|
||||
AlipayOrderID: &alipayOrderID,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransferRechargeRecord 工厂方法 - 创建对公转账充值记录
|
||||
func NewTransferRechargeRecord(userID string, amount decimal.Decimal, transferOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeTransfer,
|
||||
Status: RechargeStatusPending,
|
||||
TransferOrderID: &transferOrderID,
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
|
||||
// NewGiftRechargeRecord 工厂方法 - 创建赠送充值记录
|
||||
func NewGiftRechargeRecord(userID string, amount decimal.Decimal, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeGift,
|
||||
Status: RechargeStatusSuccess, // 赠送直接标记为成功
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserSecrets 用户密钥实体
|
||||
// 存储用户的API访问密钥信息,用于第三方服务集成和API调用
|
||||
// 支持密钥的生命周期管理,包括激活状态、过期时间、使用统计等
|
||||
type UserSecrets struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"密钥记录唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;uniqueIndex" comment:"关联用户ID"`
|
||||
AccessID string `gorm:"type:varchar(100);not null;uniqueIndex" comment:"访问ID(用于API认证)"`
|
||||
AccessKey string `gorm:"type:varchar(255);not null" comment:"访问密钥(加密存储)"`
|
||||
|
||||
// 密钥状态 - 密钥的生命周期管理
|
||||
IsActive bool `gorm:"default:true" comment:"密钥是否激活"`
|
||||
LastUsedAt *time.Time `comment:"最后使用时间"`
|
||||
ExpiresAt *time.Time `comment:"密钥过期时间"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (UserSecrets) TableName() string {
|
||||
return "user_secrets"
|
||||
}
|
||||
|
||||
// IsExpired 检查密钥是否已过期
|
||||
// 判断密钥是否超过有效期,过期后需要重新生成或续期
|
||||
func (u *UserSecrets) IsExpired() bool {
|
||||
if u.ExpiresAt == nil {
|
||||
return false // 没有过期时间表示永不过期
|
||||
}
|
||||
return time.Now().After(*u.ExpiresAt)
|
||||
}
|
||||
|
||||
// IsValid 检查密钥是否有效
|
||||
// 综合判断密钥是否可用,包括激活状态和过期状态检查
|
||||
func (u *UserSecrets) IsValid() bool {
|
||||
return u.IsActive && !u.IsExpired()
|
||||
}
|
||||
|
||||
// UpdateLastUsedAt 更新最后使用时间
|
||||
// 在密钥被使用时调用,记录最新的使用时间,用于使用统计和监控
|
||||
func (u *UserSecrets) UpdateLastUsedAt() {
|
||||
now := time.Now()
|
||||
u.LastUsedAt = &now
|
||||
}
|
||||
|
||||
// Deactivate 停用密钥
|
||||
// 将密钥设置为非激活状态,禁止使用该密钥进行API调用
|
||||
func (u *UserSecrets) Deactivate() {
|
||||
u.IsActive = false
|
||||
}
|
||||
|
||||
// Activate 激活密钥
|
||||
// 重新启用密钥,允许使用该密钥进行API调用
|
||||
func (u *UserSecrets) Activate() {
|
||||
u.IsActive = true
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (u *UserSecrets) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == "" {
|
||||
u.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,9 +9,12 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Wallet 钱包实体
|
||||
// Wallet 钱包聚合根
|
||||
// 用户数字钱包的核心信息,支持多种钱包类型和精确的余额管理
|
||||
// 使用decimal类型确保金额计算的精确性,避免浮点数精度问题
|
||||
// 支持欠费(余额<0),但只允许扣到小于0一次,之后不能再扣
|
||||
// 新建钱包时可配置默认额度
|
||||
|
||||
type Wallet struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"钱包唯一标识"`
|
||||
@@ -20,10 +23,7 @@ type Wallet struct {
|
||||
// 钱包状态 - 钱包的基本状态信息
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"钱包是否激活"`
|
||||
Balance decimal.Decimal `gorm:"type:decimal(20,8);default:0" json:"balance" comment:"钱包余额(精确到8位小数)"`
|
||||
|
||||
// 钱包信息 - 钱包的详细配置信息
|
||||
WalletAddress string `gorm:"type:varchar(255)" json:"wallet_address,omitempty" comment:"钱包地址"`
|
||||
WalletType string `gorm:"type:varchar(50);default:'MAIN'" json:"wallet_type" comment:"钱包类型(MAIN/DEPOSIT/WITHDRAWAL)"` // MAIN, DEPOSIT, WITHDRAWAL
|
||||
Version int64 `gorm:"version" json:"version" comment:"乐观锁版本号"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
@@ -37,36 +37,53 @@ func (Wallet) TableName() string {
|
||||
}
|
||||
|
||||
// IsZeroBalance 检查余额是否为零
|
||||
// 判断钱包余额是否为零,用于业务逻辑判断
|
||||
func (w *Wallet) IsZeroBalance() bool {
|
||||
return w.Balance.IsZero()
|
||||
}
|
||||
|
||||
// HasSufficientBalance 检查是否有足够余额
|
||||
// 判断钱包余额是否足够支付指定金额,用于交易前的余额验证
|
||||
// HasSufficientBalance 检查是否有足够余额(允许透支额度)
|
||||
func (w *Wallet) HasSufficientBalance(amount decimal.Decimal) bool {
|
||||
return w.Balance.GreaterThanOrEqual(amount)
|
||||
// 允许扣到额度下限
|
||||
return w.Balance.Sub(amount).GreaterThanOrEqual(decimal.Zero)
|
||||
}
|
||||
|
||||
// AddBalance 增加余额
|
||||
// 向钱包增加指定金额,用于充值、收入等场景
|
||||
// IsArrears 是否欠费(余额<0)
|
||||
func (w *Wallet) IsArrears() bool {
|
||||
return w.Balance.LessThan(decimal.Zero)
|
||||
}
|
||||
|
||||
// IsLowBalance 是否余额较低(余额<300)
|
||||
func (w *Wallet) IsLowBalance() bool {
|
||||
return w.Balance.LessThan(decimal.NewFromInt(300))
|
||||
}
|
||||
|
||||
// GetBalanceStatus 获取余额状态
|
||||
func (w *Wallet) GetBalanceStatus() string {
|
||||
if w.IsArrears() {
|
||||
return "arrears" // 欠费
|
||||
} else if w.IsLowBalance() {
|
||||
return "low" // 余额较低
|
||||
} else {
|
||||
return "normal" // 正常
|
||||
}
|
||||
}
|
||||
|
||||
// AddBalance 增加余额(只做加法,业务规则由服务层控制是否允许充值)
|
||||
func (w *Wallet) AddBalance(amount decimal.Decimal) {
|
||||
w.Balance = w.Balance.Add(amount)
|
||||
}
|
||||
|
||||
// SubtractBalance 减少余额
|
||||
// 从钱包扣除指定金额,用于消费、转账等场景
|
||||
// 如果余额不足会返回错误,确保资金安全
|
||||
// SubtractBalance 扣减余额,含欠费业务规则
|
||||
func (w *Wallet) SubtractBalance(amount decimal.Decimal) error {
|
||||
if !w.HasSufficientBalance(amount) {
|
||||
return fmt.Errorf("余额不足")
|
||||
if w.Balance.LessThan(decimal.Zero) {
|
||||
return fmt.Errorf("已欠费,不能再扣款")
|
||||
}
|
||||
w.Balance = w.Balance.Sub(amount)
|
||||
newBalance := w.Balance.Sub(amount)
|
||||
w.Balance = newBalance
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFormattedBalance 获取格式化的余额字符串
|
||||
// 将decimal类型的余额转换为字符串格式,便于显示和传输
|
||||
func (w *Wallet) GetFormattedBalance() string {
|
||||
return w.Balance.String()
|
||||
}
|
||||
@@ -78,3 +95,13 @@ func (w *Wallet) BeforeCreate(tx *gorm.DB) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewWallet 工厂方法
|
||||
func NewWallet(userID string, defaultCreditLimit decimal.Decimal) *Wallet {
|
||||
return &Wallet{
|
||||
UserID: userID,
|
||||
IsActive: true,
|
||||
Balance: defaultCreditLimit,
|
||||
Version: 0,
|
||||
}
|
||||
}
|
||||
|
||||
52
internal/domains/finance/entities/wallet_transaction.go
Normal file
52
internal/domains/finance/entities/wallet_transaction.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// WalletTransaction 钱包扣款记录
|
||||
// 记录API调用产生的扣款操作
|
||||
type WalletTransaction struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"交易记录唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"扣款用户ID"`
|
||||
ApiCallID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"api_call_id" comment:"关联API调用ID"`
|
||||
TransactionID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id" comment:"交易ID"`
|
||||
ProductID string `gorm:"type:varchar(64);not null;index" json:"product_id" comment:"产品ID"`
|
||||
|
||||
// 扣款信息
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"扣款金额"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (WalletTransaction) TableName() string {
|
||||
return "wallet_transactions"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (t *WalletTransaction) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewWalletTransaction 工厂方法 - 创建扣款记录
|
||||
func NewWalletTransaction(userID, apiCallID, transactionID, productID string, amount decimal.Decimal) *WalletTransaction {
|
||||
return &WalletTransaction{
|
||||
UserID: userID,
|
||||
ApiCallID: apiCallID,
|
||||
TransactionID: transactionID,
|
||||
ProductID: productID,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
)
|
||||
|
||||
// AlipayOrderRepository 支付宝订单仓储接口
|
||||
type AlipayOrderRepository interface {
|
||||
Create(ctx context.Context, order entities.AlipayOrder) (entities.AlipayOrder, error)
|
||||
GetByID(ctx context.Context, id string) (entities.AlipayOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*entities.AlipayOrder, error)
|
||||
GetByRechargeID(ctx context.Context, rechargeID string) (*entities.AlipayOrder, error)
|
||||
GetByUserID(ctx context.Context, userID string) ([]entities.AlipayOrder, error)
|
||||
Update(ctx context.Context, order entities.AlipayOrder) error
|
||||
UpdateStatus(ctx context.Context, id string, status entities.AlipayOrderStatus) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// FinanceStats 财务统计信息
|
||||
type FinanceStats struct {
|
||||
TotalWallets int64
|
||||
ActiveWallets int64
|
||||
TotalBalance string
|
||||
TodayTransactions int64
|
||||
}
|
||||
|
||||
// WalletRepository 钱包仓储接口
|
||||
type WalletRepository interface {
|
||||
interfaces.Repository[entities.Wallet]
|
||||
|
||||
// 基础查询 - 直接使用实体
|
||||
GetByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
|
||||
GetByWalletAddress(ctx context.Context, walletAddress string) (*entities.Wallet, error)
|
||||
GetByWalletType(ctx context.Context, userID string, walletType string) (*entities.Wallet, error)
|
||||
|
||||
// 复杂查询 - 使用查询参数
|
||||
ListWallets(ctx context.Context, query *queries.ListWalletsQuery) ([]*entities.Wallet, int64, error)
|
||||
|
||||
// 业务操作
|
||||
UpdateBalance(ctx context.Context, walletID string, balance string) error
|
||||
AddBalance(ctx context.Context, walletID string, amount string) error
|
||||
SubtractBalance(ctx context.Context, walletID string, amount string) error
|
||||
ActivateWallet(ctx context.Context, walletID string) error
|
||||
DeactivateWallet(ctx context.Context, walletID string) error
|
||||
|
||||
// 统计信息
|
||||
GetStats(ctx context.Context) (*FinanceStats, error)
|
||||
GetUserWalletStats(ctx context.Context, userID string) (*FinanceStats, error)
|
||||
}
|
||||
|
||||
// UserSecretsRepository 用户密钥仓储接口
|
||||
type UserSecretsRepository interface {
|
||||
interfaces.Repository[entities.UserSecrets]
|
||||
|
||||
// 基础查询 - 直接使用实体
|
||||
GetByUserID(ctx context.Context, userID string) (*entities.UserSecrets, error)
|
||||
GetBySecretType(ctx context.Context, userID string, secretType string) (*entities.UserSecrets, error)
|
||||
|
||||
// 复杂查询 - 使用查询参数
|
||||
ListUserSecrets(ctx context.Context, query *queries.ListUserSecretsQuery) ([]*entities.UserSecrets, int64, error)
|
||||
|
||||
// 业务操作
|
||||
UpdateSecret(ctx context.Context, userID string, secretType string, secretValue string) error
|
||||
DeleteSecret(ctx context.Context, userID string, secretType string) error
|
||||
ValidateSecret(ctx context.Context, userID string, secretType string, secretValue string) (bool, error)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// RechargeRecordRepository 充值记录仓储接口
|
||||
type RechargeRecordRepository interface {
|
||||
Create(ctx context.Context, record entities.RechargeRecord) (entities.RechargeRecord, error)
|
||||
GetByID(ctx context.Context, id string) (entities.RechargeRecord, error)
|
||||
GetByUserID(ctx context.Context, userID string) ([]entities.RechargeRecord, error)
|
||||
GetByAlipayOrderID(ctx context.Context, alipayOrderID string) (*entities.RechargeRecord, error)
|
||||
GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error)
|
||||
Update(ctx context.Context, record entities.RechargeRecord) error
|
||||
UpdateStatus(ctx context.Context, id string, status entities.RechargeStatus) error
|
||||
|
||||
// 管理员查询方法
|
||||
List(ctx context.Context, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
|
||||
Count(ctx context.Context, options interfaces.CountOptions) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// FinanceStats 财务统计信息
|
||||
type FinanceStats struct {
|
||||
TotalWallets int64
|
||||
ActiveWallets int64
|
||||
TotalBalance string
|
||||
TodayTransactions int64
|
||||
}
|
||||
|
||||
// WalletRepository 钱包仓储接口
|
||||
// 只保留核心方法,聚合服务负责业务规则
|
||||
// 业务操作只保留乐观锁更新和基础更新
|
||||
|
||||
type WalletRepository interface {
|
||||
interfaces.Repository[entities.Wallet]
|
||||
|
||||
// 基础查询
|
||||
GetByUserID(ctx context.Context, userID string) (*entities.Wallet, error)
|
||||
|
||||
// 乐观锁更新(自动重试)
|
||||
UpdateBalanceWithVersion(ctx context.Context, walletID string, newBalance string, oldVersion int64) (bool, error)
|
||||
|
||||
// 状态操作
|
||||
ActivateWallet(ctx context.Context, walletID string) error
|
||||
DeactivateWallet(ctx context.Context, walletID string) error
|
||||
|
||||
// 统计
|
||||
GetStats(ctx context.Context) (*FinanceStats, error)
|
||||
GetUserWalletStats(ctx context.Context, userID string) (*FinanceStats, error)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// WalletTransactionRepository 钱包扣款记录仓储接口
|
||||
type WalletTransactionRepository interface {
|
||||
interfaces.Repository[entities.WalletTransaction]
|
||||
|
||||
// 基础查询
|
||||
GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*entities.WalletTransaction, error)
|
||||
GetByApiCallID(ctx context.Context, apiCallID string) (*entities.WalletTransaction, error)
|
||||
|
||||
// 新增:分页查询用户钱包交易记录
|
||||
ListByUserId(ctx context.Context, userId string, options interfaces.ListOptions) ([]*entities.WalletTransaction, int64, error)
|
||||
|
||||
// 新增:根据条件筛选钱包交易记录
|
||||
ListByUserIdWithFilters(ctx context.Context, userId string, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.WalletTransaction, int64, error)
|
||||
|
||||
// 新增:根据条件筛选钱包交易记录(包含产品名称)
|
||||
ListByUserIdWithFiltersAndProductName(ctx context.Context, userId string, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error)
|
||||
|
||||
// 新增:统计用户钱包交易次数
|
||||
CountByUserId(ctx context.Context, userId string) (int64, error)
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
)
|
||||
|
||||
// FinanceService 财务领域服务
|
||||
// 负责财务相关的业务逻辑,包括钱包管理、余额操作等
|
||||
type FinanceService struct {
|
||||
walletRepo repositories.WalletRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewFinanceService 创建财务领域服务
|
||||
func NewFinanceService(
|
||||
walletRepo repositories.WalletRepository,
|
||||
logger *zap.Logger,
|
||||
) *FinanceService {
|
||||
return &FinanceService{
|
||||
walletRepo: walletRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWallet 创建钱包
|
||||
func (s *FinanceService) CreateWallet(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
// 检查用户是否已有钱包
|
||||
existingWallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err == nil && existingWallet != nil {
|
||||
return nil, fmt.Errorf("用户已有钱包")
|
||||
}
|
||||
|
||||
// 创建钱包
|
||||
wallet := &entities.Wallet{
|
||||
UserID: userID,
|
||||
Balance: decimal.Zero,
|
||||
IsActive: true,
|
||||
WalletType: "MAIN",
|
||||
}
|
||||
|
||||
createdWallet, err := s.walletRepo.Create(ctx, *wallet)
|
||||
if err != nil {
|
||||
s.logger.Error("创建钱包失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建钱包失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("钱包创建成功",
|
||||
zap.String("wallet_id", createdWallet.ID),
|
||||
zap.String("user_id", userID),
|
||||
)
|
||||
|
||||
return &createdWallet, nil
|
||||
}
|
||||
|
||||
// GetWallet 获取钱包信息
|
||||
func (s *FinanceService) GetWallet(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("钱包不存在: %w", err)
|
||||
}
|
||||
return wallet, nil
|
||||
}
|
||||
|
||||
// GetWalletByID 根据ID获取钱包
|
||||
func (s *FinanceService) GetWalletByID(ctx context.Context, walletID string) (*entities.Wallet, error) {
|
||||
wallet, err := s.walletRepo.GetByID(ctx, walletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("钱包不存在: %w", err)
|
||||
}
|
||||
return &wallet, nil
|
||||
}
|
||||
|
||||
// RechargeWallet 充值钱包
|
||||
func (s *FinanceService) RechargeWallet(ctx context.Context, userID string, amount float64) error {
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("充值金额必须大于0")
|
||||
}
|
||||
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在: %w", err)
|
||||
}
|
||||
|
||||
// 更新余额
|
||||
amountDecimal := decimal.NewFromFloat(amount)
|
||||
wallet.AddBalance(amountDecimal)
|
||||
|
||||
if err := s.walletRepo.Update(ctx, *wallet); err != nil {
|
||||
s.logger.Error("充值失败", zap.Error(err))
|
||||
return fmt.Errorf("充值失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("wallet_id", wallet.ID),
|
||||
zap.String("user_id", userID),
|
||||
zap.Float64("amount", amount),
|
||||
zap.String("new_balance", wallet.GetFormattedBalance()),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeductWallet 扣减钱包余额
|
||||
func (s *FinanceService) DeductWallet(ctx context.Context, userID string, amount float64) error {
|
||||
if amount <= 0 {
|
||||
return fmt.Errorf("扣减金额必须大于0")
|
||||
}
|
||||
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在: %w", err)
|
||||
}
|
||||
|
||||
amountDecimal := decimal.NewFromFloat(amount)
|
||||
if err := wallet.SubtractBalance(amountDecimal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.walletRepo.Update(ctx, *wallet); err != nil {
|
||||
s.logger.Error("扣减失败", zap.Error(err))
|
||||
return fmt.Errorf("扣减失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("钱包扣减成功",
|
||||
zap.String("wallet_id", wallet.ID),
|
||||
zap.String("user_id", userID),
|
||||
zap.Float64("amount", amount),
|
||||
zap.String("new_balance", wallet.GetFormattedBalance()),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWalletBalance 获取钱包余额
|
||||
func (s *FinanceService) GetWalletBalance(ctx context.Context, userID string) (float64, error) {
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("钱包不存在: %w", err)
|
||||
}
|
||||
balance, _ := wallet.Balance.Float64()
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
// CheckWalletBalance 检查钱包余额是否足够
|
||||
func (s *FinanceService) CheckWalletBalance(ctx context.Context, userID string, amount float64) (bool, error) {
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("钱包不存在: %w", err)
|
||||
}
|
||||
|
||||
amountDecimal := decimal.NewFromFloat(amount)
|
||||
return wallet.HasSufficientBalance(amountDecimal), nil
|
||||
}
|
||||
349
internal/domains/finance/services/recharge_record_service.go
Normal file
349
internal/domains/finance/services/recharge_record_service.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// RechargeRecordService 充值记录服务接口
|
||||
type RechargeRecordService interface {
|
||||
// 对公转账充值
|
||||
TransferRecharge(ctx context.Context, userID string, amount decimal.Decimal, transferOrderID, notes string) (*entities.RechargeRecord, error)
|
||||
|
||||
// 赠送充值
|
||||
GiftRecharge(ctx context.Context, userID string, amount decimal.Decimal, operatorID, notes string) (*entities.RechargeRecord, error)
|
||||
|
||||
// 支付宝充值
|
||||
CreateAlipayRecharge(ctx context.Context, userID string, amount decimal.Decimal, alipayOrderID string) (*entities.RechargeRecord, error)
|
||||
GetRechargeRecordByAlipayOrderID(ctx context.Context, alipayOrderID string) (*entities.RechargeRecord, error)
|
||||
|
||||
// 支付宝订单管理
|
||||
CreateAlipayOrder(ctx context.Context, rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) error
|
||||
HandleAlipayPaymentSuccess(ctx context.Context, outTradeNo string, amount decimal.Decimal, tradeNo string) error
|
||||
|
||||
// 通用查询
|
||||
GetByID(ctx context.Context, id string) (*entities.RechargeRecord, error)
|
||||
GetByUserID(ctx context.Context, userID string) ([]entities.RechargeRecord, error)
|
||||
GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error)
|
||||
|
||||
// 管理员查询
|
||||
GetAll(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]entities.RechargeRecord, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
}
|
||||
|
||||
// RechargeRecordServiceImpl 充值记录服务实现
|
||||
type RechargeRecordServiceImpl struct {
|
||||
rechargeRecordRepo repositories.RechargeRecordRepository
|
||||
alipayOrderRepo repositories.AlipayOrderRepository
|
||||
walletRepo repositories.WalletRepository
|
||||
walletService WalletAggregateService
|
||||
txManager *database.TransactionManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewRechargeRecordService(
|
||||
rechargeRecordRepo repositories.RechargeRecordRepository,
|
||||
alipayOrderRepo repositories.AlipayOrderRepository,
|
||||
walletRepo repositories.WalletRepository,
|
||||
walletService WalletAggregateService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
) RechargeRecordService {
|
||||
return &RechargeRecordServiceImpl{
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
walletRepo: walletRepo,
|
||||
walletService: walletService,
|
||||
txManager: txManager,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// TransferRecharge 对公转账充值
|
||||
func (s *RechargeRecordServiceImpl) TransferRecharge(ctx context.Context, userID string, amount decimal.Decimal, transferOrderID, notes string) (*entities.RechargeRecord, error) {
|
||||
// 检查钱包是否存在
|
||||
_, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
// 检查转账订单号是否已存在
|
||||
existingRecord, _ := s.rechargeRecordRepo.GetByTransferOrderID(ctx, transferOrderID)
|
||||
if existingRecord != nil {
|
||||
return nil, fmt.Errorf("转账订单号已存在")
|
||||
}
|
||||
|
||||
var createdRecord entities.RechargeRecord
|
||||
|
||||
// 在事务中执行所有更新操作
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 创建充值记录
|
||||
rechargeRecord := entities.NewTransferRechargeRecord(userID, amount, transferOrderID, notes)
|
||||
record, err := s.rechargeRecordRepo.Create(txCtx, *rechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建转账充值记录失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
createdRecord = record
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额
|
||||
err = s.walletService.Recharge(txCtx, userID, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 标记充值记录为成功
|
||||
createdRecord.MarkSuccess()
|
||||
err = s.rechargeRecordRepo.Update(txCtx, createdRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("对公转账充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("transfer_order_id", transferOrderID))
|
||||
|
||||
return &createdRecord, nil
|
||||
}
|
||||
|
||||
// GiftRecharge 赠送充值
|
||||
func (s *RechargeRecordServiceImpl) GiftRecharge(ctx context.Context, userID string, amount decimal.Decimal, operatorID, notes string) (*entities.RechargeRecord, error) {
|
||||
// 检查钱包是否存在
|
||||
_, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
var createdRecord entities.RechargeRecord
|
||||
|
||||
// 在事务中执行所有更新操作
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 创建赠送充值记录
|
||||
rechargeRecord := entities.NewGiftRechargeRecord(userID, amount, notes)
|
||||
record, err := s.rechargeRecordRepo.Create(txCtx, *rechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建赠送充值记录失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
createdRecord = record
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额
|
||||
err = s.walletService.Recharge(txCtx, userID, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("赠送充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("notes", notes))
|
||||
|
||||
return &createdRecord, nil
|
||||
}
|
||||
|
||||
// CreateAlipayRecharge 创建支付宝充值记录
|
||||
func (s *RechargeRecordServiceImpl) CreateAlipayRecharge(ctx context.Context, userID string, amount decimal.Decimal, alipayOrderID string) (*entities.RechargeRecord, error) {
|
||||
// 检查钱包是否存在
|
||||
_, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
// 检查支付宝订单号是否已存在
|
||||
existingRecord, _ := s.rechargeRecordRepo.GetByAlipayOrderID(ctx, alipayOrderID)
|
||||
if existingRecord != nil {
|
||||
return nil, fmt.Errorf("支付宝订单号已存在")
|
||||
}
|
||||
|
||||
// 创建充值记录
|
||||
rechargeRecord := entities.NewAlipayRechargeRecord(userID, amount, alipayOrderID)
|
||||
createdRecord, err := s.rechargeRecordRepo.Create(ctx, *rechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建支付宝充值记录失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("支付宝充值记录创建成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("alipay_order_id", alipayOrderID),
|
||||
zap.String("recharge_id", createdRecord.ID))
|
||||
|
||||
return &createdRecord, nil
|
||||
}
|
||||
|
||||
// CreateAlipayOrder 创建支付宝订单
|
||||
func (s *RechargeRecordServiceImpl) CreateAlipayOrder(ctx context.Context, rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) error {
|
||||
// 检查充值记录是否存在
|
||||
_, err := s.rechargeRecordRepo.GetByID(ctx, rechargeID)
|
||||
if err != nil {
|
||||
s.logger.Error("充值记录不存在", zap.String("recharge_id", rechargeID), zap.Error(err))
|
||||
return fmt.Errorf("充值记录不存在")
|
||||
}
|
||||
|
||||
// 检查支付宝订单号是否已存在
|
||||
existingOrder, _ := s.alipayOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if existingOrder != nil {
|
||||
s.logger.Info("支付宝订单已存在,跳过重复创建", zap.String("out_trade_no", outTradeNo))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建支付宝订单
|
||||
alipayOrder := entities.NewAlipayOrder(rechargeID, outTradeNo, subject, amount, platform)
|
||||
_, err = s.alipayOrderRepo.Create(ctx, *alipayOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("创建支付宝订单失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("支付宝订单创建成功",
|
||||
zap.String("recharge_id", rechargeID),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("subject", subject),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("platform", platform))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRechargeRecordByAlipayOrderID 根据支付宝订单号获取充值记录
|
||||
func (s *RechargeRecordServiceImpl) GetRechargeRecordByAlipayOrderID(ctx context.Context, alipayOrderID string) (*entities.RechargeRecord, error) {
|
||||
return s.rechargeRecordRepo.GetByAlipayOrderID(ctx, alipayOrderID)
|
||||
}
|
||||
|
||||
// HandleAlipayPaymentSuccess 处理支付宝支付成功回调
|
||||
func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Context, outTradeNo string, amount decimal.Decimal, tradeNo string) error {
|
||||
// 查找支付宝订单
|
||||
alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("查找支付宝订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return fmt.Errorf("查找支付宝订单失败: %w", err)
|
||||
}
|
||||
|
||||
if alipayOrder == nil {
|
||||
s.logger.Error("支付宝订单不存在", zap.String("out_trade_no", outTradeNo))
|
||||
return fmt.Errorf("支付宝订单不存在")
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if alipayOrder.Status == entities.AlipayOrderStatusSuccess {
|
||||
s.logger.Info("支付宝订单已处理成功,跳过重复处理",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("order_id", alipayOrder.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查找对应的充值记录
|
||||
rechargeRecord, err := s.rechargeRecordRepo.GetByID(ctx, alipayOrder.RechargeID)
|
||||
if err != nil {
|
||||
s.logger.Error("查找充值记录失败", zap.String("recharge_id", alipayOrder.RechargeID), zap.Error(err))
|
||||
return fmt.Errorf("查找充值记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查充值记录状态
|
||||
if rechargeRecord.Status == entities.RechargeStatusSuccess {
|
||||
s.logger.Info("充值记录已处理成功,跳过重复处理",
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 在事务中执行所有更新操作
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 更新支付宝订单状态为成功
|
||||
alipayOrder.MarkSuccess(tradeNo, "", "", amount, amount)
|
||||
err := s.alipayOrderRepo.Update(txCtx, *alipayOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新支付宝订单状态失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新充值记录状态为成功
|
||||
rechargeRecord.MarkSuccess()
|
||||
err = s.rechargeRecordRepo.Update(txCtx, rechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, amount)
|
||||
if err != nil {
|
||||
s.logger.Error("更新钱包余额失败", zap.String("user_id", rechargeRecord.UserID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("支付宝支付成功回调处理成功",
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", tradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("order_id", alipayOrder.ID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取充值记录
|
||||
func (s *RechargeRecordServiceImpl) GetByID(ctx context.Context, id string) (*entities.RechargeRecord, error) {
|
||||
record, err := s.rechargeRecordRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
// GetByUserID 根据用户ID获取充值记录列表
|
||||
func (s *RechargeRecordServiceImpl) GetByUserID(ctx context.Context, userID string) ([]entities.RechargeRecord, error) {
|
||||
return s.rechargeRecordRepo.GetByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
// GetByTransferOrderID 根据转账订单号获取充值记录
|
||||
func (s *RechargeRecordServiceImpl) GetByTransferOrderID(ctx context.Context, transferOrderID string) (*entities.RechargeRecord, error) {
|
||||
return s.rechargeRecordRepo.GetByTransferOrderID(ctx, transferOrderID)
|
||||
}
|
||||
|
||||
// GetAll 获取所有充值记录(管理员功能)
|
||||
func (s *RechargeRecordServiceImpl) GetAll(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]entities.RechargeRecord, error) {
|
||||
return s.rechargeRecordRepo.List(ctx, options)
|
||||
}
|
||||
|
||||
// Count 统计充值记录数量(管理员功能)
|
||||
func (s *RechargeRecordServiceImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
||||
countOptions := interfaces.CountOptions{
|
||||
Filters: filters,
|
||||
}
|
||||
return s.rechargeRecordRepo.Count(ctx, countOptions)
|
||||
}
|
||||
142
internal/domains/finance/services/wallet_aggregate_service.go
Normal file
142
internal/domains/finance/services/wallet_aggregate_service.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
)
|
||||
|
||||
// WalletAggregateService 钱包聚合服务接口
|
||||
type WalletAggregateService interface {
|
||||
CreateWallet(ctx context.Context, userID string) (*entities.Wallet, error)
|
||||
Recharge(ctx context.Context, userID string, amount decimal.Decimal) error
|
||||
Deduct(ctx context.Context, userID string, amount decimal.Decimal, apiCallID, transactionID, productID string) error
|
||||
GetBalance(ctx context.Context, userID string) (decimal.Decimal, error)
|
||||
LoadWalletByUserId(ctx context.Context, userID string) (*entities.Wallet, error)
|
||||
}
|
||||
|
||||
// WalletAggregateServiceImpl 实现
|
||||
|
||||
// WalletAggregateServiceImpl 钱包聚合服务实现
|
||||
type WalletAggregateServiceImpl struct {
|
||||
walletRepo repositories.WalletRepository
|
||||
transactionRepo repositories.WalletTransactionRepository
|
||||
logger *zap.Logger
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewWalletAggregateService(
|
||||
walletRepo repositories.WalletRepository,
|
||||
transactionRepo repositories.WalletTransactionRepository,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) WalletAggregateService {
|
||||
return &WalletAggregateServiceImpl{
|
||||
walletRepo: walletRepo,
|
||||
transactionRepo: transactionRepo,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWallet 创建钱包
|
||||
func (s *WalletAggregateServiceImpl) CreateWallet(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
// 检查是否已存在
|
||||
w, _ := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if w != nil {
|
||||
return nil, fmt.Errorf("用户已存在钱包")
|
||||
}
|
||||
wallet := entities.NewWallet(userID, decimal.NewFromFloat(s.cfg.Wallet.DefaultCreditLimit))
|
||||
created, err := s.walletRepo.Create(ctx, *wallet)
|
||||
if err != nil {
|
||||
s.logger.Error("创建钱包失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
s.logger.Info("钱包创建成功", zap.String("user_id", userID), zap.String("wallet_id", created.ID))
|
||||
return &created, nil
|
||||
}
|
||||
|
||||
// Recharge 充值
|
||||
func (s *WalletAggregateServiceImpl) Recharge(ctx context.Context, userID string, amount decimal.Decimal) error {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
w.AddBalance(amount)
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
}
|
||||
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("wallet_id", w.ID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("balance_after", w.Balance.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
// Deduct 扣款,含欠费规则
|
||||
func (s *WalletAggregateServiceImpl) Deduct(ctx context.Context, userID string, amount decimal.Decimal, apiCallID, transactionID, productID string) error {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
|
||||
// 扣减余额
|
||||
if err := w.SubtractBalance(amount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
|
||||
// 创建扣款记录
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
_, err = s.transactionRepo.Create(ctx, *transaction)
|
||||
if err != nil {
|
||||
s.logger.Error("创建扣款记录失败", zap.Error(err))
|
||||
// 不返回错误,因为钱包余额已经更新成功
|
||||
}
|
||||
|
||||
s.logger.Info("钱包扣款成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("wallet_id", w.ID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("balance_after", w.Balance.String()),
|
||||
zap.String("api_call_id", apiCallID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
// GetBalance 查询余额
|
||||
func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("钱包不存在")
|
||||
}
|
||||
return w.Balance, nil
|
||||
}
|
||||
|
||||
func (s *WalletAggregateServiceImpl) LoadWalletByUserId(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
return s.walletRepo.GetByUserID(ctx, userID)
|
||||
}
|
||||
Reference in New Issue
Block a user