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