This commit is contained in:
2026-04-21 22:36:48 +08:00
commit 488c695fdf
748 changed files with 266838 additions and 0 deletions

View 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")
}

View 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,
)
}

View 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,
}
}

View 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,
}
}

View 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,
}
}

View 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
}

View 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,
}
}

View 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,
}
}

View 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)
}

View 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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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
}

View 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
}

View 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
}

View 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)
}

View File

@@ -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), "关闭赠送时应返回零")
}

View 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
}

View 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))
}
}

View 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
}

View 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 "未知类型"
}
}