f
This commit is contained in:
28
internal/domains/finance/entities/alipay_order.go
Normal file
28
internal/domains/finance/entities/alipay_order.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package entities
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
// AlipayOrderStatus 支付宝订单状态枚举(别名)
|
||||
type AlipayOrderStatus = PayOrderStatus
|
||||
|
||||
const (
|
||||
AlipayOrderStatusPending AlipayOrderStatus = PayOrderStatusPending // 待支付
|
||||
AlipayOrderStatusSuccess AlipayOrderStatus = PayOrderStatusSuccess // 支付成功
|
||||
AlipayOrderStatusFailed AlipayOrderStatus = PayOrderStatusFailed // 支付失败
|
||||
AlipayOrderStatusCancelled AlipayOrderStatus = PayOrderStatusCancelled // 已取消
|
||||
AlipayOrderStatusClosed AlipayOrderStatus = PayOrderStatusClosed // 已关闭
|
||||
)
|
||||
|
||||
const (
|
||||
AlipayOrderPlatformApp = "app" // 支付宝APP支付
|
||||
AlipayOrderPlatformH5 = "h5" // 支付宝H5支付
|
||||
AlipayOrderPlatformPC = "pc" // 支付宝PC支付
|
||||
)
|
||||
|
||||
// AlipayOrder 支付宝订单实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type AlipayOrder = PayOrder
|
||||
|
||||
// NewAlipayOrder 工厂方法 - 创建支付宝订单
|
||||
func NewAlipayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *AlipayOrder {
|
||||
return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "alipay")
|
||||
}
|
||||
163
internal/domains/finance/entities/invoice_application.go
Normal file
163
internal/domains/finance/entities/invoice_application.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
)
|
||||
|
||||
// ApplicationStatus 申请状态枚举
|
||||
type ApplicationStatus string
|
||||
|
||||
const (
|
||||
ApplicationStatusPending ApplicationStatus = "pending" // 待处理
|
||||
ApplicationStatusCompleted ApplicationStatus = "completed" // 已完成(已上传发票)
|
||||
ApplicationStatusRejected ApplicationStatus = "rejected" // 已拒绝
|
||||
)
|
||||
|
||||
// InvoiceApplication 发票申请聚合根
|
||||
type InvoiceApplication 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"`
|
||||
|
||||
// 申请信息
|
||||
InvoiceType value_objects.InvoiceType `gorm:"type:varchar(20);not null" json:"invoice_type" comment:"发票类型"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"申请金额"`
|
||||
Status ApplicationStatus `gorm:"type:varchar(20);not null;default:'pending';index" json:"status" comment:"申请状态"`
|
||||
|
||||
// 开票信息快照(申请时的信息,用于历史记录追踪)
|
||||
CompanyName string `gorm:"type:varchar(200);not null" json:"company_name" comment:"公司名称"`
|
||||
TaxpayerID string `gorm:"type:varchar(50);not null" json:"taxpayer_id" comment:"纳税人识别号"`
|
||||
BankName string `gorm:"type:varchar(100)" json:"bank_name" comment:"开户银行"`
|
||||
BankAccount string `gorm:"type:varchar(50)" json:"bank_account" comment:"银行账号"`
|
||||
CompanyAddress string `gorm:"type:varchar(500)" json:"company_address" comment:"企业地址"`
|
||||
CompanyPhone string `gorm:"type:varchar(20)" json:"company_phone" comment:"企业电话"`
|
||||
ReceivingEmail string `gorm:"type:varchar(100);not null" json:"receiving_email" comment:"发票接收邮箱"`
|
||||
|
||||
// 开票信息引用(关联到用户开票信息表,用于模板功能)
|
||||
UserInvoiceInfoID string `gorm:"type:varchar(36);not null" json:"user_invoice_info_id" comment:"用户开票信息ID"`
|
||||
|
||||
// 文件信息(申请通过后才有)
|
||||
FileID *string `gorm:"type:varchar(200)" json:"file_id,omitempty" comment:"文件ID"`
|
||||
FileName *string `gorm:"type:varchar(200)" json:"file_name,omitempty" comment:"文件名"`
|
||||
FileSize *int64 `json:"file_size,omitempty" comment:"文件大小"`
|
||||
FileURL *string `gorm:"type:varchar(500)" json:"file_url,omitempty" comment:"文件URL"`
|
||||
|
||||
// 处理信息
|
||||
ProcessedBy *string `gorm:"type:varchar(36)" json:"processed_by,omitempty" comment:"处理人ID"`
|
||||
ProcessedAt *time.Time `json:"processed_at,omitempty" comment:"处理时间"`
|
||||
RejectReason *string `gorm:"type:varchar(500)" json:"reject_reason,omitempty" comment:"拒绝原因"`
|
||||
AdminNotes *string `gorm:"type:varchar(500)" json:"admin_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 (InvoiceApplication) TableName() string {
|
||||
return "invoice_applications"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (ia *InvoiceApplication) BeforeCreate(tx *gorm.DB) error {
|
||||
if ia.ID == "" {
|
||||
ia.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待处理状态
|
||||
func (ia *InvoiceApplication) IsPending() bool {
|
||||
return ia.Status == ApplicationStatusPending
|
||||
}
|
||||
|
||||
|
||||
|
||||
// IsCompleted 检查是否为已完成状态
|
||||
func (ia *InvoiceApplication) IsCompleted() bool {
|
||||
return ia.Status == ApplicationStatusCompleted
|
||||
}
|
||||
|
||||
// IsRejected 检查是否为已拒绝状态
|
||||
func (ia *InvoiceApplication) IsRejected() bool {
|
||||
return ia.Status == ApplicationStatusRejected
|
||||
}
|
||||
|
||||
// CanProcess 检查是否可以处理
|
||||
func (ia *InvoiceApplication) CanProcess() bool {
|
||||
return ia.IsPending()
|
||||
}
|
||||
|
||||
// CanReject 检查是否可以拒绝
|
||||
func (ia *InvoiceApplication) CanReject() bool {
|
||||
return ia.IsPending()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MarkCompleted 标记为已完成
|
||||
func (ia *InvoiceApplication) MarkCompleted(processedBy string) {
|
||||
ia.Status = ApplicationStatusCompleted
|
||||
ia.ProcessedBy = &processedBy
|
||||
now := time.Now()
|
||||
ia.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// MarkRejected 标记为已拒绝
|
||||
func (ia *InvoiceApplication) MarkRejected(reason string, processedBy string) {
|
||||
ia.Status = ApplicationStatusRejected
|
||||
ia.RejectReason = &reason
|
||||
ia.ProcessedBy = &processedBy
|
||||
now := time.Now()
|
||||
ia.ProcessedAt = &now
|
||||
}
|
||||
|
||||
// SetFileInfo 设置文件信息
|
||||
func (ia *InvoiceApplication) SetFileInfo(fileID, fileName, fileURL string, fileSize int64) {
|
||||
ia.FileID = &fileID
|
||||
ia.FileName = &fileName
|
||||
ia.FileURL = &fileURL
|
||||
ia.FileSize = &fileSize
|
||||
}
|
||||
|
||||
// NewInvoiceApplication 工厂方法
|
||||
func NewInvoiceApplication(userID string, invoiceType value_objects.InvoiceType, amount decimal.Decimal, userInvoiceInfoID string) *InvoiceApplication {
|
||||
return &InvoiceApplication{
|
||||
UserID: userID,
|
||||
InvoiceType: invoiceType,
|
||||
Amount: amount,
|
||||
Status: ApplicationStatusPending,
|
||||
UserInvoiceInfoID: userInvoiceInfoID,
|
||||
}
|
||||
}
|
||||
|
||||
// SetInvoiceInfoSnapshot 设置开票信息快照
|
||||
func (ia *InvoiceApplication) SetInvoiceInfoSnapshot(info *value_objects.InvoiceInfo) {
|
||||
ia.CompanyName = info.CompanyName
|
||||
ia.TaxpayerID = info.TaxpayerID
|
||||
ia.BankName = info.BankName
|
||||
ia.BankAccount = info.BankAccount
|
||||
ia.CompanyAddress = info.CompanyAddress
|
||||
ia.CompanyPhone = info.CompanyPhone
|
||||
ia.ReceivingEmail = info.ReceivingEmail
|
||||
}
|
||||
|
||||
// GetInvoiceInfoSnapshot 获取开票信息快照
|
||||
func (ia *InvoiceApplication) GetInvoiceInfoSnapshot() *value_objects.InvoiceInfo {
|
||||
return value_objects.NewInvoiceInfo(
|
||||
ia.CompanyName,
|
||||
ia.TaxpayerID,
|
||||
ia.BankName,
|
||||
ia.BankAccount,
|
||||
ia.CompanyAddress,
|
||||
ia.CompanyPhone,
|
||||
ia.ReceivingEmail,
|
||||
)
|
||||
}
|
||||
136
internal/domains/finance/entities/pay_order.go
Normal file
136
internal/domains/finance/entities/pay_order.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PayOrderStatus 支付订单状态枚举(通用)
|
||||
type PayOrderStatus string
|
||||
|
||||
const (
|
||||
PayOrderStatusPending PayOrderStatus = "pending" // 待支付
|
||||
PayOrderStatusSuccess PayOrderStatus = "success" // 支付成功
|
||||
PayOrderStatusFailed PayOrderStatus = "failed" // 支付失败
|
||||
PayOrderStatusCancelled PayOrderStatus = "cancelled" // 已取消
|
||||
PayOrderStatusClosed PayOrderStatus = "closed" // 已关闭
|
||||
)
|
||||
|
||||
// PayOrder 支付订单详情实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type PayOrder 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/wx_h5/wx_mini等"`
|
||||
PayChannel string `gorm:"type:varchar(20);not null;default:'alipay';index" json:"pay_channel" comment:"支付渠道:alipay/wechat"`
|
||||
Status PayOrderStatus `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 (PayOrder) TableName() string {
|
||||
return "typay_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (p *PayOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPending 检查是否为待支付状态
|
||||
func (p *PayOrder) IsPending() bool {
|
||||
return p.Status == PayOrderStatusPending
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否为支付成功状态
|
||||
func (p *PayOrder) IsSuccess() bool {
|
||||
return p.Status == PayOrderStatusSuccess
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (p *PayOrder) IsFailed() bool {
|
||||
return p.Status == PayOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (p *PayOrder) IsCancelled() bool {
|
||||
return p.Status == PayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (p *PayOrder) IsClosed() bool {
|
||||
return p.Status == PayOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkSuccess 标记为支付成功
|
||||
func (p *PayOrder) MarkSuccess(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
p.Status = PayOrderStatusSuccess
|
||||
p.TradeNo = &tradeNo
|
||||
p.BuyerID = buyerID
|
||||
p.SellerID = sellerID
|
||||
p.PayAmount = payAmount
|
||||
p.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
p.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (p *PayOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
p.Status = PayOrderStatusFailed
|
||||
p.ErrorCode = errorCode
|
||||
p.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (p *PayOrder) MarkCancelled() {
|
||||
p.Status = PayOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (p *PayOrder) MarkClosed() {
|
||||
p.Status = PayOrderStatusClosed
|
||||
}
|
||||
|
||||
// NewPayOrder 通用工厂方法 - 创建支付订单(支持多支付渠道)
|
||||
func NewPayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform, payChannel string) *PayOrder {
|
||||
return &PayOrder{
|
||||
ID: uuid.New().String(),
|
||||
RechargeID: rechargeID,
|
||||
OutTradeNo: outTradeNo,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Platform: platform,
|
||||
PayChannel: payChannel,
|
||||
Status: PayOrderStatusPending,
|
||||
}
|
||||
}
|
||||
180
internal/domains/finance/entities/purchase_order.go
Normal file
180
internal/domains/finance/entities/purchase_order.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PurchaseOrderStatus 购买订单状态枚举(通用)
|
||||
type PurchaseOrderStatus string
|
||||
|
||||
const (
|
||||
PurchaseOrderStatusCreated PurchaseOrderStatus = "created" // 已创建
|
||||
PurchaseOrderStatusPaid PurchaseOrderStatus = "paid" // 已支付
|
||||
PurchaseOrderStatusFailed PurchaseOrderStatus = "failed" // 支付失败
|
||||
PurchaseOrderStatusCancelled PurchaseOrderStatus = "cancelled" // 已取消
|
||||
PurchaseOrderStatusRefunded PurchaseOrderStatus = "refunded" // 已退款
|
||||
PurchaseOrderStatusClosed PurchaseOrderStatus = "closed" // 已关闭
|
||||
)
|
||||
|
||||
// PurchaseOrder 购买订单实体(统一表 ty_purchase_orders,兼容多支付渠道)
|
||||
type PurchaseOrder 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"`
|
||||
OrderNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"order_no" comment:"商户订单号"`
|
||||
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"第三方支付交易号"`
|
||||
|
||||
// 产品信息
|
||||
ProductID string `gorm:"type:varchar(36);not null;index" json:"product_id" comment:"产品ID"`
|
||||
ProductCode string `gorm:"type:varchar(50);not null" json:"product_code" comment:"产品编号"`
|
||||
ProductName string `gorm:"type:varchar(200);not null" json:"product_name" comment:"产品名称"`
|
||||
Category string `gorm:"type:varchar(50)" json:"category,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:"订单金额"`
|
||||
PayAmount *decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
|
||||
Status PurchaseOrderStatus `gorm:"type:varchar(20);not null;default:'created';index" json:"status" comment:"订单状态"`
|
||||
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"下单平台:app/h5/pc/wx_h5/wx_mini等"`
|
||||
PayChannel string `gorm:"type:varchar(20);default:'alipay';index" json:"pay_channel" comment:"支付渠道:alipay/wechat"`
|
||||
PaymentType string `gorm:"type:varchar(20);not null" json:"payment_type" comment:"支付类型:alipay, wechat, free"`
|
||||
|
||||
// 支付渠道返回信息
|
||||
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家ID(支付渠道方)"`
|
||||
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家ID(支付渠道方)"`
|
||||
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:"同步返回时间"`
|
||||
PayTime *time.Time `gorm:"index" json:"pay_time,omitempty" comment:"支付完成时间"`
|
||||
|
||||
// 文件信息
|
||||
FilePath *string `gorm:"type:varchar(500)" json:"file_path,omitempty" comment:"产品文件路径"`
|
||||
FileSize *int64 `gorm:"type:bigint" json:"file_size,omitempty" comment:"文件大小(字节)"`
|
||||
|
||||
// 备注信息
|
||||
Remark string `gorm:"type:varchar(500)" json:"remark,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 (PurchaseOrder) TableName() string {
|
||||
return "ty_purchase_orders"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID和订单号
|
||||
func (p *PurchaseOrder) BeforeCreate(tx *gorm.DB) error {
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
if p.OrderNo == "" {
|
||||
p.OrderNo = generatePurchaseOrderNo()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generatePurchaseOrderNo 生成购买订单号
|
||||
func generatePurchaseOrderNo() string {
|
||||
// 使用时间戳+随机数生成唯一订单号,例如:PO202312200001
|
||||
timestamp := time.Now().Format("20060102")
|
||||
random := fmt.Sprintf("%04d", rand.Intn(9999))
|
||||
return fmt.Sprintf("PO%s%s", timestamp, random)
|
||||
}
|
||||
|
||||
// IsCreated 检查是否为已创建状态
|
||||
func (p *PurchaseOrder) IsCreated() bool {
|
||||
return p.Status == PurchaseOrderStatusCreated
|
||||
}
|
||||
|
||||
// IsPaid 检查是否为已支付状态
|
||||
func (p *PurchaseOrder) IsPaid() bool {
|
||||
return p.Status == PurchaseOrderStatusPaid
|
||||
}
|
||||
|
||||
// IsFailed 检查是否为支付失败状态
|
||||
func (p *PurchaseOrder) IsFailed() bool {
|
||||
return p.Status == PurchaseOrderStatusFailed
|
||||
}
|
||||
|
||||
// IsCancelled 检查是否为已取消状态
|
||||
func (p *PurchaseOrder) IsCancelled() bool {
|
||||
return p.Status == PurchaseOrderStatusCancelled
|
||||
}
|
||||
|
||||
// IsRefunded 检查是否为已退款状态
|
||||
func (p *PurchaseOrder) IsRefunded() bool {
|
||||
return p.Status == PurchaseOrderStatusRefunded
|
||||
}
|
||||
|
||||
// IsClosed 检查是否为已关闭状态
|
||||
func (p *PurchaseOrder) IsClosed() bool {
|
||||
return p.Status == PurchaseOrderStatusClosed
|
||||
}
|
||||
|
||||
// MarkPaid 标记为已支付
|
||||
func (p *PurchaseOrder) MarkPaid(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
|
||||
p.Status = PurchaseOrderStatusPaid
|
||||
p.TradeNo = &tradeNo
|
||||
p.BuyerID = buyerID
|
||||
p.SellerID = sellerID
|
||||
p.PayAmount = &payAmount
|
||||
p.ReceiptAmount = receiptAmount
|
||||
now := time.Now()
|
||||
p.PayTime = &now
|
||||
p.NotifyTime = &now
|
||||
}
|
||||
|
||||
// MarkFailed 标记为支付失败
|
||||
func (p *PurchaseOrder) MarkFailed(errorCode, errorMessage string) {
|
||||
p.Status = PurchaseOrderStatusFailed
|
||||
p.ErrorCode = errorCode
|
||||
p.ErrorMessage = errorMessage
|
||||
}
|
||||
|
||||
// MarkCancelled 标记为已取消
|
||||
func (p *PurchaseOrder) MarkCancelled() {
|
||||
p.Status = PurchaseOrderStatusCancelled
|
||||
}
|
||||
|
||||
// MarkRefunded 标记为已退款
|
||||
func (p *PurchaseOrder) MarkRefunded() {
|
||||
p.Status = PurchaseOrderStatusRefunded
|
||||
}
|
||||
|
||||
// MarkClosed 标记为已关闭
|
||||
func (p *PurchaseOrder) MarkClosed() {
|
||||
p.Status = PurchaseOrderStatusClosed
|
||||
}
|
||||
|
||||
// NewPurchaseOrder 通用工厂方法 - 创建购买订单(支持多支付渠道)
|
||||
func NewPurchaseOrder(userID, productID, productCode, productName, subject string, amount decimal.Decimal, platform, payChannel, paymentType string) *PurchaseOrder {
|
||||
return &PurchaseOrder{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
OrderNo: generatePurchaseOrderNo(),
|
||||
ProductID: productID,
|
||||
ProductCode: productCode,
|
||||
ProductName: productName,
|
||||
Subject: subject,
|
||||
Amount: amount,
|
||||
Status: PurchaseOrderStatusCreated,
|
||||
Platform: platform,
|
||||
PayChannel: payChannel,
|
||||
PaymentType: paymentType,
|
||||
}
|
||||
}
|
||||
221
internal/domains/finance/entities/recharge_record.go
Normal file
221
internal/domains/finance/entities/recharge_record.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RechargeType 充值类型枚举
|
||||
type RechargeType string
|
||||
|
||||
const (
|
||||
RechargeTypeAlipay RechargeType = "alipay" // 支付宝充值
|
||||
RechargeTypeWechat RechargeType = "wechat" // 微信充值
|
||||
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:"支付宝订单号"`
|
||||
WechatOrderID *string `gorm:"type:varchar(64);uniqueIndex" json:"wechat_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 != ""
|
||||
hasWechat := r.WechatOrderID != nil && *r.WechatOrderID != ""
|
||||
hasTransfer := r.TransferOrderID != nil && *r.TransferOrderID != ""
|
||||
|
||||
count := 0
|
||||
if hasAlipay {
|
||||
count++
|
||||
}
|
||||
if hasWechat {
|
||||
count++
|
||||
}
|
||||
if hasTransfer {
|
||||
count++
|
||||
}
|
||||
if count > 1 {
|
||||
return errors.New("支付宝、微信或转账订单号只能存在一个")
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.New("必须提供支付宝、微信或转账订单号")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrderID 获取订单号(根据充值类型返回对应的订单号)
|
||||
func (r *RechargeRecord) GetOrderID() string {
|
||||
switch r.RechargeType {
|
||||
case RechargeTypeAlipay:
|
||||
if r.AlipayOrderID != nil {
|
||||
return *r.AlipayOrderID
|
||||
}
|
||||
case RechargeTypeWechat:
|
||||
if r.WechatOrderID != nil {
|
||||
return *r.WechatOrderID
|
||||
}
|
||||
case RechargeTypeTransfer:
|
||||
if r.TransferOrderID != nil {
|
||||
return *r.TransferOrderID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetAlipayOrderID 设置支付宝订单号
|
||||
func (r *RechargeRecord) SetAlipayOrderID(orderID string) {
|
||||
r.AlipayOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetWechatOrderID 设置微信订单号
|
||||
func (r *RechargeRecord) SetWechatOrderID(orderID string) {
|
||||
r.WechatOrderID = &orderID
|
||||
}
|
||||
|
||||
// SetTransferOrderID 设置转账订单号
|
||||
func (r *RechargeRecord) SetTransferOrderID(orderID string) {
|
||||
r.TransferOrderID = &orderID
|
||||
}
|
||||
|
||||
// NewAlipayRechargeRecord 工厂方法 - 创建支付宝充值记录
|
||||
func NewAlipayRechargeRecord(userID string, amount decimal.Decimal, alipayOrderID string) *RechargeRecord {
|
||||
return NewAlipayRechargeRecordWithNotes(userID, amount, alipayOrderID, "")
|
||||
}
|
||||
|
||||
// NewAlipayRechargeRecordWithNotes 工厂方法 - 创建支付宝充值记录(带备注)
|
||||
func NewAlipayRechargeRecordWithNotes(userID string, amount decimal.Decimal, alipayOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeAlipay,
|
||||
Status: RechargeStatusPending,
|
||||
AlipayOrderID: &alipayOrderID,
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
|
||||
// NewWechatRechargeRecord 工厂方法 - 创建微信充值记录
|
||||
func NewWechatRechargeRecord(userID string, amount decimal.Decimal, wechatOrderID string) *RechargeRecord {
|
||||
return NewWechatRechargeRecordWithNotes(userID, amount, wechatOrderID, "")
|
||||
}
|
||||
|
||||
// NewWechatRechargeRecordWithNotes 工厂方法 - 创建微信充值记录(带备注)
|
||||
func NewWechatRechargeRecordWithNotes(userID string, amount decimal.Decimal, wechatOrderID, notes string) *RechargeRecord {
|
||||
return &RechargeRecord{
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
RechargeType: RechargeTypeWechat,
|
||||
Status: RechargeStatusPending,
|
||||
WechatOrderID: &wechatOrderID,
|
||||
Notes: notes,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
71
internal/domains/finance/entities/user_invoice_info.go
Normal file
71
internal/domains/finance/entities/user_invoice_info.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserInvoiceInfo 用户开票信息实体
|
||||
type UserInvoiceInfo struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
UserID string `gorm:"uniqueIndex;type:varchar(36);not null" json:"user_id"`
|
||||
|
||||
// 开票信息字段
|
||||
CompanyName string `gorm:"type:varchar(200);not null" json:"company_name"` // 公司名称
|
||||
TaxpayerID string `gorm:"type:varchar(50);not null" json:"taxpayer_id"` // 纳税人识别号
|
||||
BankName string `gorm:"type:varchar(100)" json:"bank_name"` // 开户银行
|
||||
BankAccount string `gorm:"type:varchar(50)" json:"bank_account"` // 银行账号
|
||||
CompanyAddress string `gorm:"type:varchar(500)" json:"company_address"` // 企业地址
|
||||
CompanyPhone string `gorm:"type:varchar(20)" json:"company_phone"` // 企业电话
|
||||
ReceivingEmail string `gorm:"type:varchar(100);not null" json:"receiving_email"` // 发票接收邮箱
|
||||
|
||||
// 元数据
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (UserInvoiceInfo) TableName() string {
|
||||
return "user_invoice_info"
|
||||
}
|
||||
|
||||
// IsComplete 检查开票信息是否完整
|
||||
func (u *UserInvoiceInfo) IsComplete() bool {
|
||||
return u.CompanyName != "" && u.TaxpayerID != "" && u.ReceivingEmail != ""
|
||||
}
|
||||
|
||||
// IsCompleteForSpecialInvoice 检查专票信息是否完整
|
||||
func (u *UserInvoiceInfo) IsCompleteForSpecialInvoice() bool {
|
||||
return u.CompanyName != "" && u.TaxpayerID != "" && u.BankName != "" &&
|
||||
u.BankAccount != "" && u.CompanyAddress != "" && u.CompanyPhone != "" &&
|
||||
u.ReceivingEmail != ""
|
||||
}
|
||||
|
||||
// GetMissingFields 获取缺失的字段
|
||||
func (u *UserInvoiceInfo) GetMissingFields() []string {
|
||||
var missing []string
|
||||
if u.CompanyName == "" {
|
||||
missing = append(missing, "公司名称")
|
||||
}
|
||||
if u.TaxpayerID == "" {
|
||||
missing = append(missing, "纳税人识别号")
|
||||
}
|
||||
if u.BankName == "" {
|
||||
missing = append(missing, "开户银行")
|
||||
}
|
||||
if u.BankAccount == "" {
|
||||
missing = append(missing, "银行账号")
|
||||
}
|
||||
if u.CompanyAddress == "" {
|
||||
missing = append(missing, "企业地址")
|
||||
}
|
||||
if u.CompanyPhone == "" {
|
||||
missing = append(missing, "企业电话")
|
||||
}
|
||||
if u.ReceivingEmail == "" {
|
||||
missing = append(missing, "发票接收邮箱")
|
||||
}
|
||||
return missing
|
||||
}
|
||||
107
internal/domains/finance/entities/wallet.go
Normal file
107
internal/domains/finance/entities/wallet.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Wallet 钱包聚合根
|
||||
// 用户数字钱包的核心信息,支持多种钱包类型和精确的余额管理
|
||||
// 使用decimal类型确保金额计算的精确性,避免浮点数精度问题
|
||||
// 支持欠费(余额<0),但只允许扣到小于0一次,之后不能再扣
|
||||
// 新建钱包时可配置默认额度
|
||||
|
||||
type Wallet struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"钱包唯一标识"`
|
||||
UserID string `gorm:"type:varchar(36);not null;uniqueIndex" json:"user_id" comment:"关联用户ID"`
|
||||
|
||||
// 钱包状态 - 钱包的基本状态信息
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"钱包是否激活"`
|
||||
Balance decimal.Decimal `gorm:"type:decimal(20,8);default:0" json:"balance" comment:"钱包余额(精确到8位小数)"`
|
||||
Version int64 `gorm:"default:0" json:"version" 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 (Wallet) TableName() string {
|
||||
return "wallets"
|
||||
}
|
||||
|
||||
// IsZeroBalance 检查余额是否为零
|
||||
func (w *Wallet) IsZeroBalance() bool {
|
||||
return w.Balance.IsZero()
|
||||
}
|
||||
|
||||
// HasSufficientBalance 检查是否有足够余额(允许透支额度)
|
||||
func (w *Wallet) HasSufficientBalance(amount decimal.Decimal) bool {
|
||||
// 允许扣到额度下限
|
||||
return w.Balance.Sub(amount).GreaterThanOrEqual(decimal.Zero)
|
||||
}
|
||||
|
||||
// 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 扣减余额,含欠费业务规则
|
||||
func (w *Wallet) SubtractBalance(amount decimal.Decimal) error {
|
||||
if w.Balance.LessThan(decimal.Zero) {
|
||||
return fmt.Errorf("已欠费,不能再扣款")
|
||||
}
|
||||
newBalance := w.Balance.Sub(amount)
|
||||
w.Balance = newBalance
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFormattedBalance 获取格式化的余额字符串
|
||||
func (w *Wallet) GetFormattedBalance() string {
|
||||
return w.Balance.String()
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (w *Wallet) BeforeCreate(tx *gorm.DB) error {
|
||||
if w.ID == "" {
|
||||
w.ID = uuid.New().String()
|
||||
}
|
||||
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(36);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,
|
||||
}
|
||||
}
|
||||
33
internal/domains/finance/entities/wechat_order.go
Normal file
33
internal/domains/finance/entities/wechat_order.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package entities
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
|
||||
// WechatOrderStatus 微信订单状态枚举(别名)
|
||||
type WechatOrderStatus = PayOrderStatus
|
||||
|
||||
const (
|
||||
WechatOrderStatusPending WechatOrderStatus = PayOrderStatusPending // 待支付
|
||||
WechatOrderStatusSuccess WechatOrderStatus = PayOrderStatusSuccess // 支付成功
|
||||
WechatOrderStatusFailed WechatOrderStatus = PayOrderStatusFailed // 支付失败
|
||||
WechatOrderStatusCancelled WechatOrderStatus = PayOrderStatusCancelled // 已取消
|
||||
WechatOrderStatusClosed WechatOrderStatus = PayOrderStatusClosed // 已关闭
|
||||
)
|
||||
|
||||
const (
|
||||
WechatOrderPlatformApp = "app" // 微信APP支付
|
||||
WechatOrderPlatformH5 = "h5" // 微信H5支付
|
||||
WechatOrderPlatformMini = "mini" // 微信小程序支付
|
||||
)
|
||||
|
||||
// WechatOrder 微信订单实体(统一表 typay_orders,兼容多支付渠道)
|
||||
type WechatOrder = PayOrder
|
||||
|
||||
// NewWechatOrder 工厂方法 - 创建微信订单(统一表 typay_orders)
|
||||
func NewWechatOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *WechatOrder {
|
||||
return NewPayOrder(rechargeID, outTradeNo, subject, amount, platform, "wechat")
|
||||
}
|
||||
|
||||
// NewWechatPayOrder 工厂方法 - 创建微信支付订单(别名,保持向后兼容)
|
||||
func NewWechatPayOrder(rechargeID, outTradeNo, subject string, amount decimal.Decimal, platform string) *WechatOrder {
|
||||
return NewWechatOrder(rechargeID, outTradeNo, subject, amount, platform)
|
||||
}
|
||||
213
internal/domains/finance/events/invoice_events.go
Normal file
213
internal/domains/finance/events/invoice_events.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// BaseEvent 基础事件结构
|
||||
type BaseEvent struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Source string `json:"source"`
|
||||
AggregateID string `json:"aggregate_id"`
|
||||
AggregateType string `json:"aggregate_type"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// NewBaseEvent 创建基础事件
|
||||
func NewBaseEvent(eventType, aggregateID, aggregateType string) BaseEvent {
|
||||
return BaseEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: eventType,
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "finance-domain",
|
||||
AggregateID: aggregateID,
|
||||
AggregateType: aggregateType,
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// GetID 获取事件ID
|
||||
func (e BaseEvent) GetID() string {
|
||||
return e.ID
|
||||
}
|
||||
|
||||
// GetType 获取事件类型
|
||||
func (e BaseEvent) GetType() string {
|
||||
return e.Type
|
||||
}
|
||||
|
||||
// GetVersion 获取事件版本
|
||||
func (e BaseEvent) GetVersion() string {
|
||||
return e.Version
|
||||
}
|
||||
|
||||
// GetTimestamp 获取事件时间戳
|
||||
func (e BaseEvent) GetTimestamp() time.Time {
|
||||
return e.Timestamp
|
||||
}
|
||||
|
||||
// GetSource 获取事件来源
|
||||
func (e BaseEvent) GetSource() string {
|
||||
return e.Source
|
||||
}
|
||||
|
||||
// GetAggregateID 获取聚合根ID
|
||||
func (e BaseEvent) GetAggregateID() string {
|
||||
return e.AggregateID
|
||||
}
|
||||
|
||||
// GetAggregateType 获取聚合根类型
|
||||
func (e BaseEvent) GetAggregateType() string {
|
||||
return e.AggregateType
|
||||
}
|
||||
|
||||
// GetMetadata 获取事件元数据
|
||||
func (e BaseEvent) GetMetadata() map[string]interface{} {
|
||||
return e.Metadata
|
||||
}
|
||||
|
||||
// Marshal 序列化事件
|
||||
func (e BaseEvent) Marshal() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
// Unmarshal 反序列化事件
|
||||
func (e BaseEvent) Unmarshal(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
// InvoiceApplicationCreatedEvent 发票申请创建事件
|
||||
type InvoiceApplicationCreatedEvent struct {
|
||||
BaseEvent
|
||||
ApplicationID string `json:"application_id"`
|
||||
UserID string `json:"user_id"`
|
||||
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
CompanyName string `json:"company_name"`
|
||||
ReceivingEmail string `json:"receiving_email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// NewInvoiceApplicationCreatedEvent 创建发票申请创建事件
|
||||
func NewInvoiceApplicationCreatedEvent(applicationID, userID string, invoiceType value_objects.InvoiceType, amount decimal.Decimal, companyName, receivingEmail string) *InvoiceApplicationCreatedEvent {
|
||||
event := &InvoiceApplicationCreatedEvent{
|
||||
BaseEvent: NewBaseEvent("InvoiceApplicationCreated", applicationID, "InvoiceApplication"),
|
||||
ApplicationID: applicationID,
|
||||
UserID: userID,
|
||||
InvoiceType: invoiceType,
|
||||
Amount: amount,
|
||||
CompanyName: companyName,
|
||||
ReceivingEmail: receivingEmail,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// GetPayload 获取事件载荷
|
||||
func (e *InvoiceApplicationCreatedEvent) GetPayload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
// InvoiceApplicationApprovedEvent 发票申请通过事件
|
||||
type InvoiceApplicationApprovedEvent struct {
|
||||
BaseEvent
|
||||
ApplicationID string `json:"application_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
ReceivingEmail string `json:"receiving_email"`
|
||||
ApprovedAt time.Time `json:"approved_at"`
|
||||
}
|
||||
|
||||
// NewInvoiceApplicationApprovedEvent 创建发票申请通过事件
|
||||
func NewInvoiceApplicationApprovedEvent(applicationID, userID string, amount decimal.Decimal, receivingEmail string) *InvoiceApplicationApprovedEvent {
|
||||
event := &InvoiceApplicationApprovedEvent{
|
||||
BaseEvent: NewBaseEvent("InvoiceApplicationApproved", applicationID, "InvoiceApplication"),
|
||||
ApplicationID: applicationID,
|
||||
UserID: userID,
|
||||
Amount: amount,
|
||||
ReceivingEmail: receivingEmail,
|
||||
ApprovedAt: time.Now(),
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// GetPayload 获取事件载荷
|
||||
func (e *InvoiceApplicationApprovedEvent) GetPayload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
// InvoiceApplicationRejectedEvent 发票申请拒绝事件
|
||||
type InvoiceApplicationRejectedEvent struct {
|
||||
BaseEvent
|
||||
ApplicationID string `json:"application_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Reason string `json:"reason"`
|
||||
ReceivingEmail string `json:"receiving_email"`
|
||||
RejectedAt time.Time `json:"rejected_at"`
|
||||
}
|
||||
|
||||
// NewInvoiceApplicationRejectedEvent 创建发票申请拒绝事件
|
||||
func NewInvoiceApplicationRejectedEvent(applicationID, userID, reason, receivingEmail string) *InvoiceApplicationRejectedEvent {
|
||||
event := &InvoiceApplicationRejectedEvent{
|
||||
BaseEvent: NewBaseEvent("InvoiceApplicationRejected", applicationID, "InvoiceApplication"),
|
||||
ApplicationID: applicationID,
|
||||
UserID: userID,
|
||||
Reason: reason,
|
||||
ReceivingEmail: receivingEmail,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// GetPayload 获取事件载荷
|
||||
func (e *InvoiceApplicationRejectedEvent) GetPayload() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
// InvoiceFileUploadedEvent 发票文件上传事件
|
||||
type InvoiceFileUploadedEvent struct {
|
||||
BaseEvent
|
||||
InvoiceID string `json:"invoice_id"`
|
||||
UserID string `json:"user_id"`
|
||||
FileID string `json:"file_id"`
|
||||
FileName string `json:"file_name"`
|
||||
FileURL string `json:"file_url"`
|
||||
ReceivingEmail string `json:"receiving_email"`
|
||||
CompanyName string `json:"company_name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
|
||||
UploadedAt time.Time `json:"uploaded_at"`
|
||||
}
|
||||
|
||||
// NewInvoiceFileUploadedEvent 创建发票文件上传事件
|
||||
func NewInvoiceFileUploadedEvent(invoiceID, userID, fileID, fileName, fileURL, receivingEmail, companyName string, amount decimal.Decimal, invoiceType value_objects.InvoiceType) *InvoiceFileUploadedEvent {
|
||||
event := &InvoiceFileUploadedEvent{
|
||||
BaseEvent: NewBaseEvent("InvoiceFileUploaded", invoiceID, "InvoiceApplication"),
|
||||
InvoiceID: invoiceID,
|
||||
UserID: userID,
|
||||
FileID: fileID,
|
||||
FileName: fileName,
|
||||
FileURL: fileURL,
|
||||
ReceivingEmail: receivingEmail,
|
||||
CompanyName: companyName,
|
||||
Amount: amount,
|
||||
InvoiceType: invoiceType,
|
||||
UploadedAt: time.Now(),
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// GetPayload 获取事件载荷
|
||||
func (e *InvoiceFileUploadedEvent) GetPayload() interface{} {
|
||||
return e
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hyapi-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)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
)
|
||||
|
||||
// InvoiceApplicationRepository 发票申请仓储接口
|
||||
type InvoiceApplicationRepository interface {
|
||||
Create(ctx context.Context, application *entities.InvoiceApplication) error
|
||||
Update(ctx context.Context, application *entities.InvoiceApplication) error
|
||||
Save(ctx context.Context, application *entities.InvoiceApplication) error
|
||||
FindByID(ctx context.Context, id string) (*entities.InvoiceApplication, error)
|
||||
FindByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error)
|
||||
FindPendingApplications(ctx context.Context, page, pageSize int) ([]*entities.InvoiceApplication, int64, error)
|
||||
FindByStatus(ctx context.Context, status entities.ApplicationStatus) ([]*entities.InvoiceApplication, error)
|
||||
FindByUserIDAndStatus(ctx context.Context, userID string, status entities.ApplicationStatus, page, pageSize int) ([]*entities.InvoiceApplication, int64, error)
|
||||
FindByUserIDAndStatusWithTimeRange(ctx context.Context, userID string, status entities.ApplicationStatus, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error)
|
||||
FindByStatusWithTimeRange(ctx context.Context, status entities.ApplicationStatus, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error)
|
||||
FindAllWithTimeRange(ctx context.Context, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error)
|
||||
|
||||
GetUserTotalInvoicedAmount(ctx context.Context, userID string) (string, error)
|
||||
GetUserTotalAppliedAmount(ctx context.Context, userID string) (string, error)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
finance_entities "hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// PurchaseOrderRepository 购买订单仓储接口
|
||||
type PurchaseOrderRepository interface {
|
||||
// 创建订单
|
||||
Create(ctx context.Context, order *finance_entities.PurchaseOrder) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 更新订单
|
||||
Update(ctx context.Context, order *finance_entities.PurchaseOrder) error
|
||||
|
||||
// 根据ID获取订单
|
||||
GetByID(ctx context.Context, id string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据订单号获取订单
|
||||
GetByOrderNo(ctx context.Context, orderNo string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据用户ID获取订单列表
|
||||
GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*finance_entities.PurchaseOrder, int64, error)
|
||||
|
||||
// 根据产品ID和用户ID获取订单
|
||||
GetByUserIDAndProductID(ctx context.Context, userID, productID string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据支付类型和第三方交易号获取订单
|
||||
GetByPaymentTypeAndTransactionID(ctx context.Context, paymentType, transactionID string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据交易号获取订单
|
||||
GetByTradeNo(ctx context.Context, tradeNo string) (*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 更新支付状态
|
||||
UpdatePaymentStatus(ctx context.Context, orderID string, status finance_entities.PurchaseOrderStatus, tradeNo *string, payAmount, receiptAmount *string, paymentTime *time.Time) error
|
||||
|
||||
// 获取用户已购买的产品编号列表
|
||||
GetUserPurchasedProductCodes(ctx context.Context, userID string) ([]string, error)
|
||||
|
||||
// 检查用户是否已购买指定产品
|
||||
HasUserPurchased(ctx context.Context, userID string, productCode string) (bool, error)
|
||||
|
||||
// 获取即将过期的订单(用于清理)
|
||||
GetExpiringOrders(ctx context.Context, before time.Time, limit int) ([]*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 获取已过期订单(用于清理)
|
||||
GetExpiredOrders(ctx context.Context, limit int) ([]*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 获取用户已支付的产品ID列表
|
||||
GetUserPaidProductIDs(ctx context.Context, userID string) ([]string, error)
|
||||
|
||||
// 根据状态获取订单列表
|
||||
GetByStatus(ctx context.Context, status finance_entities.PurchaseOrderStatus, limit, offset int) ([]*finance_entities.PurchaseOrder, int64, error)
|
||||
|
||||
// 根据筛选条件获取订单列表
|
||||
GetByFilters(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*finance_entities.PurchaseOrder, error)
|
||||
|
||||
// 根据筛选条件统计订单数量
|
||||
CountByFilters(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package queries
|
||||
|
||||
// ListWalletsQuery 钱包列表查询参数
|
||||
type ListWalletsQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
UserID string `json:"user_id"`
|
||||
WalletType string `json:"wallet_type"`
|
||||
WalletAddress string `json:"wallet_address"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
}
|
||||
|
||||
// ListUserSecretsQuery 用户密钥列表查询参数
|
||||
type ListUserSecretsQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
UserID string `json:"user_id"`
|
||||
SecretType string `json:"secret_type"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-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)
|
||||
|
||||
// 统计相关方法
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
)
|
||||
|
||||
// UserInvoiceInfoRepository 用户开票信息仓储接口
|
||||
type UserInvoiceInfoRepository interface {
|
||||
// Create 创建用户开票信息
|
||||
Create(ctx context.Context, info *entities.UserInvoiceInfo) error
|
||||
|
||||
// Update 更新用户开票信息
|
||||
Update(ctx context.Context, info *entities.UserInvoiceInfo) error
|
||||
|
||||
// Save 保存用户开票信息(创建或更新)
|
||||
Save(ctx context.Context, info *entities.UserInvoiceInfo) error
|
||||
|
||||
// FindByUserID 根据用户ID查找开票信息
|
||||
FindByUserID(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error)
|
||||
|
||||
// FindByID 根据ID查找开票信息
|
||||
FindByID(ctx context.Context, id string) (*entities.UserInvoiceInfo, error)
|
||||
|
||||
// Delete 删除用户开票信息
|
||||
Delete(ctx context.Context, userID string) error
|
||||
|
||||
// Exists 检查用户开票信息是否存在
|
||||
Exists(ctx context.Context, userID string) (bool, error)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// 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, amount decimal.Decimal, operation string) (bool, error)
|
||||
// 乐观锁更新(通过用户ID直接更新,避免重复查询)
|
||||
UpdateBalanceByUserID(ctx context.Context, userID string, amount decimal.Decimal, operation string) (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,48 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-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)
|
||||
|
||||
// 统计相关方法
|
||||
CountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (int64, error)
|
||||
GetTotalAmountByUserId(ctx context.Context, userId string) (float64, error)
|
||||
GetTotalAmountByUserIdAndDateRange(ctx context.Context, userId string, startDate, endDate time.Time) (float64, error)
|
||||
GetDailyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetMonthlyStatsByUserId(ctx context.Context, userId string, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
|
||||
// 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
|
||||
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error)
|
||||
|
||||
// 管理端:导出钱包交易记录(包含产品名称和企业信息)
|
||||
ExportWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}) ([]*entities.WalletTransaction, error)
|
||||
|
||||
// 系统级别统计方法
|
||||
GetSystemTotalAmount(ctx context.Context) (float64, error)
|
||||
GetSystemAmountByDateRange(ctx context.Context, startDate, endDate time.Time) (float64, error)
|
||||
GetSystemDailyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
GetSystemMonthlyStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
)
|
||||
|
||||
// WechatOrderRepository 微信订单仓储接口
|
||||
type WechatOrderRepository interface {
|
||||
Create(ctx context.Context, order entities.WechatOrder) (entities.WechatOrder, error)
|
||||
GetByID(ctx context.Context, id string) (entities.WechatOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*entities.WechatOrder, error)
|
||||
GetByRechargeID(ctx context.Context, rechargeID string) (*entities.WechatOrder, error)
|
||||
GetByUserID(ctx context.Context, userID string) ([]entities.WechatOrder, error)
|
||||
Update(ctx context.Context, order entities.WechatOrder) error
|
||||
UpdateStatus(ctx context.Context, id string, status entities.WechatOrderStatus) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
Exists(ctx context.Context, id string) (bool, error)
|
||||
}
|
||||
236
internal/domains/finance/services/balance_alert_service.go
Normal file
236
internal/domains/finance/services/balance_alert_service.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"hyapi-server/internal/config"
|
||||
"hyapi-server/internal/domains/api/entities"
|
||||
api_repositories "hyapi-server/internal/domains/api/repositories"
|
||||
user_repositories "hyapi-server/internal/domains/user/repositories"
|
||||
"hyapi-server/internal/infrastructure/external/notification"
|
||||
"hyapi-server/internal/infrastructure/external/sms"
|
||||
)
|
||||
|
||||
// BalanceAlertService 余额预警服务接口
|
||||
type BalanceAlertService interface {
|
||||
CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error
|
||||
}
|
||||
|
||||
// BalanceAlertServiceImpl 余额预警服务实现
|
||||
type BalanceAlertServiceImpl struct {
|
||||
apiUserRepo api_repositories.ApiUserRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository
|
||||
smsService sms.SMSSender
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
wechatWorkService *notification.WeChatWorkService
|
||||
}
|
||||
|
||||
// NewBalanceAlertService 创建余额预警服务
|
||||
func NewBalanceAlertService(
|
||||
apiUserRepo api_repositories.ApiUserRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||
smsService sms.SMSSender,
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
) BalanceAlertService {
|
||||
var wechatSvc *notification.WeChatWorkService
|
||||
if config != nil && config.WechatWork.WebhookURL != "" {
|
||||
wechatSvc = notification.NewWeChatWorkService(config.WechatWork.WebhookURL, config.WechatWork.Secret, logger)
|
||||
}
|
||||
return &BalanceAlertServiceImpl{
|
||||
apiUserRepo: apiUserRepo,
|
||||
userRepo: userRepo,
|
||||
enterpriseInfoRepo: enterpriseInfoRepo,
|
||||
smsService: smsService,
|
||||
config: config,
|
||||
logger: logger,
|
||||
wechatWorkService: wechatSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndSendAlert 检查余额并发送预警
|
||||
func (s *BalanceAlertServiceImpl) CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error {
|
||||
// 1. 获取API用户信息
|
||||
apiUser, err := s.apiUserRepo.FindByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取API用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取API用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
if apiUser == nil {
|
||||
s.logger.Debug("API用户不存在,跳过余额预警检查", zap.String("user_id", userID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. 兼容性处理:如果API用户没有配置预警信息,从用户表获取并更新
|
||||
needUpdate := false
|
||||
if apiUser.AlertPhone == "" {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
if user.Phone != "" {
|
||||
apiUser.AlertPhone = user.Phone
|
||||
needUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 兼容性处理:如果API用户没有配置预警阈值,使用默认值
|
||||
if apiUser.BalanceAlertThreshold == 0 {
|
||||
apiUser.BalanceAlertThreshold = s.config.Wallet.BalanceAlert.DefaultThreshold
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
// 4. 如果需要更新API用户信息,保存到数据库
|
||||
if needUpdate {
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警配置失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
// 不返回错误,继续执行预警检查
|
||||
}
|
||||
}
|
||||
|
||||
balanceFloat, _ := balance.Float64()
|
||||
|
||||
// 5. 检查是否需要发送欠费预警(不受冷却期限制)
|
||||
if apiUser.ShouldSendArrearsAlert(balanceFloat) {
|
||||
if err := s.sendArrearsAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送欠费预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 欠费预警不受冷却期限制,不需要更新LastArrearsAlert时间
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. 检查是否需要发送低余额预警
|
||||
if apiUser.ShouldSendLowBalanceAlert(balanceFloat) {
|
||||
if err := s.sendLowBalanceAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送低余额预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 标记预警已发送
|
||||
apiUser.MarkLowBalanceAlertSent()
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警时间失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendArrearsAlert 发送欠费预警
|
||||
func (s *BalanceAlertServiceImpl) sendArrearsAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", "海宇数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "海宇数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送欠费预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
if err := s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 企业微信欠费告警通知(仅展示企业名称和联系手机)
|
||||
if s.wechatWorkService != nil {
|
||||
content := fmt.Sprintf(
|
||||
"### 【海宇数据】用户余额欠费告警\n"+
|
||||
"<font color=\"warning\">该企业已发生欠费,请及时联系并处理。</font>\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 当前余额:%.2f 元\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
apiUser.AlertPhone,
|
||||
balance,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendLowBalanceAlert 发送低余额预警
|
||||
func (s *BalanceAlertServiceImpl) sendLowBalanceAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", "海宇数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "海宇数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送低余额预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.Float64("threshold", apiUser.BalanceAlertThreshold),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
if err := s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 企业微信余额预警通知(仅展示企业名称和联系手机)
|
||||
if s.wechatWorkService != nil {
|
||||
content := fmt.Sprintf(
|
||||
"### 【海宇数据】用户余额预警\n"+
|
||||
"<font color=\"warning\">用户余额已低于预警阈值,请及时跟进。</font>\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 当前余额:%.2f 元\n"+
|
||||
"> 预警阈值:%.2f 元\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
apiUser.AlertPhone,
|
||||
balance,
|
||||
apiUser.BalanceAlertThreshold,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
277
internal/domains/finance/services/invoice_aggregate_service.go
Normal file
277
internal/domains/finance/services/invoice_aggregate_service.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/domains/finance/events"
|
||||
"hyapi-server/internal/domains/finance/repositories"
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
|
||||
"hyapi-server/internal/infrastructure/external/storage"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ApplyInvoiceRequest 申请开票请求
|
||||
type ApplyInvoiceRequest struct {
|
||||
InvoiceType value_objects.InvoiceType `json:"invoice_type" binding:"required"`
|
||||
Amount string `json:"amount" binding:"required"`
|
||||
InvoiceInfo *value_objects.InvoiceInfo `json:"invoice_info" binding:"required"`
|
||||
}
|
||||
|
||||
// ApproveInvoiceRequest 通过发票申请请求
|
||||
type ApproveInvoiceRequest struct {
|
||||
AdminNotes string `json:"admin_notes"`
|
||||
}
|
||||
|
||||
// RejectInvoiceRequest 拒绝发票申请请求
|
||||
type RejectInvoiceRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
// InvoiceAggregateService 发票聚合服务接口
|
||||
// 职责:协调发票申请聚合根的生命周期,调用领域服务进行业务规则验证,发布领域事件
|
||||
type InvoiceAggregateService interface {
|
||||
// 申请开票
|
||||
ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*entities.InvoiceApplication, error)
|
||||
|
||||
// 通过发票申请(上传发票)
|
||||
ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error
|
||||
|
||||
// 拒绝发票申请
|
||||
RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error
|
||||
}
|
||||
|
||||
// InvoiceAggregateServiceImpl 发票聚合服务实现
|
||||
type InvoiceAggregateServiceImpl struct {
|
||||
applicationRepo repositories.InvoiceApplicationRepository
|
||||
userInvoiceInfoRepo repositories.UserInvoiceInfoRepository
|
||||
domainService InvoiceDomainService
|
||||
qiniuStorageService *storage.QiNiuStorageService
|
||||
logger *zap.Logger
|
||||
eventPublisher EventPublisher
|
||||
}
|
||||
|
||||
// EventPublisher 事件发布器接口
|
||||
type EventPublisher interface {
|
||||
PublishInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error
|
||||
PublishInvoiceApplicationApproved(ctx context.Context, event *events.InvoiceApplicationApprovedEvent) error
|
||||
PublishInvoiceApplicationRejected(ctx context.Context, event *events.InvoiceApplicationRejectedEvent) error
|
||||
PublishInvoiceFileUploaded(ctx context.Context, event *events.InvoiceFileUploadedEvent) error
|
||||
}
|
||||
|
||||
// NewInvoiceAggregateService 创建发票聚合服务
|
||||
func NewInvoiceAggregateService(
|
||||
applicationRepo repositories.InvoiceApplicationRepository,
|
||||
userInvoiceInfoRepo repositories.UserInvoiceInfoRepository,
|
||||
domainService InvoiceDomainService,
|
||||
qiniuStorageService *storage.QiNiuStorageService,
|
||||
logger *zap.Logger,
|
||||
eventPublisher EventPublisher,
|
||||
) InvoiceAggregateService {
|
||||
return &InvoiceAggregateServiceImpl{
|
||||
applicationRepo: applicationRepo,
|
||||
userInvoiceInfoRepo: userInvoiceInfoRepo,
|
||||
domainService: domainService,
|
||||
qiniuStorageService: qiniuStorageService,
|
||||
logger: logger,
|
||||
eventPublisher: eventPublisher,
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyInvoice 申请开票
|
||||
func (s *InvoiceAggregateServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*entities.InvoiceApplication, error) {
|
||||
// 1. 解析金额
|
||||
amount, err := decimal.NewFromString(req.Amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的金额格式: %w", err)
|
||||
}
|
||||
|
||||
// 2. 验证发票信息
|
||||
if err := s.domainService.ValidateInvoiceInfo(ctx, req.InvoiceInfo, req.InvoiceType); err != nil {
|
||||
return nil, fmt.Errorf("发票信息验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 获取用户开票信息
|
||||
userInvoiceInfo, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
|
||||
}
|
||||
if userInvoiceInfo == nil {
|
||||
return nil, fmt.Errorf("用户开票信息不存在")
|
||||
}
|
||||
|
||||
// 4. 创建发票申请聚合根
|
||||
application := entities.NewInvoiceApplication(userID, req.InvoiceType, amount, userInvoiceInfo.ID)
|
||||
|
||||
// 5. 设置开票信息快照
|
||||
application.SetInvoiceInfoSnapshot(req.InvoiceInfo)
|
||||
|
||||
// 6. 验证聚合根业务规则
|
||||
if err := s.domainService.ValidateInvoiceApplication(ctx, application); err != nil {
|
||||
return nil, fmt.Errorf("发票申请业务规则验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 7. 保存聚合根
|
||||
if err := s.applicationRepo.Create(ctx, application); err != nil {
|
||||
return nil, fmt.Errorf("保存发票申请失败: %w", err)
|
||||
}
|
||||
|
||||
// 8. 发布领域事件
|
||||
event := events.NewInvoiceApplicationCreatedEvent(
|
||||
application.ID,
|
||||
application.UserID,
|
||||
application.InvoiceType,
|
||||
application.Amount,
|
||||
application.CompanyName,
|
||||
application.ReceivingEmail,
|
||||
)
|
||||
|
||||
if err := s.eventPublisher.PublishInvoiceApplicationCreated(ctx, event); err != nil {
|
||||
// 记录错误但不影响主流程
|
||||
fmt.Printf("发布发票申请创建事件失败: %v\n", err)
|
||||
}
|
||||
|
||||
return application, nil
|
||||
}
|
||||
|
||||
// ApproveInvoiceApplication 通过发票申请(上传发票)
|
||||
func (s *InvoiceAggregateServiceImpl) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error {
|
||||
// 1. 获取发票申请
|
||||
application, err := s.applicationRepo.FindByID(ctx, applicationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取发票申请失败: %w", err)
|
||||
}
|
||||
if application == nil {
|
||||
return fmt.Errorf("发票申请不存在")
|
||||
}
|
||||
|
||||
// 2. 验证状态转换
|
||||
if err := s.domainService.ValidateStatusTransition(application.Status, entities.ApplicationStatusCompleted); err != nil {
|
||||
return fmt.Errorf("状态转换验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 处理文件上传
|
||||
// 读取文件内容
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
s.logger.Error("读取上传文件失败", zap.Error(err))
|
||||
return fmt.Errorf("读取上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成文件名(使用时间戳确保唯一性)
|
||||
fileName := fmt.Sprintf("invoice_%s_%d.pdf", applicationID, time.Now().Unix())
|
||||
|
||||
// 上传文件到七牛云
|
||||
uploadResult, err := s.qiniuStorageService.UploadFile(ctx, fileBytes, fileName)
|
||||
if err != nil {
|
||||
s.logger.Error("上传发票文件到七牛云失败", zap.String("file_name", fileName), zap.Error(err))
|
||||
return fmt.Errorf("上传发票文件到七牛云失败: %w", err)
|
||||
}
|
||||
|
||||
// 从上传结果获取文件信息
|
||||
fileID := uploadResult.Key
|
||||
fileURL := uploadResult.URL
|
||||
fileSize := uploadResult.Size
|
||||
|
||||
// 4. 更新聚合根状态
|
||||
application.MarkCompleted("admin_user_id")
|
||||
application.SetFileInfo(fileID, fileName, fileURL, fileSize)
|
||||
application.AdminNotes = &req.AdminNotes
|
||||
|
||||
// 5. 保存聚合根
|
||||
if err := s.applicationRepo.Update(ctx, application); err != nil {
|
||||
return fmt.Errorf("更新发票申请失败: %w", err)
|
||||
}
|
||||
|
||||
// 6. 发布领域事件
|
||||
approvedEvent := events.NewInvoiceApplicationApprovedEvent(
|
||||
application.ID,
|
||||
application.UserID,
|
||||
application.Amount,
|
||||
application.ReceivingEmail,
|
||||
)
|
||||
|
||||
if err := s.eventPublisher.PublishInvoiceApplicationApproved(ctx, approvedEvent); err != nil {
|
||||
s.logger.Error("发布发票申请通过事件失败",
|
||||
zap.String("application_id", applicationID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 事件发布失败不影响主流程,只记录日志
|
||||
} else {
|
||||
s.logger.Info("发票申请通过事件发布成功",
|
||||
zap.String("application_id", applicationID),
|
||||
)
|
||||
}
|
||||
|
||||
fileUploadedEvent := events.NewInvoiceFileUploadedEvent(
|
||||
application.ID,
|
||||
application.UserID,
|
||||
fileID,
|
||||
fileName,
|
||||
fileURL,
|
||||
application.ReceivingEmail,
|
||||
application.CompanyName,
|
||||
application.Amount,
|
||||
application.InvoiceType,
|
||||
)
|
||||
|
||||
if err := s.eventPublisher.PublishInvoiceFileUploaded(ctx, fileUploadedEvent); err != nil {
|
||||
s.logger.Error("发布发票文件上传事件失败",
|
||||
zap.String("application_id", applicationID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 事件发布失败不影响主流程,只记录日志
|
||||
} else {
|
||||
s.logger.Info("发票文件上传事件发布成功",
|
||||
zap.String("application_id", applicationID),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RejectInvoiceApplication 拒绝发票申请
|
||||
func (s *InvoiceAggregateServiceImpl) RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error {
|
||||
// 1. 获取发票申请
|
||||
application, err := s.applicationRepo.FindByID(ctx, applicationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取发票申请失败: %w", err)
|
||||
}
|
||||
if application == nil {
|
||||
return fmt.Errorf("发票申请不存在")
|
||||
}
|
||||
|
||||
// 2. 验证状态转换
|
||||
if err := s.domainService.ValidateStatusTransition(application.Status, entities.ApplicationStatusRejected); err != nil {
|
||||
return fmt.Errorf("状态转换验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 更新聚合根状态
|
||||
application.MarkRejected(req.Reason, "admin_user_id")
|
||||
|
||||
// 4. 保存聚合根
|
||||
if err := s.applicationRepo.Update(ctx, application); err != nil {
|
||||
return fmt.Errorf("更新发票申请失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 发布领域事件
|
||||
event := events.NewInvoiceApplicationRejectedEvent(
|
||||
application.ID,
|
||||
application.UserID,
|
||||
req.Reason,
|
||||
application.ReceivingEmail,
|
||||
)
|
||||
|
||||
if err := s.eventPublisher.PublishInvoiceApplicationRejected(ctx, event); err != nil {
|
||||
fmt.Printf("发布发票申请拒绝事件失败: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
152
internal/domains/finance/services/invoice_domain_service.go
Normal file
152
internal/domains/finance/services/invoice_domain_service.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// InvoiceDomainService 发票领域服务接口
|
||||
// 职责:处理发票领域的业务规则和计算逻辑,不涉及外部依赖
|
||||
type InvoiceDomainService interface {
|
||||
// 验证发票信息完整性
|
||||
ValidateInvoiceInfo(ctx context.Context, info *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error
|
||||
|
||||
// 验证开票金额是否合法(基于业务规则)
|
||||
ValidateInvoiceAmount(ctx context.Context, amount decimal.Decimal, availableAmount decimal.Decimal) error
|
||||
|
||||
// 计算可开票金额(纯计算逻辑)
|
||||
CalculateAvailableAmount(totalRecharged decimal.Decimal, totalGifted decimal.Decimal, totalInvoiced decimal.Decimal) decimal.Decimal
|
||||
|
||||
// 验证发票申请状态转换
|
||||
ValidateStatusTransition(currentStatus entities.ApplicationStatus, targetStatus entities.ApplicationStatus) error
|
||||
|
||||
// 验证发票申请业务规则
|
||||
ValidateInvoiceApplication(ctx context.Context, application *entities.InvoiceApplication) error
|
||||
}
|
||||
|
||||
// InvoiceDomainServiceImpl 发票领域服务实现
|
||||
type InvoiceDomainServiceImpl struct {
|
||||
// 领域服务不依赖仓储,只处理业务规则
|
||||
}
|
||||
|
||||
// NewInvoiceDomainService 创建发票领域服务
|
||||
func NewInvoiceDomainService() InvoiceDomainService {
|
||||
return &InvoiceDomainServiceImpl{}
|
||||
}
|
||||
|
||||
// ValidateInvoiceInfo 验证发票信息完整性
|
||||
func (s *InvoiceDomainServiceImpl) ValidateInvoiceInfo(ctx context.Context, info *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error {
|
||||
if info == nil {
|
||||
return errors.New("发票信息不能为空")
|
||||
}
|
||||
|
||||
switch invoiceType {
|
||||
case value_objects.InvoiceTypeGeneral:
|
||||
return info.ValidateForGeneralInvoice()
|
||||
case value_objects.InvoiceTypeSpecial:
|
||||
return info.ValidateForSpecialInvoice()
|
||||
default:
|
||||
return errors.New("无效的发票类型")
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateInvoiceAmount 验证开票金额是否合法(基于业务规则)
|
||||
func (s *InvoiceDomainServiceImpl) ValidateInvoiceAmount(ctx context.Context, amount decimal.Decimal, availableAmount decimal.Decimal) error {
|
||||
if amount.LessThanOrEqual(decimal.Zero) {
|
||||
return errors.New("开票金额必须大于0")
|
||||
}
|
||||
|
||||
if amount.GreaterThan(availableAmount) {
|
||||
return fmt.Errorf("开票金额不能超过可开票金额,可开票金额:%s", availableAmount.String())
|
||||
}
|
||||
|
||||
// 最小开票金额限制
|
||||
minAmount := decimal.NewFromFloat(0.01) // 最小0.01元
|
||||
if amount.LessThan(minAmount) {
|
||||
return fmt.Errorf("开票金额不能少于%s元", minAmount.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CalculateAvailableAmount 计算可开票金额(纯计算逻辑)
|
||||
func (s *InvoiceDomainServiceImpl) CalculateAvailableAmount(totalRecharged decimal.Decimal, totalGifted decimal.Decimal, totalInvoiced decimal.Decimal) decimal.Decimal {
|
||||
// 可开票金额 = 充值金额 - 已开票金额(不包含赠送金额)
|
||||
availableAmount := totalRecharged.Sub(totalInvoiced)
|
||||
if availableAmount.LessThan(decimal.Zero) {
|
||||
availableAmount = decimal.Zero
|
||||
}
|
||||
return availableAmount
|
||||
}
|
||||
|
||||
// ValidateStatusTransition 验证发票申请状态转换
|
||||
func (s *InvoiceDomainServiceImpl) ValidateStatusTransition(currentStatus entities.ApplicationStatus, targetStatus entities.ApplicationStatus) error {
|
||||
// 定义允许的状态转换
|
||||
allowedTransitions := map[entities.ApplicationStatus][]entities.ApplicationStatus{
|
||||
entities.ApplicationStatusPending: {
|
||||
entities.ApplicationStatusCompleted,
|
||||
entities.ApplicationStatusRejected,
|
||||
},
|
||||
entities.ApplicationStatusCompleted: {
|
||||
// 已完成状态不能再转换
|
||||
},
|
||||
entities.ApplicationStatusRejected: {
|
||||
// 已拒绝状态不能再转换
|
||||
},
|
||||
}
|
||||
|
||||
allowedTargets, exists := allowedTransitions[currentStatus]
|
||||
if !exists {
|
||||
return fmt.Errorf("无效的当前状态:%s", currentStatus)
|
||||
}
|
||||
|
||||
for _, allowed := range allowedTargets {
|
||||
if allowed == targetStatus {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("不允许从状态 %s 转换到状态 %s", currentStatus, targetStatus)
|
||||
}
|
||||
|
||||
// ValidateInvoiceApplication 验证发票申请业务规则
|
||||
func (s *InvoiceDomainServiceImpl) ValidateInvoiceApplication(ctx context.Context, application *entities.InvoiceApplication) error {
|
||||
if application == nil {
|
||||
return errors.New("发票申请不能为空")
|
||||
}
|
||||
|
||||
// 验证基础字段
|
||||
if application.UserID == "" {
|
||||
return errors.New("用户ID不能为空")
|
||||
}
|
||||
|
||||
if application.Amount.LessThanOrEqual(decimal.Zero) {
|
||||
return errors.New("申请金额必须大于0")
|
||||
}
|
||||
|
||||
// 验证发票类型
|
||||
if !application.InvoiceType.IsValid() {
|
||||
return errors.New("无效的发票类型")
|
||||
}
|
||||
|
||||
// 验证开票信息
|
||||
if application.CompanyName == "" {
|
||||
return errors.New("公司名称不能为空")
|
||||
}
|
||||
|
||||
if application.TaxpayerID == "" {
|
||||
return errors.New("纳税人识别号不能为空")
|
||||
}
|
||||
|
||||
if application.ReceivingEmail == "" {
|
||||
return errors.New("发票接收邮箱不能为空")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
426
internal/domains/finance/services/recharge_record_service.go
Normal file
426
internal/domains/finance/services/recharge_record_service.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"hyapi-server/internal/config"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/domains/finance/repositories"
|
||||
"hyapi-server/internal/shared/database"
|
||||
"hyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// calculateAlipayRechargeBonus 计算支付宝充值赠送金额(受 recharge_bonus_enabled 开关控制)
|
||||
func calculateAlipayRechargeBonus(rechargeAmount decimal.Decimal, walletConfig *config.WalletConfig) decimal.Decimal {
|
||||
if walletConfig == nil || !walletConfig.RechargeBonusEnabled || len(walletConfig.AliPayRechargeBonus) == 0 {
|
||||
return decimal.Zero
|
||||
}
|
||||
|
||||
// 按充值金额从高到低排序,找到第一个匹配的赠送规则
|
||||
// 由于配置中规则是按充值金额从低到高排列的,我们需要反向遍历
|
||||
for i := len(walletConfig.AliPayRechargeBonus) - 1; i >= 0; i-- {
|
||||
rule := walletConfig.AliPayRechargeBonus[i]
|
||||
if rechargeAmount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) {
|
||||
return decimal.NewFromFloat(rule.BonusAmount)
|
||||
}
|
||||
}
|
||||
|
||||
return decimal.Zero
|
||||
}
|
||||
|
||||
// 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
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewRechargeRecordService(
|
||||
rechargeRecordRepo repositories.RechargeRecordRepository,
|
||||
alipayOrderRepo repositories.AlipayOrderRepository,
|
||||
walletRepo repositories.WalletRepository,
|
||||
walletService WalletAggregateService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) RechargeRecordService {
|
||||
return &RechargeRecordServiceImpl{
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
walletRepo: walletRepo,
|
||||
walletService: walletService,
|
||||
txManager: txManager,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 检查是否是组件报告下载订单(通过备注判断)
|
||||
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
|
||||
|
||||
s.logger.Info("处理支付宝支付成功回调",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("notes", rechargeRecord.Notes),
|
||||
zap.Bool("is_component_report", isComponentReportOrder),
|
||||
)
|
||||
|
||||
// 计算充值赠送金额(组件报告下载订单不需要赠送)
|
||||
bonusAmount := decimal.Zero
|
||||
if !isComponentReportOrder {
|
||||
bonusAmount = calculateAlipayRechargeBonus(amount, &s.cfg.Wallet)
|
||||
}
|
||||
totalAmount := amount.Add(bonusAmount)
|
||||
|
||||
// 在事务中执行所有更新操作
|
||||
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
|
||||
}
|
||||
|
||||
// 更新充值记录状态为成功(使用UpdateStatus方法直接更新状态字段)
|
||||
err = s.rechargeRecordRepo.UpdateStatus(txCtx, rechargeRecord.ID, entities.RechargeStatusSuccess)
|
||||
if err != nil {
|
||||
s.logger.Error("更新充值记录状态失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是组件报告下载订单,不增加钱包余额,不创建赠送记录
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("组件报告下载订单,跳过钱包余额增加和赠送",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果有赠送金额,创建赠送充值记录
|
||||
if bonusAmount.GreaterThan(decimal.Zero) {
|
||||
giftRechargeRecord := entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送")
|
||||
_, err = s.rechargeRecordRepo.Create(txCtx, *giftRechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建赠送充值记录失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
s.logger.Info("创建赠送充值记录成功",
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("gift_recharge_id", giftRechargeRecord.ID))
|
||||
}
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额(包含赠送金额)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalAmount)
|
||||
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("recharge_amount", amount.String()),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("total_amount", totalAmount.String()),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", tradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.String("order_id", alipayOrder.ID))
|
||||
|
||||
// 检查是否有组件报告下载记录需要更新
|
||||
// 注意:这里需要在调用方(finance应用服务)中处理,因为这里没有组件报告下载的repository
|
||||
// 但为了保持服务层的独立性,我们通过事件或回调来处理
|
||||
|
||||
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) {
|
||||
// 将filters添加到options中
|
||||
if filters != nil {
|
||||
if options.Filters == nil {
|
||||
options.Filters = make(map[string]interface{})
|
||||
}
|
||||
for key, value := range filters {
|
||||
options.Filters[key] = value
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"hyapi-server/internal/config"
|
||||
)
|
||||
|
||||
func TestCalculateAlipayRechargeBonus(t *testing.T) {
|
||||
// 创建测试配置(开启赠送)
|
||||
walletConfig := &config.WalletConfig{
|
||||
RechargeBonusEnabled: true,
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{
|
||||
{RechargeAmount: 1000.00, BonusAmount: 50.00}, // 充1000送50
|
||||
{RechargeAmount: 5000.00, BonusAmount: 300.00}, // 充5000送300
|
||||
{RechargeAmount: 10000.00, BonusAmount: 800.00}, // 充10000送800
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rechargeAmount decimal.Decimal
|
||||
expectedBonus decimal.Decimal
|
||||
}{
|
||||
{
|
||||
name: "充值500元,无赠送",
|
||||
rechargeAmount: decimal.NewFromFloat(500.00),
|
||||
expectedBonus: decimal.Zero,
|
||||
},
|
||||
{
|
||||
name: "充值1000元,赠送50元",
|
||||
rechargeAmount: decimal.NewFromFloat(1000.00),
|
||||
expectedBonus: decimal.NewFromFloat(50.00),
|
||||
},
|
||||
{
|
||||
name: "充值2000元,赠送50元",
|
||||
rechargeAmount: decimal.NewFromFloat(2000.00),
|
||||
expectedBonus: decimal.NewFromFloat(50.00),
|
||||
},
|
||||
{
|
||||
name: "充值5000元,赠送300元",
|
||||
rechargeAmount: decimal.NewFromFloat(5000.00),
|
||||
expectedBonus: decimal.NewFromFloat(300.00),
|
||||
},
|
||||
{
|
||||
name: "充值8000元,赠送300元",
|
||||
rechargeAmount: decimal.NewFromFloat(8000.00),
|
||||
expectedBonus: decimal.NewFromFloat(300.00),
|
||||
},
|
||||
{
|
||||
name: "充值10000元,赠送800元",
|
||||
rechargeAmount: decimal.NewFromFloat(10000.00),
|
||||
expectedBonus: decimal.NewFromFloat(800.00),
|
||||
},
|
||||
{
|
||||
name: "充值15000元,赠送800元",
|
||||
rechargeAmount: decimal.NewFromFloat(15000.00),
|
||||
expectedBonus: decimal.NewFromFloat(800.00),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bonus := calculateAlipayRechargeBonus(tt.rechargeAmount, walletConfig)
|
||||
assert.True(t, bonus.Equal(tt.expectedBonus),
|
||||
"充值金额: %s, 期望赠送: %s, 实际赠送: %s",
|
||||
tt.rechargeAmount.String(), tt.expectedBonus.String(), bonus.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAlipayRechargeBonus_EmptyConfig(t *testing.T) {
|
||||
// 测试空配置
|
||||
walletConfig := &config.WalletConfig{
|
||||
RechargeBonusEnabled: true,
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{},
|
||||
}
|
||||
|
||||
bonus := calculateAlipayRechargeBonus(decimal.NewFromFloat(1000.00), walletConfig)
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "空配置应该返回零赠送金额")
|
||||
|
||||
// 测试nil配置
|
||||
bonus = calculateAlipayRechargeBonus(decimal.NewFromFloat(1000.00), nil)
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "nil配置应该返回零赠送金额")
|
||||
}
|
||||
|
||||
func TestCalculateAlipayRechargeBonus_Disabled(t *testing.T) {
|
||||
// 关闭赠送时,任意金额均不赠送
|
||||
walletConfig := &config.WalletConfig{
|
||||
RechargeBonusEnabled: false,
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{
|
||||
{RechargeAmount: 1000.00, BonusAmount: 50.00},
|
||||
{RechargeAmount: 10000.00, BonusAmount: 800.00},
|
||||
},
|
||||
}
|
||||
bonus := calculateAlipayRechargeBonus(decimal.NewFromFloat(10000.00), walletConfig)
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "关闭赠送时应返回零")
|
||||
}
|
||||
250
internal/domains/finance/services/user_invoice_info_service.go
Normal file
250
internal/domains/finance/services/user_invoice_info_service.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-server/internal/domains/finance/repositories"
|
||||
"hyapi-server/internal/domains/finance/value_objects"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserInvoiceInfoService 用户开票信息服务接口
|
||||
type UserInvoiceInfoService interface {
|
||||
// GetUserInvoiceInfo 获取用户开票信息
|
||||
GetUserInvoiceInfo(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error)
|
||||
|
||||
// GetUserInvoiceInfoWithEnterpriseInfo 获取用户开票信息(包含企业认证信息)
|
||||
GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
|
||||
|
||||
// CreateOrUpdateUserInvoiceInfo 创建或更新用户开票信息
|
||||
CreateOrUpdateUserInvoiceInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo) (*entities.UserInvoiceInfo, error)
|
||||
|
||||
// CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo 创建或更新用户开票信息(包含企业认证信息)
|
||||
CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
|
||||
|
||||
// ValidateInvoiceInfo 验证开票信息
|
||||
ValidateInvoiceInfo(ctx context.Context, invoiceInfo *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error
|
||||
|
||||
// DeleteUserInvoiceInfo 删除用户开票信息
|
||||
DeleteUserInvoiceInfo(ctx context.Context, userID string) error
|
||||
}
|
||||
|
||||
// UserInvoiceInfoServiceImpl 用户开票信息服务实现
|
||||
type UserInvoiceInfoServiceImpl struct {
|
||||
userInvoiceInfoRepo repositories.UserInvoiceInfoRepository
|
||||
}
|
||||
|
||||
// NewUserInvoiceInfoService 创建用户开票信息服务
|
||||
func NewUserInvoiceInfoService(userInvoiceInfoRepo repositories.UserInvoiceInfoRepository) UserInvoiceInfoService {
|
||||
return &UserInvoiceInfoServiceImpl{
|
||||
userInvoiceInfoRepo: userInvoiceInfoRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserInvoiceInfo 获取用户开票信息
|
||||
func (s *UserInvoiceInfoServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error) {
|
||||
info, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有找到开票信息记录,创建新的实体
|
||||
if info == nil {
|
||||
info = &entities.UserInvoiceInfo{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
CompanyName: "",
|
||||
TaxpayerID: "",
|
||||
BankName: "",
|
||||
BankAccount: "",
|
||||
CompanyAddress: "",
|
||||
CompanyPhone: "",
|
||||
ReceivingEmail: "",
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// GetUserInvoiceInfoWithEnterpriseInfo 获取用户开票信息(包含企业认证信息)
|
||||
func (s *UserInvoiceInfoServiceImpl) GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error) {
|
||||
info, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有找到开票信息记录,创建新的实体
|
||||
if info == nil {
|
||||
info = &entities.UserInvoiceInfo{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
CompanyName: companyName, // 使用企业认证信息填充
|
||||
TaxpayerID: taxpayerID, // 使用企业认证信息填充
|
||||
BankName: "",
|
||||
BankAccount: "",
|
||||
CompanyAddress: "",
|
||||
CompanyPhone: "",
|
||||
ReceivingEmail: "",
|
||||
}
|
||||
} else {
|
||||
// 如果已有记录,使用传入的企业认证信息覆盖公司名称和纳税人识别号
|
||||
if companyName != "" {
|
||||
info.CompanyName = companyName
|
||||
}
|
||||
if taxpayerID != "" {
|
||||
info.TaxpayerID = taxpayerID
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateUserInvoiceInfo 创建或更新用户开票信息
|
||||
func (s *UserInvoiceInfoServiceImpl) CreateOrUpdateUserInvoiceInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo) (*entities.UserInvoiceInfo, error) {
|
||||
// 验证开票信息
|
||||
if err := s.ValidateInvoiceInfo(ctx, invoiceInfo, value_objects.InvoiceTypeGeneral); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
exists, err := s.userInvoiceInfoRepo.Exists(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
var userInvoiceInfo *entities.UserInvoiceInfo
|
||||
|
||||
if exists {
|
||||
// 更新现有记录
|
||||
userInvoiceInfo, err = s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
userInvoiceInfo.CompanyName = invoiceInfo.CompanyName
|
||||
userInvoiceInfo.TaxpayerID = invoiceInfo.TaxpayerID
|
||||
userInvoiceInfo.BankName = invoiceInfo.BankName
|
||||
userInvoiceInfo.BankAccount = invoiceInfo.BankAccount
|
||||
userInvoiceInfo.CompanyAddress = invoiceInfo.CompanyAddress
|
||||
userInvoiceInfo.CompanyPhone = invoiceInfo.CompanyPhone
|
||||
userInvoiceInfo.ReceivingEmail = invoiceInfo.ReceivingEmail
|
||||
|
||||
err = s.userInvoiceInfoRepo.Update(ctx, userInvoiceInfo)
|
||||
} else {
|
||||
// 创建新记录
|
||||
userInvoiceInfo = &entities.UserInvoiceInfo{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
CompanyName: invoiceInfo.CompanyName,
|
||||
TaxpayerID: invoiceInfo.TaxpayerID,
|
||||
BankName: invoiceInfo.BankName,
|
||||
BankAccount: invoiceInfo.BankAccount,
|
||||
CompanyAddress: invoiceInfo.CompanyAddress,
|
||||
CompanyPhone: invoiceInfo.CompanyPhone,
|
||||
ReceivingEmail: invoiceInfo.ReceivingEmail,
|
||||
}
|
||||
|
||||
err = s.userInvoiceInfoRepo.Create(ctx, userInvoiceInfo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
return userInvoiceInfo, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo 创建或更新用户开票信息(包含企业认证信息)
|
||||
func (s *UserInvoiceInfoServiceImpl) CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error) {
|
||||
// 检查企业认证信息
|
||||
if companyName == "" || taxpayerID == "" {
|
||||
return nil, fmt.Errorf("用户未完成企业认证,无法创建开票信息")
|
||||
}
|
||||
|
||||
// 创建新的开票信息对象,使用传入的企业认证信息
|
||||
updatedInvoiceInfo := &value_objects.InvoiceInfo{
|
||||
CompanyName: companyName, // 从企业认证信息获取
|
||||
TaxpayerID: taxpayerID, // 从企业认证信息获取
|
||||
BankName: invoiceInfo.BankName, // 用户输入
|
||||
BankAccount: invoiceInfo.BankAccount, // 用户输入
|
||||
CompanyAddress: invoiceInfo.CompanyAddress, // 用户输入
|
||||
CompanyPhone: invoiceInfo.CompanyPhone, // 用户输入
|
||||
ReceivingEmail: invoiceInfo.ReceivingEmail, // 用户输入
|
||||
}
|
||||
|
||||
// 验证开票信息
|
||||
if err := s.ValidateInvoiceInfo(ctx, updatedInvoiceInfo, value_objects.InvoiceTypeGeneral); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
exists, err := s.userInvoiceInfoRepo.Exists(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
var userInvoiceInfo *entities.UserInvoiceInfo
|
||||
|
||||
if exists {
|
||||
// 更新现有记录
|
||||
userInvoiceInfo, err = s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新字段(公司名称和纳税人识别号从企业认证信息获取,其他字段从用户输入获取)
|
||||
userInvoiceInfo.CompanyName = companyName
|
||||
userInvoiceInfo.TaxpayerID = taxpayerID
|
||||
userInvoiceInfo.BankName = invoiceInfo.BankName
|
||||
userInvoiceInfo.BankAccount = invoiceInfo.BankAccount
|
||||
userInvoiceInfo.CompanyAddress = invoiceInfo.CompanyAddress
|
||||
userInvoiceInfo.CompanyPhone = invoiceInfo.CompanyPhone
|
||||
userInvoiceInfo.ReceivingEmail = invoiceInfo.ReceivingEmail
|
||||
|
||||
err = s.userInvoiceInfoRepo.Update(ctx, userInvoiceInfo)
|
||||
} else {
|
||||
// 创建新记录
|
||||
userInvoiceInfo = &entities.UserInvoiceInfo{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
CompanyName: companyName, // 从企业认证信息获取
|
||||
TaxpayerID: taxpayerID, // 从企业认证信息获取
|
||||
BankName: invoiceInfo.BankName, // 用户输入
|
||||
BankAccount: invoiceInfo.BankAccount, // 用户输入
|
||||
CompanyAddress: invoiceInfo.CompanyAddress, // 用户输入
|
||||
CompanyPhone: invoiceInfo.CompanyPhone, // 用户输入
|
||||
ReceivingEmail: invoiceInfo.ReceivingEmail, // 用户输入
|
||||
}
|
||||
|
||||
err = s.userInvoiceInfoRepo.Create(ctx, userInvoiceInfo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存用户开票信息失败: %w", err)
|
||||
}
|
||||
|
||||
return userInvoiceInfo, nil
|
||||
}
|
||||
|
||||
// ValidateInvoiceInfo 验证开票信息
|
||||
func (s *UserInvoiceInfoServiceImpl) ValidateInvoiceInfo(ctx context.Context, invoiceInfo *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error {
|
||||
if invoiceType == value_objects.InvoiceTypeGeneral {
|
||||
return invoiceInfo.ValidateForGeneralInvoice()
|
||||
} else if invoiceType == value_objects.InvoiceTypeSpecial {
|
||||
return invoiceInfo.ValidateForSpecialInvoice()
|
||||
}
|
||||
|
||||
return fmt.Errorf("无效的发票类型: %s", invoiceType)
|
||||
}
|
||||
|
||||
// DeleteUserInvoiceInfo 删除用户开票信息
|
||||
func (s *UserInvoiceInfoServiceImpl) DeleteUserInvoiceInfo(ctx context.Context, userID string) error {
|
||||
err := s.userInvoiceInfoRepo.Delete(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除用户开票信息失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
155
internal/domains/finance/services/wallet_aggregate_service.go
Normal file
155
internal/domains/finance/services/wallet_aggregate_service.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"hyapi-server/internal/config"
|
||||
"hyapi-server/internal/domains/finance/entities"
|
||||
"hyapi-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 {
|
||||
db *gorm.DB
|
||||
walletRepo repositories.WalletRepository
|
||||
transactionRepo repositories.WalletTransactionRepository
|
||||
balanceAlertSvc BalanceAlertService
|
||||
logger *zap.Logger
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewWalletAggregateService(
|
||||
db *gorm.DB,
|
||||
walletRepo repositories.WalletRepository,
|
||||
transactionRepo repositories.WalletTransactionRepository,
|
||||
balanceAlertSvc BalanceAlertService,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) WalletAggregateService {
|
||||
return &WalletAggregateServiceImpl{
|
||||
db: db,
|
||||
walletRepo: walletRepo,
|
||||
transactionRepo: transactionRepo,
|
||||
balanceAlertSvc: balanceAlertSvc,
|
||||
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 {
|
||||
// 使用数据库事务确保一致性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(ctx, userID, amount, "add")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
}
|
||||
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Deduct 扣款,含欠费规则 - 使用事务确保一致性
|
||||
func (s *WalletAggregateServiceImpl) Deduct(ctx context.Context, userID string, amount decimal.Decimal, apiCallID, transactionID, productID string) error {
|
||||
// 使用数据库事务确保一致性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 使用乐观锁更新余额(通过用户ID直接更新,避免重复查询)
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(ctx, userID, amount, "subtract")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
|
||||
// 2. 创建扣款记录(检查是否已存在)
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
|
||||
if err := tx.Create(transaction).Error; err != nil {
|
||||
return fmt.Errorf("创建扣款记录失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("钱包扣款成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("api_call_id", apiCallID),
|
||||
zap.String("transaction_id", transactionID))
|
||||
|
||||
// 3. 扣费成功后异步检查余额预警
|
||||
go s.checkBalanceAlertAsync(context.Background(), userID)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// checkBalanceAlertAsync 异步检查余额预警
|
||||
func (s *WalletAggregateServiceImpl) checkBalanceAlertAsync(ctx context.Context, userID string) {
|
||||
// 获取最新余额
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取钱包余额失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查并发送预警
|
||||
if err := s.balanceAlertSvc.CheckAndSendAlert(ctx, userID, wallet.Balance); err != nil {
|
||||
s.logger.Error("余额预警检查失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
105
internal/domains/finance/value_objects/invoice_info.go
Normal file
105
internal/domains/finance/value_objects/invoice_info.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package value_objects
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InvoiceInfo 发票信息值对象
|
||||
type InvoiceInfo struct {
|
||||
CompanyName string `json:"company_name"` // 公司名称
|
||||
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号
|
||||
BankName string `json:"bank_name"` // 基本开户银行
|
||||
BankAccount string `json:"bank_account"` // 基本开户账号
|
||||
CompanyAddress string `json:"company_address"` // 企业注册地址
|
||||
CompanyPhone string `json:"company_phone"` // 企业注册电话
|
||||
ReceivingEmail string `json:"receiving_email"` // 发票接收邮箱
|
||||
}
|
||||
|
||||
// NewInvoiceInfo 创建发票信息值对象
|
||||
func NewInvoiceInfo(companyName, taxpayerID, bankName, bankAccount, companyAddress, companyPhone, receivingEmail string) *InvoiceInfo {
|
||||
return &InvoiceInfo{
|
||||
CompanyName: strings.TrimSpace(companyName),
|
||||
TaxpayerID: strings.TrimSpace(taxpayerID),
|
||||
BankName: strings.TrimSpace(bankName),
|
||||
BankAccount: strings.TrimSpace(bankAccount),
|
||||
CompanyAddress: strings.TrimSpace(companyAddress),
|
||||
CompanyPhone: strings.TrimSpace(companyPhone),
|
||||
ReceivingEmail: strings.TrimSpace(receivingEmail),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateForGeneralInvoice 验证普票信息
|
||||
func (ii *InvoiceInfo) ValidateForGeneralInvoice() error {
|
||||
if ii.CompanyName == "" {
|
||||
return errors.New("公司名称不能为空")
|
||||
}
|
||||
if ii.TaxpayerID == "" {
|
||||
return errors.New("纳税人识别号不能为空")
|
||||
}
|
||||
if ii.ReceivingEmail == "" {
|
||||
return errors.New("发票接收邮箱不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateForSpecialInvoice 验证专票信息
|
||||
func (ii *InvoiceInfo) ValidateForSpecialInvoice() error {
|
||||
// 先验证普票必填项
|
||||
if err := ii.ValidateForGeneralInvoice(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 专票额外必填项
|
||||
if ii.BankName == "" {
|
||||
return errors.New("基本开户银行不能为空")
|
||||
}
|
||||
if ii.BankAccount == "" {
|
||||
return errors.New("基本开户账号不能为空")
|
||||
}
|
||||
if ii.CompanyAddress == "" {
|
||||
return errors.New("企业注册地址不能为空")
|
||||
}
|
||||
if ii.CompanyPhone == "" {
|
||||
return errors.New("企业注册电话不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsComplete 检查信息是否完整(专票要求)
|
||||
func (ii *InvoiceInfo) IsComplete() bool {
|
||||
return ii.CompanyName != "" &&
|
||||
ii.TaxpayerID != "" &&
|
||||
ii.BankName != "" &&
|
||||
ii.BankAccount != "" &&
|
||||
ii.CompanyAddress != "" &&
|
||||
ii.CompanyPhone != "" &&
|
||||
ii.ReceivingEmail != ""
|
||||
}
|
||||
|
||||
// GetMissingFields 获取缺失的字段(专票要求)
|
||||
func (ii *InvoiceInfo) GetMissingFields() []string {
|
||||
var missing []string
|
||||
if ii.CompanyName == "" {
|
||||
missing = append(missing, "公司名称")
|
||||
}
|
||||
if ii.TaxpayerID == "" {
|
||||
missing = append(missing, "纳税人识别号")
|
||||
}
|
||||
if ii.BankName == "" {
|
||||
missing = append(missing, "基本开户银行")
|
||||
}
|
||||
if ii.BankAccount == "" {
|
||||
missing = append(missing, "基本开户账号")
|
||||
}
|
||||
if ii.CompanyAddress == "" {
|
||||
missing = append(missing, "企业注册地址")
|
||||
}
|
||||
if ii.CompanyPhone == "" {
|
||||
missing = append(missing, "企业注册电话")
|
||||
}
|
||||
if ii.ReceivingEmail == "" {
|
||||
missing = append(missing, "发票接收邮箱")
|
||||
}
|
||||
return missing
|
||||
}
|
||||
36
internal/domains/finance/value_objects/invoice_type.go
Normal file
36
internal/domains/finance/value_objects/invoice_type.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package value_objects
|
||||
|
||||
// InvoiceType 发票类型枚举
|
||||
type InvoiceType string
|
||||
|
||||
const (
|
||||
InvoiceTypeGeneral InvoiceType = "general" // 增值税普通发票 (普票)
|
||||
InvoiceTypeSpecial InvoiceType = "special" // 增值税专用发票 (专票)
|
||||
)
|
||||
|
||||
// String 返回发票类型的字符串表示
|
||||
func (it InvoiceType) String() string {
|
||||
return string(it)
|
||||
}
|
||||
|
||||
// IsValid 验证发票类型是否有效
|
||||
func (it InvoiceType) IsValid() bool {
|
||||
switch it {
|
||||
case InvoiceTypeGeneral, InvoiceTypeSpecial:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetDisplayName 获取发票类型的显示名称
|
||||
func (it InvoiceType) GetDisplayName() string {
|
||||
switch it {
|
||||
case InvoiceTypeGeneral:
|
||||
return "增值税普通发票 (普票)"
|
||||
case InvoiceTypeSpecial:
|
||||
return "增值税专用发票 (专票)"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user