This commit is contained in:
2025-08-02 02:54:21 +08:00
parent 934dce2776
commit 66845d3fe0
74 changed files with 8686 additions and 212 deletions

View File

@@ -222,7 +222,8 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&financeEntities.WalletTransaction{},
&financeEntities.RechargeRecord{},
&financeEntities.AlipayOrder{},
&financeEntities.InvoiceApplication{},
&financeEntities.UserInvoiceInfo{},
// 产品域
&productEntities.Product{},
&productEntities.ProductPackageItem{},

View File

@@ -13,6 +13,7 @@ import (
"tyapi-server/internal/domains/api/services/processors"
finance_services "tyapi-server/internal/domains/finance/services"
product_services "tyapi-server/internal/domains/product/services"
user_repositories "tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/shared/crypto"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/interfaces"
@@ -33,6 +34,9 @@ type ApiApplicationService interface {
// 获取用户API调用记录
GetUserApiCalls(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error)
// 管理端API调用记录
GetAdminApiCalls(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error)
}
type ApiApplicationServiceImpl struct {
@@ -43,13 +47,14 @@ type ApiApplicationServiceImpl struct {
walletService finance_services.WalletAggregateService
productManagementService *product_services.ProductManagementService
productSubscriptionService *product_services.ProductSubscriptionService
userRepo user_repositories.UserRepository
txManager *database.TransactionManager
config *config.Config
logger *zap.Logger
}
func NewApiApplicationService(apiCallService services.ApiCallAggregateService, apiUserService services.ApiUserAggregateService, apiRequestService *services.ApiRequestService, apiCallRepository repositories.ApiCallRepository, walletService finance_services.WalletAggregateService, productManagementService *product_services.ProductManagementService, productSubscriptionService *product_services.ProductSubscriptionService, txManager *database.TransactionManager, config *config.Config, logger *zap.Logger) ApiApplicationService {
return &ApiApplicationServiceImpl{apiCallService: apiCallService, apiUserService: apiUserService, apiRequestService: apiRequestService, apiCallRepository: apiCallRepository, walletService: walletService, productManagementService: productManagementService, productSubscriptionService: productSubscriptionService, txManager: txManager, config: config, logger: logger}
func NewApiApplicationService(apiCallService services.ApiCallAggregateService, apiUserService services.ApiUserAggregateService, apiRequestService *services.ApiRequestService, apiCallRepository repositories.ApiCallRepository, walletService finance_services.WalletAggregateService, productManagementService *product_services.ProductManagementService, productSubscriptionService *product_services.ProductSubscriptionService, userRepo user_repositories.UserRepository, txManager *database.TransactionManager, config *config.Config, logger *zap.Logger) ApiApplicationService {
return &ApiApplicationServiceImpl{apiCallService: apiCallService, apiUserService: apiUserService, apiRequestService: apiRequestService, apiCallRepository: apiCallRepository, walletService: walletService, productManagementService: productManagementService, productSubscriptionService: productSubscriptionService, userRepo: userRepo, txManager: txManager, config: config, logger: logger}
}
// CallApi 应用服务层统一入口
@@ -405,3 +410,80 @@ func (s *ApiApplicationServiceImpl) GetUserApiCalls(ctx context.Context, userID
Size: options.PageSize,
}, nil
}
// GetAdminApiCalls 获取管理端API调用记录
func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*dto.ApiCallListResponse, error) {
// 查询API调用记录包含产品名称
productNameMap, calls, total, err := s.apiCallRepository.ListWithFiltersAndProductName(ctx, filters, options)
if err != nil {
s.logger.Error("查询API调用记录失败", zap.Error(err))
return nil, err
}
// 转换为响应DTO
var items []dto.ApiCallRecordResponse
for _, call := range calls {
item := dto.ApiCallRecordResponse{
ID: call.ID,
AccessId: call.AccessId,
UserId: *call.UserId,
TransactionId: call.TransactionId,
ClientIp: call.ClientIp,
Status: call.Status,
StartAt: call.StartAt.Format("2006-01-02 15:04:05"),
CreatedAt: call.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: call.UpdatedAt.Format("2006-01-02 15:04:05"),
}
// 处理可选字段
if call.ProductId != nil {
item.ProductId = call.ProductId
}
// 从映射中获取产品名称
if productName, exists := productNameMap[call.ID]; exists {
item.ProductName = &productName
}
if call.EndAt != nil {
endAt := call.EndAt.Format("2006-01-02 15:04:05")
item.EndAt = &endAt
}
if call.Cost != nil {
cost := call.Cost.String()
item.Cost = &cost
}
if call.ErrorType != nil {
item.ErrorType = call.ErrorType
}
if call.ErrorMsg != nil {
item.ErrorMsg = call.ErrorMsg
// 添加翻译后的错误信息
item.TranslatedErrorMsg = utils.TranslateErrorMsg(call.ErrorType, call.ErrorMsg)
}
// 获取用户信息和企业名称
if call.UserId != nil {
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, *call.UserId)
if err == nil {
companyName := "未知企业"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
item.CompanyName = &companyName
item.User = &dto.UserSimpleResponse{
ID: user.ID,
CompanyName: companyName,
Phone: user.Phone,
}
}
}
items = append(items, item)
}
return &dto.ApiCallListResponse{
Items: items,
Total: total,
Page: options.Page,
Size: options.PageSize,
}, nil
}

View File

@@ -54,10 +54,19 @@ type ApiCallRecordResponse struct {
ErrorType *string `json:"error_type,omitempty"`
ErrorMsg *string `json:"error_msg,omitempty"`
TranslatedErrorMsg *string `json:"translated_error_msg,omitempty"`
CompanyName *string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// UserSimpleResponse 用户简单信息响应
type UserSimpleResponse struct {
ID string `json:"id"`
CompanyName string `json:"company_name"`
Phone string `json:"phone"`
}
type ApiCallListResponse struct {
Items []ApiCallRecordResponse `json:"items"`
Total int64 `json:"total"`

View File

@@ -0,0 +1,121 @@
package dto
import (
"time"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/value_objects"
"github.com/shopspring/decimal"
)
// InvoiceApplicationResponse 发票申请响应
type InvoiceApplicationResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
Amount decimal.Decimal `json:"amount"`
Status entities.ApplicationStatus `json:"status"`
InvoiceInfo *value_objects.InvoiceInfo `json:"invoice_info"`
CreatedAt time.Time `json:"created_at"`
}
// InvoiceInfoResponse 发票信息响应
type InvoiceInfoResponse 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"` // 用户可编辑
IsComplete bool `json:"is_complete"`
MissingFields []string `json:"missing_fields,omitempty"`
// 字段权限标识
CompanyNameReadOnly bool `json:"company_name_read_only"` // 公司名称是否只读
TaxpayerIDReadOnly bool `json:"taxpayer_id_read_only"` // 纳税人识别号是否只读
}
// InvoiceRecordResponse 发票记录响应
type InvoiceRecordResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
Amount decimal.Decimal `json:"amount"`
Status entities.ApplicationStatus `json:"status"`
// 开票信息(快照数据)
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"` // 接收邮箱
// 文件信息
FileName *string `json:"file_name,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
FileURL *string `json:"file_url,omitempty"`
// 时间信息
ProcessedAt *time.Time `json:"processed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
// 拒绝原因
RejectReason *string `json:"reject_reason,omitempty"`
}
// InvoiceRecordsResponse 发票记录列表响应
type InvoiceRecordsResponse struct {
Records []*InvoiceRecordResponse `json:"records"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// FileDownloadResponse 文件下载响应
type FileDownloadResponse struct {
FileID string `json:"file_id"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
FileURL string `json:"file_url"`
FileContent []byte `json:"file_content"`
}
// AvailableAmountResponse 可开票金额响应
type AvailableAmountResponse struct {
AvailableAmount decimal.Decimal `json:"available_amount"` // 可开票金额
TotalRecharged decimal.Decimal `json:"total_recharged"` // 总充值金额
TotalGifted decimal.Decimal `json:"total_gifted"` // 总赠送金额
TotalInvoiced decimal.Decimal `json:"total_invoiced"` // 已开票金额
PendingApplications decimal.Decimal `json:"pending_applications"` // 待处理申请金额
}
// PendingApplicationResponse 待处理申请响应
type PendingApplicationResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
InvoiceType value_objects.InvoiceType `json:"invoice_type"`
Amount decimal.Decimal `json:"amount"`
Status entities.ApplicationStatus `json:"status"`
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"`
FileName *string `json:"file_name,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
FileURL *string `json:"file_url,omitempty"`
ProcessedAt *time.Time `json:"processed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
RejectReason *string `json:"reject_reason,omitempty"`
}
// PendingApplicationsResponse 待处理申请列表响应
type PendingApplicationsResponse struct {
Applications []*PendingApplicationResponse `json:"applications"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}

View File

@@ -58,6 +58,8 @@ type RechargeRecordResponse struct {
TransferOrderID string `json:"transfer_order_id,omitempty"`
Notes string `json:"notes,omitempty"`
OperatorID string `json:"operator_id,omitempty"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -71,6 +73,8 @@ type WalletTransactionResponse struct {
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
Amount decimal.Decimal `json:"amount"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -112,3 +116,10 @@ type AlipayRechargeBonusRuleResponse struct {
RechargeAmount float64 `json:"recharge_amount"`
BonusAmount float64 `json:"bonus_amount"`
}
// UserSimpleResponse 用户简单信息响应
type UserSimpleResponse struct {
ID string `json:"id"`
CompanyName string `json:"company_name"`
Phone string `json:"phone"`
}

View File

@@ -26,6 +26,9 @@ type FinanceApplicationService interface {
// 获取用户钱包交易记录
GetUserWalletTransactions(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 管理端消费记录
GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error)
// 获取用户充值记录
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)

View File

@@ -11,6 +11,7 @@ import (
finance_entities "tyapi-server/internal/domains/finance/entities"
finance_repositories "tyapi-server/internal/domains/finance/repositories"
finance_services "tyapi-server/internal/domains/finance/services"
user_repositories "tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/payment"
@@ -27,6 +28,7 @@ type FinanceApplicationServiceImpl struct {
rechargeRecordService finance_services.RechargeRecordService
walletTransactionRepository finance_repositories.WalletTransactionRepository
alipayOrderRepo finance_repositories.AlipayOrderRepository
userRepo user_repositories.UserRepository
txManager *database.TransactionManager
logger *zap.Logger
config *config.Config
@@ -39,6 +41,7 @@ func NewFinanceApplicationService(
rechargeRecordService finance_services.RechargeRecordService,
walletTransactionRepository finance_repositories.WalletTransactionRepository,
alipayOrderRepo finance_repositories.AlipayOrderRepository,
userRepo user_repositories.UserRepository,
txManager *database.TransactionManager,
logger *zap.Logger,
config *config.Config,
@@ -49,6 +52,7 @@ func NewFinanceApplicationService(
rechargeRecordService: rechargeRecordService,
walletTransactionRepository: walletTransactionRepository,
alipayOrderRepo: alipayOrderRepo,
userRepo: userRepo,
txManager: txManager,
logger: logger,
config: config,
@@ -290,6 +294,55 @@ func (s *FinanceApplicationServiceImpl) GetUserWalletTransactions(ctx context.Co
}, nil
}
// GetAdminWalletTransactions 获取管理端钱包交易记录
func (s *FinanceApplicationServiceImpl) GetAdminWalletTransactions(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.WalletTransactionListResponse, error) {
// 查询钱包交易记录(包含产品名称)
productNameMap, transactions, total, err := s.walletTransactionRepository.ListWithFiltersAndProductName(ctx, filters, options)
if err != nil {
s.logger.Error("查询管理端钱包交易记录失败", zap.Error(err))
return nil, err
}
// 转换为响应DTO
var items []responses.WalletTransactionResponse
for _, transaction := range transactions {
item := responses.WalletTransactionResponse{
ID: transaction.ID,
UserID: transaction.UserID,
ApiCallID: transaction.ApiCallID,
TransactionID: transaction.TransactionID,
ProductID: transaction.ProductID,
ProductName: productNameMap[transaction.ProductID], // 从映射中获取产品名称
Amount: transaction.Amount,
CreatedAt: transaction.CreatedAt,
UpdatedAt: transaction.UpdatedAt,
}
// 获取用户信息和企业名称
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, transaction.UserID)
if err == nil {
companyName := "未知企业"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
item.CompanyName = companyName
item.User = &responses.UserSimpleResponse{
ID: user.ID,
CompanyName: companyName,
Phone: user.Phone,
}
}
items = append(items, item)
}
return &responses.WalletTransactionListResponse{
Items: items,
Total: total,
Page: options.Page,
Size: options.PageSize,
}, nil
}
// HandleAlipayCallback 处理支付宝回调
@@ -592,19 +645,19 @@ func (s *FinanceApplicationServiceImpl) GetUserRechargeRecords(ctx context.Conte
}, nil
}
// GetAdminRechargeRecords 管理员获取充值记录
// GetAdminRechargeRecords 获取管理端充值记录
func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) {
// 查询所有充值记录(管理员可以查看所有用户的充值记录)
// 查询充值记录
records, err := s.rechargeRecordService.GetAll(ctx, filters, options)
if err != nil {
s.logger.Error("查询管理充值记录失败", zap.Error(err))
s.logger.Error("查询管理充值记录失败", zap.Error(err))
return nil, err
}
// 获取总数
total, err := s.rechargeRecordService.Count(ctx, filters)
if err != nil {
s.logger.Error("统计管理充值记录失败", zap.Error(err))
s.logger.Error("统计管理充值记录失败", zap.Error(err))
return nil, err
}
@@ -612,14 +665,14 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
var items []responses.RechargeRecordResponse
for _, record := range records {
item := responses.RechargeRecordResponse{
ID: record.ID,
UserID: record.UserID,
Amount: record.Amount,
RechargeType: string(record.RechargeType),
Status: string(record.Status),
Notes: record.Notes,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
ID: record.ID,
UserID: record.UserID,
Amount: record.Amount,
RechargeType: string(record.RechargeType),
Status: string(record.Status),
Notes: record.Notes,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
}
// 根据充值类型设置相应的订单号
@@ -630,6 +683,21 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
item.TransferOrderID = *record.TransferOrderID
}
// 获取用户信息和企业名称
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, record.UserID)
if err == nil {
companyName := "未知企业"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
item.CompanyName = companyName
item.User = &responses.UserSimpleResponse{
ID: user.ID,
CompanyName: companyName,
Phone: user.Phone,
}
}
items = append(items, item)
}

View File

@@ -0,0 +1,750 @@
package finance
import (
"context"
"fmt"
"mime/multipart"
"time"
"tyapi-server/internal/application/finance/dto"
"tyapi-server/internal/domains/finance/entities"
finance_repo "tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/finance/services"
"tyapi-server/internal/domains/finance/value_objects"
user_repo "tyapi-server/internal/domains/user/repositories"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/external/storage"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
// ==================== 用户端发票应用服务 ====================
// InvoiceApplicationService 发票应用服务接口
// 职责:跨域协调、数据聚合、事务管理、外部服务调用
type InvoiceApplicationService interface {
// ApplyInvoice 申请开票
ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error)
// GetUserInvoiceInfo 获取用户发票信息
GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error)
// UpdateUserInvoiceInfo 更新用户发票信息
UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error
// GetUserInvoiceRecords 获取用户开票记录
GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error)
// DownloadInvoiceFile 下载发票文件
DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error)
// GetAvailableAmount 获取可开票金额
GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error)
}
// InvoiceApplicationServiceImpl 发票应用服务实现
type InvoiceApplicationServiceImpl struct {
// 仓储层依赖
invoiceRepo finance_repo.InvoiceApplicationRepository
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
userRepo user_repo.UserRepository
rechargeRecordRepo finance_repo.RechargeRecordRepository
walletRepo finance_repo.WalletRepository
// 领域服务依赖
invoiceDomainService services.InvoiceDomainService
invoiceAggregateService services.InvoiceAggregateService
userInvoiceInfoService services.UserInvoiceInfoService
userAggregateService user_service.UserAggregateService
// 外部服务依赖
storageService *storage.QiNiuStorageService
logger *zap.Logger
}
// NewInvoiceApplicationService 创建发票应用服务
func NewInvoiceApplicationService(
invoiceRepo finance_repo.InvoiceApplicationRepository,
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
userRepo user_repo.UserRepository,
userAggregateService user_service.UserAggregateService,
rechargeRecordRepo finance_repo.RechargeRecordRepository,
walletRepo finance_repo.WalletRepository,
invoiceDomainService services.InvoiceDomainService,
invoiceAggregateService services.InvoiceAggregateService,
userInvoiceInfoService services.UserInvoiceInfoService,
storageService *storage.QiNiuStorageService,
logger *zap.Logger,
) InvoiceApplicationService {
return &InvoiceApplicationServiceImpl{
invoiceRepo: invoiceRepo,
userInvoiceInfoRepo: userInvoiceInfoRepo,
userRepo: userRepo,
userAggregateService: userAggregateService,
rechargeRecordRepo: rechargeRecordRepo,
walletRepo: walletRepo,
invoiceDomainService: invoiceDomainService,
invoiceAggregateService: invoiceAggregateService,
userInvoiceInfoService: userInvoiceInfoService,
storageService: storageService,
logger: logger,
}
}
// ApplyInvoice 申请开票
func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error) {
// 1. 验证用户是否存在
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if user.ID == "" {
return nil, fmt.Errorf("用户不存在")
}
// 2. 验证发票类型
invoiceType := value_objects.InvoiceType(req.InvoiceType)
if !invoiceType.IsValid() {
return nil, fmt.Errorf("无效的发票类型")
}
// 3. 获取用户企业认证信息
userWithEnterprise, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 4. 检查用户是否有企业认证信息
if userWithEnterprise.EnterpriseInfo == nil {
return nil, fmt.Errorf("用户未完成企业认证,无法申请开票")
}
// 5. 获取用户开票信息
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(
ctx,
userID,
userWithEnterprise.EnterpriseInfo.CompanyName,
userWithEnterprise.EnterpriseInfo.UnifiedSocialCode,
)
if err != nil {
return nil, err
}
// 6. 验证开票信息完整性
invoiceInfo := value_objects.NewInvoiceInfo(
userInvoiceInfo.CompanyName,
userInvoiceInfo.TaxpayerID,
userInvoiceInfo.BankName,
userInvoiceInfo.BankAccount,
userInvoiceInfo.CompanyAddress,
userInvoiceInfo.CompanyPhone,
userInvoiceInfo.ReceivingEmail,
)
if err := s.userInvoiceInfoService.ValidateInvoiceInfo(ctx, invoiceInfo, invoiceType); err != nil {
return nil, err
}
// 7. 计算可开票金额
availableAmount, err := s.calculateAvailableAmount(ctx, userID)
if err != nil {
return nil, fmt.Errorf("计算可开票金额失败: %w", err)
}
// 8. 验证开票金额
amount, err := decimal.NewFromString(req.Amount)
if err != nil {
return nil, fmt.Errorf("无效的金额格式: %w", err)
}
if err := s.invoiceDomainService.ValidateInvoiceAmount(ctx, amount, availableAmount); err != nil {
return nil, err
}
// 9. 调用聚合服务申请开票
aggregateReq := services.ApplyInvoiceRequest{
InvoiceType: invoiceType,
Amount: req.Amount,
InvoiceInfo: invoiceInfo,
}
application, err := s.invoiceAggregateService.ApplyInvoice(ctx, userID, aggregateReq)
if err != nil {
return nil, err
}
// 10. 构建响应DTO
return &dto.InvoiceApplicationResponse{
ID: application.ID,
UserID: application.UserID,
InvoiceType: application.InvoiceType,
Amount: application.Amount,
Status: application.Status,
InvoiceInfo: invoiceInfo,
CreatedAt: application.CreatedAt,
}, nil
}
// GetUserInvoiceInfo 获取用户发票信息
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error) {
// 1. 获取用户企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 2. 获取企业认证信息
var companyName, taxpayerID string
var companyNameReadOnly, taxpayerIDReadOnly bool
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
taxpayerID = user.EnterpriseInfo.UnifiedSocialCode
companyNameReadOnly = true
taxpayerIDReadOnly = true
}
// 3. 获取用户开票信息(包含企业认证信息)
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(ctx, userID, companyName, taxpayerID)
if err != nil {
return nil, err
}
// 4. 构建响应DTO
return &dto.InvoiceInfoResponse{
CompanyName: userInvoiceInfo.CompanyName,
TaxpayerID: userInvoiceInfo.TaxpayerID,
BankName: userInvoiceInfo.BankName,
BankAccount: userInvoiceInfo.BankAccount,
CompanyAddress: userInvoiceInfo.CompanyAddress,
CompanyPhone: userInvoiceInfo.CompanyPhone,
ReceivingEmail: userInvoiceInfo.ReceivingEmail,
IsComplete: userInvoiceInfo.IsComplete(),
MissingFields: userInvoiceInfo.GetMissingFields(),
// 字段权限标识
CompanyNameReadOnly: companyNameReadOnly, // 公司名称只读(从企业认证信息获取)
TaxpayerIDReadOnly: taxpayerIDReadOnly, // 纳税人识别号只读(从企业认证信息获取)
}, nil
}
// UpdateUserInvoiceInfo 更新用户发票信息
func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
// 1. 获取用户企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 2. 检查用户是否有企业认证信息
if user.EnterpriseInfo == nil {
return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
}
// 3. 创建开票信息对象,公司名称和纳税人识别号从企业认证信息中获取
invoiceInfo := value_objects.NewInvoiceInfo(
"", // 公司名称将由服务层从企业认证信息中获取
"", // 纳税人识别号将由服务层从企业认证信息中获取
req.BankName,
req.BankAccount,
req.CompanyAddress,
req.CompanyPhone,
req.ReceivingEmail,
)
// 4. 使用包含企业认证信息的方法
_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
ctx,
userID,
invoiceInfo,
user.EnterpriseInfo.CompanyName,
user.EnterpriseInfo.UnifiedSocialCode,
)
return err
}
// GetUserInvoiceRecords 获取用户开票记录
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error) {
// 1. 验证用户是否存在
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if user.ID == "" {
return nil, fmt.Errorf("用户不存在")
}
// 2. 获取发票申请记录
var status entities.ApplicationStatus
if req.Status != "" {
status = entities.ApplicationStatus(req.Status)
}
// 3. 解析时间范围
var startTime, endTime *time.Time
if req.StartTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
startTime = &t
}
}
if req.EndTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
endTime = &t
}
}
// 4. 获取发票申请记录(需要更新仓储层方法以支持时间筛选)
applications, total, err := s.invoiceRepo.FindByUserIDAndStatusWithTimeRange(ctx, userID, status, startTime, endTime, req.Page, req.PageSize)
if err != nil {
return nil, err
}
// 5. 构建响应DTO
records := make([]*dto.InvoiceRecordResponse, len(applications))
for i, app := range applications {
// 使用快照信息(申请时的开票信息)
records[i] = &dto.InvoiceRecordResponse{
ID: app.ID,
UserID: app.UserID,
InvoiceType: app.InvoiceType,
Amount: app.Amount,
Status: app.Status,
CompanyName: app.CompanyName, // 使用快照的公司名称
TaxpayerID: app.TaxpayerID, // 使用快照的纳税人识别号
BankName: app.BankName, // 使用快照的银行名称
BankAccount: app.BankAccount, // 使用快照的银行账号
CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
CompanyPhone: app.CompanyPhone, // 使用快照的企业电话
ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
FileName: app.FileName,
FileSize: app.FileSize,
FileURL: app.FileURL,
ProcessedAt: app.ProcessedAt,
CreatedAt: app.CreatedAt,
RejectReason: app.RejectReason,
}
}
return &dto.InvoiceRecordsResponse{
Records: records,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
}, nil
}
// DownloadInvoiceFile 下载发票文件
func (s *InvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error) {
// 1. 查找申请记录
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return nil, err
}
if application == nil {
return nil, fmt.Errorf("申请记录不存在")
}
// 2. 验证权限(只能下载自己的发票)
if application.UserID != userID {
return nil, fmt.Errorf("无权访问此发票")
}
// 3. 验证状态(只能下载已完成的发票)
if application.Status != entities.ApplicationStatusCompleted {
return nil, fmt.Errorf("发票尚未通过审核")
}
// 4. 验证文件信息
if application.FileURL == nil || *application.FileURL == "" {
return nil, fmt.Errorf("发票文件不存在")
}
// 5. 从七牛云下载文件内容
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %w", err)
}
// 6. 构建响应DTO
return &dto.FileDownloadResponse{
FileID: *application.FileID,
FileName: *application.FileName,
FileSize: *application.FileSize,
FileURL: *application.FileURL,
FileContent: fileContent,
}, nil
}
// GetAvailableAmount 获取可开票金额
func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error) {
// 1. 验证用户是否存在
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if user.ID == "" {
return nil, fmt.Errorf("用户不存在")
}
// 2. 计算可开票金额
availableAmount, err := s.calculateAvailableAmount(ctx, userID)
if err != nil {
return nil, err
}
// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil {
return nil, err
}
// 4. 获取待处理申请金额
pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
if err != nil {
return nil, err
}
// 5. 构建响应DTO
return &dto.AvailableAmountResponse{
AvailableAmount: availableAmount,
TotalRecharged: realRecharged, // 使用真实充值金额(支付宝充值+对公转账)
TotalGifted: totalGifted,
TotalInvoiced: totalInvoiced,
PendingApplications: pendingAmount,
}, nil
}
// calculateAvailableAmount 计算可开票金额(私有方法)
func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
if err != nil {
return decimal.Zero, err
}
// 2. 获取待处理中的申请金额
pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
if err != nil {
return decimal.Zero, err
}
fmt.Println("realRecharged", realRecharged)
fmt.Println("totalGifted", totalGifted)
fmt.Println("totalInvoiced", totalInvoiced)
fmt.Println("pendingAmount", pendingAmount)
// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额
availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
fmt.Println("availableAmount", availableAmount)
// 确保可开票金额不为负数
if availableAmount.LessThan(decimal.Zero) {
availableAmount = decimal.Zero
}
return availableAmount, nil
}
// getAmountSummary 获取金额汇总(私有方法)
func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, userID string) (decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
// 1. 获取用户所有成功的充值记录
rechargeRecords, err := s.rechargeRecordRepo.GetByUserID(ctx, userID)
if err != nil {
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
}
// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额
var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账
var totalGifted decimal.Decimal // 总赠送金额
for _, record := range rechargeRecords {
if record.IsSuccess() {
if record.RechargeType == entities.RechargeTypeGift {
// 赠送金额不计入可开票金额
totalGifted = totalGifted.Add(record.Amount)
} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer {
// 只有支付宝充值和对公转账计入可开票金额
realRecharged = realRecharged.Add(record.Amount)
}
}
}
// 3. 获取用户所有发票申请记录(包括待处理、已完成、已拒绝)
applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000) // 获取所有记录
if err != nil {
return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
}
var totalInvoiced decimal.Decimal
for _, application := range applications {
// 计算已完成的发票申请金额
if application.IsCompleted() {
totalInvoiced = totalInvoiced.Add(application.Amount)
}
// 注意:待处理中的申请金额不计算在已开票金额中,但会在可开票金额计算时被扣除
}
return realRecharged, totalGifted, totalInvoiced, nil
}
// getPendingApplicationsAmount 获取待处理申请的总金额(私有方法)
func (s *InvoiceApplicationServiceImpl) getPendingApplicationsAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
// 获取用户所有发票申请记录
applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000)
if err != nil {
return decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
}
var pendingAmount decimal.Decimal
for _, application := range applications {
// 只计算待处理状态的申请金额
if application.Status == entities.ApplicationStatusPending {
pendingAmount = pendingAmount.Add(application.Amount)
}
}
return pendingAmount, nil
}
// ==================== 管理员端发票应用服务 ====================
// AdminInvoiceApplicationService 管理员发票应用服务接口
type AdminInvoiceApplicationService interface {
// GetPendingApplications 获取发票申请列表(支持筛选)
GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error)
// ApproveInvoiceApplication 通过发票申请
ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error
// RejectInvoiceApplication 拒绝发票申请
RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error
// DownloadInvoiceFile 下载发票文件(管理员)
DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error)
}
// AdminInvoiceApplicationServiceImpl 管理员发票应用服务实现
type AdminInvoiceApplicationServiceImpl struct {
invoiceRepo finance_repo.InvoiceApplicationRepository
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
userRepo user_repo.UserRepository
invoiceAggregateService services.InvoiceAggregateService
storageService *storage.QiNiuStorageService
logger *zap.Logger
}
// NewAdminInvoiceApplicationService 创建管理员发票应用服务
func NewAdminInvoiceApplicationService(
invoiceRepo finance_repo.InvoiceApplicationRepository,
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
userRepo user_repo.UserRepository,
invoiceAggregateService services.InvoiceAggregateService,
storageService *storage.QiNiuStorageService,
logger *zap.Logger,
) AdminInvoiceApplicationService {
return &AdminInvoiceApplicationServiceImpl{
invoiceRepo: invoiceRepo,
userInvoiceInfoRepo: userInvoiceInfoRepo,
userRepo: userRepo,
invoiceAggregateService: invoiceAggregateService,
storageService: storageService,
logger: logger,
}
}
// GetPendingApplications 获取发票申请列表(支持筛选)
func (s *AdminInvoiceApplicationServiceImpl) GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error) {
// 1. 解析状态筛选
var status entities.ApplicationStatus
if req.Status != "" {
status = entities.ApplicationStatus(req.Status)
}
// 2. 解析时间范围
var startTime, endTime *time.Time
if req.StartTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
startTime = &t
}
}
if req.EndTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
endTime = &t
}
}
// 3. 获取发票申请记录(支持筛选)
var applications []*entities.InvoiceApplication
var total int64
var err error
if status != "" {
// 按状态筛选
applications, total, err = s.invoiceRepo.FindByStatusWithTimeRange(ctx, status, startTime, endTime, req.Page, req.PageSize)
} else {
// 获取所有记录(按时间筛选)
applications, total, err = s.invoiceRepo.FindAllWithTimeRange(ctx, startTime, endTime, req.Page, req.PageSize)
}
if err != nil {
return nil, err
}
// 4. 构建响应DTO
pendingApplications := make([]*dto.PendingApplicationResponse, len(applications))
for i, app := range applications {
// 使用快照信息
pendingApplications[i] = &dto.PendingApplicationResponse{
ID: app.ID,
UserID: app.UserID,
InvoiceType: app.InvoiceType,
Amount: app.Amount,
Status: app.Status,
CompanyName: app.CompanyName, // 使用快照的公司名称
TaxpayerID: app.TaxpayerID, // 使用快照的纳税人识别号
BankName: app.BankName, // 使用快照的银行名称
BankAccount: app.BankAccount, // 使用快照的银行账号
CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
CompanyPhone: app.CompanyPhone, // 使用快照的企业电话
ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
FileName: app.FileName,
FileSize: app.FileSize,
FileURL: app.FileURL,
ProcessedAt: app.ProcessedAt,
CreatedAt: app.CreatedAt,
RejectReason: app.RejectReason,
}
}
return &dto.PendingApplicationsResponse{
Applications: pendingApplications,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
}, nil
}
// ApproveInvoiceApplication 通过发票申请
func (s *AdminInvoiceApplicationServiceImpl) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error {
// 1. 验证申请是否存在
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return err
}
if application == nil {
return fmt.Errorf("发票申请不存在")
}
// 2. 验证申请状态
if application.Status != entities.ApplicationStatusPending {
return fmt.Errorf("发票申请状态不允许处理")
}
// 3. 调用聚合服务处理申请
aggregateReq := services.ApproveInvoiceRequest{
AdminNotes: req.AdminNotes,
}
return s.invoiceAggregateService.ApproveInvoiceApplication(ctx, applicationID, file, aggregateReq)
}
// RejectInvoiceApplication 拒绝发票申请
func (s *AdminInvoiceApplicationServiceImpl) RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error {
// 1. 验证申请是否存在
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return err
}
if application == nil {
return fmt.Errorf("发票申请不存在")
}
// 2. 验证申请状态
if application.Status != entities.ApplicationStatusPending {
return fmt.Errorf("发票申请状态不允许处理")
}
// 3. 调用聚合服务处理申请
aggregateReq := services.RejectInvoiceRequest{
Reason: req.Reason,
}
return s.invoiceAggregateService.RejectInvoiceApplication(ctx, applicationID, aggregateReq)
}
// ==================== 请求和响应DTO ====================
type ApplyInvoiceRequest struct {
InvoiceType string `json:"invoice_type" binding:"required"` // 发票类型general/special
Amount string `json:"amount" binding:"required"` // 开票金额
}
type UpdateInvoiceInfoRequest struct {
CompanyName string `json:"company_name"` // 公司名称(从企业认证信息获取,用户不可修改)
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号(从企业认证信息获取,用户不可修改)
BankName string `json:"bank_name"` // 银行名称
CompanyAddress string `json:"company_address"` // 公司地址
BankAccount string `json:"bank_account"` // 银行账户
CompanyPhone string `json:"company_phone"` // 企业注册电话
ReceivingEmail string `json:"receiving_email" binding:"required,email"` // 发票接收邮箱
}
type GetInvoiceRecordsRequest struct {
Page int `json:"page"` // 页码
PageSize int `json:"page_size"` // 每页数量
Status string `json:"status"` // 状态筛选
StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
EndTime string `json:"end_time"` // 结束时间 (格式: 2006-01-02 15:04:05)
}
type GetPendingApplicationsRequest struct {
Page int `json:"page"` // 页码
PageSize int `json:"page_size"` // 每页数量
Status string `json:"status"` // 状态筛选pending/completed/rejected
StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
EndTime string `json:"end_time"` // 结束时间 (格式: 2006-01-02 15:04:05)
}
type ApproveInvoiceRequest struct {
AdminNotes string `json:"admin_notes"` // 管理员备注
}
type RejectInvoiceRequest struct {
Reason string `json:"reason" binding:"required"` // 拒绝原因
}
// DownloadInvoiceFile 下载发票文件(管理员)
func (s *AdminInvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error) {
// 1. 查找申请记录
application, err := s.invoiceRepo.FindByID(ctx, applicationID)
if err != nil {
return nil, err
}
if application == nil {
return nil, fmt.Errorf("申请记录不存在")
}
// 2. 验证状态(只能下载已完成的发票)
if application.Status != entities.ApplicationStatusCompleted {
return nil, fmt.Errorf("发票尚未通过审核")
}
// 3. 验证文件信息
if application.FileURL == nil || *application.FileURL == "" {
return nil, fmt.Errorf("发票文件不存在")
}
// 4. 从七牛云下载文件内容
fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
if err != nil {
return nil, fmt.Errorf("下载文件失败: %w", err)
}
// 5. 构建响应DTO
return &dto.FileDownloadResponse{
FileID: *application.FileID,
FileName: *application.FileName,
FileSize: *application.FileSize,
FileURL: *application.FileURL,
FileContent: fileContent,
}, nil
}

View File

@@ -4,10 +4,16 @@ package queries
type ListSubscriptionsQuery struct {
Page int `form:"page" binding:"omitempty,min=1" comment:"页码"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" comment:"每页数量"`
UserID string `form:"-" comment:"用户ID"`
UserID string `form:"user_id" binding:"omitempty" comment:"用户ID"`
Keyword string `form:"keyword" binding:"omitempty,max=100" comment:"搜索关键词"`
SortBy string `form:"sort_by" binding:"omitempty,oneof=created_at updated_at price" comment:"排序字段"`
SortOrder string `form:"sort_order" binding:"omitempty,sort_order" comment:"排序方向"`
// 新增筛选字段
CompanyName string `form:"company_name" binding:"omitempty,max=100" comment:"企业名称"`
ProductName string `form:"product_name" binding:"omitempty,max=100" comment:"产品名称"`
StartTime string `form:"start_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅开始时间"`
EndTime string `form:"end_time" binding:"omitempty,datetime=2006-01-02 15:04:05" comment:"订阅结束时间"`
}
// GetSubscriptionQuery 获取订阅详情查询

View File

@@ -4,6 +4,13 @@ import (
"time"
)
// UserSimpleResponse 用户简单信息响应
type UserSimpleResponse struct {
ID string `json:"id" comment:"用户ID"`
CompanyName string `json:"company_name" comment:"公司名称"`
Phone string `json:"phone" comment:"手机号"`
}
// SubscriptionInfoResponse 订阅详情响应
type SubscriptionInfoResponse struct {
ID string `json:"id" comment:"订阅ID"`
@@ -13,6 +20,7 @@ type SubscriptionInfoResponse struct {
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
// 关联信息
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"`

View File

@@ -2,9 +2,6 @@ package product
import (
"context"
"fmt"
"github.com/shopspring/decimal"
"go.uber.org/zap"
@@ -14,22 +11,26 @@ import (
"tyapi-server/internal/domains/product/entities"
repoQueries "tyapi-server/internal/domains/product/repositories/queries"
product_service "tyapi-server/internal/domains/product/services"
user_repositories "tyapi-server/internal/domains/user/repositories"
)
// SubscriptionApplicationServiceImpl 订阅应用服务实现
// 负责业务流程编排、事务管理、数据转换,不直接操作仓库
type SubscriptionApplicationServiceImpl struct {
productSubscriptionService *product_service.ProductSubscriptionService
userRepo user_repositories.UserRepository
logger *zap.Logger
}
// NewSubscriptionApplicationService 创建订阅应用服务
func NewSubscriptionApplicationService(
productSubscriptionService *product_service.ProductSubscriptionService,
userRepo user_repositories.UserRepository,
logger *zap.Logger,
) SubscriptionApplicationService {
return &SubscriptionApplicationServiceImpl{
productSubscriptionService: productSubscriptionService,
userRepo: userRepo,
logger: logger,
}
}
@@ -37,19 +38,7 @@ func NewSubscriptionApplicationService(
// UpdateSubscriptionPrice 更新订阅价格
// 业务流程1. 获取订阅 2. 更新价格 3. 保存订阅
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
// 1. 获取现有订阅
subscription, err := s.productSubscriptionService.GetSubscriptionByID(ctx, cmd.ID)
if err != nil {
return err
}
// 2. 更新订阅价格
subscription.Price = decimal.NewFromFloat(cmd.Price)
// 3. 保存订阅
// 这里需要扩展领域服务来支持更新操作
// 暂时返回错误
return fmt.Errorf("更新订阅价格功能暂未实现")
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price)
}
// CreateSubscription 创建订阅
@@ -74,12 +63,16 @@ func (s *SubscriptionApplicationServiceImpl) GetSubscriptionByID(ctx context.Con
// 业务流程1. 获取订阅列表 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Context, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
repoQuery := &repoQueries.ListSubscriptionsQuery{
Page: query.Page,
PageSize: query.PageSize,
UserID: query.UserID, // 管理员可以按用户筛选
Keyword: query.Keyword,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
Page: query.Page,
PageSize: query.PageSize,
UserID: query.UserID, // 管理员可以按用户筛选
Keyword: query.Keyword,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
CompanyName: query.CompanyName,
ProductName: query.ProductName,
StartTime: query.StartTime,
EndTime: query.EndTime,
}
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
if err != nil {
@@ -104,12 +97,16 @@ func (s *SubscriptionApplicationServiceImpl) ListSubscriptions(ctx context.Conte
// 业务流程1. 获取用户订阅列表 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) ListMySubscriptions(ctx context.Context, userID string, query *appQueries.ListSubscriptionsQuery) (*responses.SubscriptionListResponse, error) {
repoQuery := &repoQueries.ListSubscriptionsQuery{
Page: query.Page,
PageSize: query.PageSize,
UserID: userID, // 强制设置为当前用户ID
Keyword: query.Keyword,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
Page: query.Page,
PageSize: query.PageSize,
UserID: userID, // 强制设置为当前用户ID
Keyword: query.Keyword,
SortBy: query.SortBy,
SortOrder: query.SortOrder,
CompanyName: query.CompanyName,
ProductName: query.ProductName,
StartTime: query.StartTime,
EndTime: query.EndTime,
}
subscriptions, total, err := s.productSubscriptionService.ListSubscriptions(ctx, repoQuery)
if err != nil {
@@ -173,42 +170,56 @@ func (s *SubscriptionApplicationServiceImpl) GetSubscriptionUsage(ctx context.Co
// GetSubscriptionStats 获取订阅统计信息
// 业务流程1. 获取订阅统计 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetSubscriptionStats(ctx context.Context) (*responses.SubscriptionStatsResponse, error) {
// 这里需要扩展领域服务来支持统计功能
// 暂时返回默认值
stats, err := s.productSubscriptionService.GetSubscriptionStats(ctx)
if err != nil {
return nil, err
}
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: 0,
TotalRevenue: 0,
TotalSubscriptions: stats["total_subscriptions"].(int64),
TotalRevenue: stats["total_revenue"].(float64),
}, nil
}
// GetMySubscriptionStats 获取我的订阅统计信息
// 业务流程1. 获取用户订阅统计 2. 构建响应数据
func (s *SubscriptionApplicationServiceImpl) GetMySubscriptionStats(ctx context.Context, userID string) (*responses.SubscriptionStatsResponse, error) {
// 获取用户订阅数量
subscriptions, err := s.productSubscriptionService.GetUserSubscriptions(ctx, userID)
stats, err := s.productSubscriptionService.GetUserSubscriptionStats(ctx, userID)
if err != nil {
return nil, err
}
// 计算总收益
var totalRevenue float64
for _, subscription := range subscriptions {
totalRevenue += subscription.Price.InexactFloat64()
}
return &responses.SubscriptionStatsResponse{
TotalSubscriptions: int64(len(subscriptions)),
TotalRevenue: totalRevenue,
TotalSubscriptions: stats["total_subscriptions"].(int64),
TotalRevenue: stats["total_revenue"].(float64),
}, nil
}
// convertToSubscriptionInfoResponse 转换为订阅信息响应
func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(subscription *entities.Subscription) *responses.SubscriptionInfoResponse {
// 查询用户信息
var userInfo *responses.UserSimpleResponse
if subscription.UserID != "" {
user, err := s.userRepo.GetByIDWithEnterpriseInfo(context.Background(), subscription.UserID)
if err == nil {
companyName := "未知公司"
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
}
userInfo = &responses.UserSimpleResponse{
ID: user.ID,
CompanyName: companyName,
Phone: user.Phone,
}
}
}
return &responses.SubscriptionInfoResponse{
ID: subscription.ID,
UserID: subscription.UserID,
ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(),
User: userInfo,
Product: s.convertToProductSimpleResponse(subscription.Product),
APIUsed: subscription.APIUsed,
CreatedAt: subscription.CreatedAt,

View File

@@ -31,6 +31,19 @@ type EnterpriseInfoItem struct {
LegalPersonPhone string `json:"legal_person_phone"`
EnterpriseAddress string `json:"enterprise_address"`
CreatedAt time.Time `json:"created_at"`
// 合同信息
Contracts []*ContractInfoItem `json:"contracts,omitempty"`
}
// ContractInfoItem 合同信息项
type ContractInfoItem struct {
ID string `json:"id"`
ContractName string `json:"contract_name"`
ContractType string `json:"contract_type"` // 合同类型代码
ContractTypeName string `json:"contract_type_name"` // 合同类型中文名称
ContractFileURL string `json:"contract_file_url"`
CreatedAt time.Time `json:"created_at"`
}
// UserListResponse 用户列表响应
@@ -46,4 +59,9 @@ type UserStatsResponse struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
CertifiedUsers int64 `json:"certified_users"`
}
// UserDetailResponse 用户详情响应
type UserDetailResponse struct {
*UserListItem
}

View File

@@ -20,5 +20,6 @@ type UserApplicationService interface {
// 管理员功能
ListUsers(ctx context.Context, query *queries.ListUsersQuery) (*responses.UserListResponse, error)
GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error)
GetUserStats(ctx context.Context) (*responses.UserStatsResponse, error)
}

View File

@@ -24,6 +24,7 @@ type UserApplicationServiceImpl struct {
userAuthService *user_service.UserAuthService
smsCodeService *user_service.SMSCodeService
walletService finance_service.WalletAggregateService
contractService user_service.ContractAggregateService
eventBus interfaces.EventBus
jwtAuth *middleware.JWTAuthMiddleware
logger *zap.Logger
@@ -35,6 +36,7 @@ func NewUserApplicationService(
userAuthService *user_service.UserAuthService,
smsCodeService *user_service.SMSCodeService,
walletService finance_service.WalletAggregateService,
contractService user_service.ContractAggregateService,
eventBus interfaces.EventBus,
jwtAuth *middleware.JWTAuthMiddleware,
logger *zap.Logger,
@@ -44,6 +46,7 @@ func NewUserApplicationService(
userAuthService: userAuthService,
smsCodeService: smsCodeService,
walletService: walletService,
contractService: contractService,
eventBus: eventBus,
jwtAuth: jwtAuth,
logger: logger,
@@ -342,6 +345,23 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
CreatedAt: user.EnterpriseInfo.CreatedAt,
}
// 获取企业合同信息
contracts, err := s.contractService.FindByUserID(ctx, user.ID)
if err == nil && len(contracts) > 0 {
contractItems := make([]*responses.ContractInfoItem, 0, len(contracts))
for _, contract := range contracts {
contractItems = append(contractItems, &responses.ContractInfoItem{
ID: contract.ID,
ContractName: contract.ContractName,
ContractType: string(contract.ContractType),
ContractTypeName: contract.GetContractTypeName(),
ContractFileURL: contract.ContractFileURL,
CreatedAt: contract.CreatedAt,
})
}
item.EnterpriseInfo.Contracts = contractItems
}
}
// 添加钱包余额信息
@@ -363,6 +383,72 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri
}, nil
}
// GetUserDetail 获取用户详情(管理员功能)
// 业务流程1. 查询用户详情 2. 构建响应数据
func (s *UserApplicationServiceImpl) GetUserDetail(ctx context.Context, userID string) (*responses.UserDetailResponse, error) {
// 1. 查询用户详情(包含企业信息)
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, err
}
// 2. 构建响应数据
item := &responses.UserListItem{
ID: user.ID,
Phone: user.Phone,
UserType: user.UserType,
Username: user.Username,
IsActive: user.Active,
IsCertified: user.IsCertified,
LoginCount: user.LoginCount,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
// 添加企业信息
if user.EnterpriseInfo != nil {
item.EnterpriseInfo = &responses.EnterpriseInfoItem{
ID: user.EnterpriseInfo.ID,
CompanyName: user.EnterpriseInfo.CompanyName,
UnifiedSocialCode: user.EnterpriseInfo.UnifiedSocialCode,
LegalPersonName: user.EnterpriseInfo.LegalPersonName,
LegalPersonPhone: user.EnterpriseInfo.LegalPersonPhone,
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
CreatedAt: user.EnterpriseInfo.CreatedAt,
}
// 获取企业合同信息
contracts, err := s.contractService.FindByUserID(ctx, user.ID)
if err == nil && len(contracts) > 0 {
contractItems := make([]*responses.ContractInfoItem, 0, len(contracts))
for _, contract := range contracts {
contractItems = append(contractItems, &responses.ContractInfoItem{
ID: contract.ID,
ContractName: contract.ContractName,
ContractType: string(contract.ContractType),
ContractTypeName: contract.GetContractTypeName(),
ContractFileURL: contract.ContractFileURL,
CreatedAt: contract.CreatedAt,
})
}
item.EnterpriseInfo.Contracts = contractItems
}
}
// 添加钱包余额信息
wallet, err := s.walletService.LoadWalletByUserId(ctx, user.ID)
if err == nil && wallet != nil {
item.WalletBalance = wallet.Balance.String()
} else {
item.WalletBalance = "0"
}
return &responses.UserDetailResponse{
UserListItem: item,
}, nil
}
// GetUserStats 获取用户统计信息(管理员功能)
// 业务流程1. 查询用户统计信息 2. 构建响应数据
func (s *UserApplicationServiceImpl) GetUserStats(ctx context.Context) (*responses.UserStatsResponse, error) {

View File

@@ -14,6 +14,7 @@ type Config struct {
JWT JWTConfig `mapstructure:"jwt"`
API APIConfig `mapstructure:"api"`
SMS SMSConfig `mapstructure:"sms"`
Email EmailConfig `mapstructure:"email"`
Storage StorageConfig `mapstructure:"storage"`
OCR OCRConfig `mapstructure:"ocr"`
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
@@ -184,6 +185,18 @@ type SMSRateLimit struct {
MinInterval time.Duration `mapstructure:"min_interval"` // 最小发送间隔
}
// EmailConfig 邮件服务配置
type EmailConfig struct {
Host string `mapstructure:"host"` // SMTP服务器地址
Port int `mapstructure:"port"` // SMTP服务器端口
Username string `mapstructure:"username"` // 邮箱用户名
Password string `mapstructure:"password"` // 邮箱密码/授权码
FromEmail string `mapstructure:"from_email"` // 发件人邮箱
UseSSL bool `mapstructure:"use_ssl"` // 是否使用SSL
Timeout time.Duration `mapstructure:"timeout"` // 超时时间
Domain string `mapstructure:"domain"` // 控制台域名
}
// GetDSN 获取数据库DSN连接字符串
func (d DatabaseConfig) GetDSN() string {
return "host=" + d.Host +

View File

@@ -26,6 +26,8 @@ import (
certification_repo "tyapi-server/internal/infrastructure/database/repositories/certification"
finance_repo "tyapi-server/internal/infrastructure/database/repositories/finance"
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
infra_events "tyapi-server/internal/infrastructure/events"
"tyapi-server/internal/infrastructure/external/email"
"tyapi-server/internal/infrastructure/external/ocr"
"tyapi-server/internal/infrastructure/external/sms"
"tyapi-server/internal/infrastructure/external/storage"
@@ -114,22 +116,24 @@ func NewContainer() *Container {
}
},
// 提供普通的*zap.Logger用于大多数场景
func(log logger.Logger) *zap.Logger {
// 尝试转换为ZapLogger
if zapLogger, ok := log.(*logger.ZapLogger); ok {
return zapLogger.GetZapLogger()
}
// 尝试转换为LevelLogger
if levelLogger, ok := log.(*logger.LevelLogger); ok {
// 获取Info级别的日志器作为默认
if infoLogger := levelLogger.GetLevelLogger(zapcore.InfoLevel); infoLogger != nil {
return infoLogger
fx.Annotate(
func(log logger.Logger) *zap.Logger {
// 尝试转换为ZapLogger
if zapLogger, ok := log.(*logger.ZapLogger); ok {
return zapLogger.GetZapLogger()
}
}
// 如果类型转换失败创建一个默认的zap logger
defaultLogger, _ := zap.NewProduction()
return defaultLogger
},
// 尝试转换为LevelLogger
if levelLogger, ok := log.(*logger.LevelLogger); ok {
// 获取Info级别的日志器作为默认
if infoLogger := levelLogger.GetLevelLogger(zapcore.InfoLevel); infoLogger != nil {
return infoLogger
}
}
// 如果类型转换失败创建一个默认的zap logger
defaultLogger, _ := zap.NewProduction()
return defaultLogger
},
),
// 数据库连接
func(cfg *config.Config, cacheService interfaces.CacheService, logger *zap.Logger) (*gorm.DB, error) {
@@ -178,8 +182,10 @@ func NewContainer() *Container {
func() int {
return 5 // 默认5个工作协程
},
events.NewMemoryEventBus,
fx.Annotate(events.NewMemoryEventBus, fx.As(new(interfaces.EventBus))),
fx.Annotate(
events.NewMemoryEventBus,
fx.As(new(interfaces.EventBus)),
),
// 健康检查
health.NewHealthChecker,
// 提供 config.SMSConfig
@@ -196,6 +202,12 @@ func NewContainer() *Container {
},
// 短信服务
sms.NewAliSMSService,
// 邮件服务
fx.Annotate(
func(cfg *config.Config, logger *zap.Logger) *email.QQEmailService {
return email.NewQQEmailService(cfg.Email, logger)
},
),
// 存储服务
fx.Annotate(
func(cfg *config.Config, logger *zap.Logger) *storage.QiNiuStorageService {
@@ -421,6 +433,16 @@ func NewContainer() *Container {
finance_repo.NewGormAlipayOrderRepository,
fx.As(new(domain_finance_repo.AlipayOrderRepository)),
),
// 发票申请仓储
fx.Annotate(
finance_repo.NewGormInvoiceApplicationRepository,
fx.As(new(domain_finance_repo.InvoiceApplicationRepository)),
),
// 用户开票信息仓储
fx.Annotate(
finance_repo.NewGormUserInvoiceInfoRepository,
fx.As(new(domain_finance_repo.UserInvoiceInfoRepository)),
),
),
// 仓储层 - 产品域
@@ -465,7 +487,9 @@ func NewContainer() *Container {
// 领域服务
fx.Provide(
user_service.NewUserAggregateService,
fx.Annotate(
user_service.NewUserAggregateService,
),
user_service.NewUserAuthService,
user_service.NewSMSCodeService,
user_service.NewContractAggregateService,
@@ -475,6 +499,43 @@ func NewContainer() *Container {
product_service.NewProductDocumentationService,
finance_service.NewWalletAggregateService,
finance_service.NewRechargeRecordService,
// 发票领域服务
fx.Annotate(
finance_service.NewInvoiceDomainService,
),
// 用户开票信息服务
fx.Annotate(
finance_service.NewUserInvoiceInfoService,
),
// 发票事件发布器 - 绑定到接口
fx.Annotate(
func(logger *zap.Logger, eventBus interfaces.EventBus) finance_service.EventPublisher {
return infra_events.NewInvoiceEventPublisher(logger, eventBus)
},
fx.As(new(finance_service.EventPublisher)),
),
// 发票聚合服务 - 需要用户开票信息仓储
fx.Annotate(
func(
applicationRepo domain_finance_repo.InvoiceApplicationRepository,
userInvoiceInfoRepo domain_finance_repo.UserInvoiceInfoRepository,
domainService finance_service.InvoiceDomainService,
qiniuStorageService *storage.QiNiuStorageService,
logger *zap.Logger,
eventPublisher finance_service.EventPublisher,
) finance_service.InvoiceAggregateService {
return finance_service.NewInvoiceAggregateService(
applicationRepo,
userInvoiceInfoRepo,
domainService,
qiniuStorageService,
logger,
eventPublisher,
)
},
),
// 发票事件处理器
infra_events.NewInvoiceEventHandler,
certification_service.NewCertificationAggregateService,
certification_service.NewEnterpriseInfoSubmitRecordService,
),
@@ -508,6 +569,16 @@ func NewContainer() *Container {
finance.NewFinanceApplicationService,
fx.As(new(finance.FinanceApplicationService)),
),
// 发票应用服务 - 绑定到接口
fx.Annotate(
finance.NewInvoiceApplicationService,
fx.As(new(finance.InvoiceApplicationService)),
),
// 管理员发票应用服务 - 绑定到接口
fx.Annotate(
finance.NewAdminInvoiceApplicationService,
fx.As(new(finance.AdminInvoiceApplicationService)),
),
// 产品应用服务 - 绑定到接口
fx.Annotate(
product.NewProductApplicationService,
@@ -571,6 +642,7 @@ func NewContainer() *Container {
RegisterLifecycleHooks,
RegisterMiddlewares,
RegisterRoutes,
RegisterEventHandlers,
),
)
@@ -761,3 +833,34 @@ func NewRedisCache(client *redis.Client, logger *zap.Logger, cfg *config.Config)
func NewTracedRedisCache(client *redis.Client, tracer *tracing.Tracer, logger *zap.Logger, cfg *config.Config) interfaces.CacheService {
return tracing.NewTracedRedisCache(client, tracer, logger, "app")
}
// RegisterEventHandlers 注册事件处理器
func RegisterEventHandlers(
eventBus interfaces.EventBus,
invoiceEventHandler *infra_events.InvoiceEventHandler,
logger *zap.Logger,
) {
// 启动事件总线
if err := eventBus.Start(context.Background()); err != nil {
logger.Error("启动事件总线失败", zap.Error(err))
return
}
// 注册发票事件处理器
for _, eventType := range invoiceEventHandler.GetEventTypes() {
if err := eventBus.Subscribe(eventType, invoiceEventHandler); err != nil {
logger.Error("注册发票事件处理器失败",
zap.String("event_type", eventType),
zap.String("handler", invoiceEventHandler.GetName()),
zap.Error(err),
)
} else {
logger.Info("发票事件处理器注册成功",
zap.String("event_type", eventType),
zap.String("handler", invoiceEventHandler.GetName()),
)
}
}
logger.Info("所有事件处理器已注册")
}

View File

@@ -31,6 +31,9 @@ type FLXG162AReq struct {
type FLXG0687Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type FLXG21Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}
type FLXG970FReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`

View File

@@ -26,4 +26,7 @@ type ApiCallRepository interface {
// 新增根据TransactionID查询
FindByTransactionId(ctx context.Context, transactionId string) (*entities.ApiCall, error)
// 管理端根据条件筛选所有API调用记录包含产品名称
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error)
}

View File

@@ -1,62 +0,0 @@
package services
import (
"context"
"encoding/json"
"testing"
"tyapi-server/internal/application/api/commands"
)
// 基础测试结构体
type apiRequestServiceTestSuite struct {
t *testing.T
ctx context.Context
service *ApiRequestService
}
// 初始化测试套件
func newApiRequestServiceTestSuite(t *testing.T) *apiRequestServiceTestSuite {
// 这里可以初始化依赖的mock或fake对象
// 例如mockProcessorDeps := &MockProcessorDeps{}
// service := &ApiRequestService{processorDeps: mockProcessorDeps}
// 这里只做基础架构具体mock实现后续补充
return &apiRequestServiceTestSuite{
t: t,
ctx: context.Background(),
service: nil, // 这里后续可替换为实际service或mock
}
}
// 示例测试PreprocessRequestApi方法仅结构具体mock和断言后续补充
func TestApiRequestService_PreprocessRequestApi(t *testing.T) {
suite := newApiRequestServiceTestSuite(t)
// 假设有一个mock processor和注册
// RequestProcessors = map[string]processors.ProcessorFunc{
// "MOCKAPI": func(ctx context.Context, params []byte, deps interface{}) ([]byte, error) {
// return []byte("ok"), nil
// },
// }
// 这里仅做结构示例
apiCode := "QYGL23T7"
params := map[string]string{
"code": "91460000MAE471M58X",
"name": "海南天远大数据科技有限公司",
"legalPersonName": "刘福思",
}
paramsByte, err := json.Marshal(params)
if err != nil {
t.Fatalf("参数序列化失败: %v", err)
}
options := commands.ApiCallOptions{} // 实际应为*commands.ApiCallOptions
// 由于service为nil这里仅做断言结构示例
if suite.service != nil {
resp, err := suite.service.PreprocessRequestApi(suite.ctx, apiCode, paramsByte, &options)
if err != nil {
t.Errorf("PreprocessRequestApi 调用出错: %v", err)
}
t.Logf("PreprocessRequestApi 返回结果: %s", string(resp))
}
}

View File

@@ -22,5 +22,9 @@ func ProcessCOMB298YRequest(ctx context.Context, params []byte, deps *processors
// 调用组合包服务处理请求
// Options会自动传递给所有子处理器
return deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB298Y")
combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB298Y")
if err != nil {
return nil, err
}
return json.Marshal(combinedResult)
}

View File

@@ -22,5 +22,15 @@ func ProcessCOMB86PMRequest(ctx context.Context, params []byte, deps *processors
// 调用组合包服务处理请求
// Options会自动传递给所有子处理器
return deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB86PM")
combinedResult, err := deps.CombService.ProcessCombRequest(ctx, params, deps, "COMB86PM")
if err != nil {
return nil, err
}
// 如果有ApiCode为FLXG54F5的子产品改名为FLXG54F6
for _, resp := range combinedResult.Responses {
if resp.ApiCode == "FLXG54F5" {
resp.ApiCode = "FLXG54F5"
}
}
return json.Marshal(combinedResult)
}

View File

@@ -32,7 +32,7 @@ func (cs *CombService) RegisterProcessor(apiCode string, processor processors.Pr
}
// ProcessCombRequest 处理组合包请求 - 实现 CombServiceInterface
func (cs *CombService) ProcessCombRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies, packageCode string) ([]byte, error) {
func (cs *CombService) ProcessCombRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies, packageCode string) (*processors.CombinedResult, error) {
// 1. 根据组合包code获取产品信息
packageProduct, err := cs.productManagementService.GetProductByCode(ctx, packageCode)
if err != nil {
@@ -66,8 +66,8 @@ func (cs *CombService) processSubProducts(
params []byte,
deps *processors.ProcessorDependencies,
packageItems []*entities.ProductPackageItem,
) []*SubProductResult {
results := make([]*SubProductResult, 0, len(packageItems))
) []*processors.SubProductResult {
results := make([]*processors.SubProductResult, 0, len(packageItems))
var wg sync.WaitGroup
var mu sync.Mutex
@@ -101,8 +101,8 @@ func (cs *CombService) processSingleSubProduct(
params []byte,
deps *processors.ProcessorDependencies,
item *entities.ProductPackageItem,
) *SubProductResult {
result := &SubProductResult{
) *processors.SubProductResult {
result := &processors.SubProductResult{
ApiCode: item.Product.Code,
SortOrder: item.SortOrder,
Success: false,
@@ -136,31 +136,12 @@ func (cs *CombService) processSingleSubProduct(
}
// combineResults 组合所有子产品的结果
func (cs *CombService) combineResults(results []*SubProductResult) ([]byte, error) {
func (cs *CombService) combineResults(results []*processors.SubProductResult) (*processors.CombinedResult, error) {
// 构建组合结果
combinedResult := &CombinedResult{
combinedResult := &processors.CombinedResult{
Responses: results,
}
// 序列化结果
respBytes, err := json.Marshal(combinedResult)
if err != nil {
return nil, fmt.Errorf("序列化组合结果失败: %s", err.Error())
}
return respBytes, nil
return combinedResult, nil
}
// SubProductResult 子产品处理结果
type SubProductResult struct {
ApiCode string `json:"api_code"` // 子接口标识
Data interface{} `json:"data"` // 子接口返回数据
Success bool `json:"success"` // 是否成功
Error string `json:"error,omitempty"` // 错误信息(仅在失败时)
SortOrder int `json:"-"` // 排序字段不输出到JSON
}
// CombinedResult 组合结果
type CombinedResult struct {
Responses []*SubProductResult `json:"responses"` // 子接口响应列表
}

View File

@@ -11,7 +11,7 @@ import (
// CombServiceInterface 组合包服务接口
type CombServiceInterface interface {
ProcessCombRequest(ctx context.Context, params []byte, deps *ProcessorDependencies, packageCode string) ([]byte, error)
ProcessCombRequest(ctx context.Context, params []byte, deps *ProcessorDependencies, packageCode string) (*CombinedResult, error)
}
// ProcessorDependencies 处理器依赖容器
@@ -49,4 +49,21 @@ func (deps *ProcessorDependencies) WithOptions(options *commands.ApiCallOptions)
}
// ProcessorFunc 处理器函数类型定义
type ProcessorFunc func(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error)
type ProcessorFunc func(ctx context.Context, params []byte, deps *ProcessorDependencies) ([]byte, error)
// CombinedResult 组合结果
type CombinedResult struct {
Responses []*SubProductResult `json:"responses"` // 子接口响应列表
}
// SubProductResult 子产品处理结果
type SubProductResult struct {
ApiCode string `json:"api_code"` // 子接口标识
Data interface{} `json:"data"` // 子接口返回数据
Success bool `json:"success"` // 是否成功
Error string `json:"error,omitempty"` // 错误信息(仅在失败时)
SortOrder int `json:"-"` // 排序字段不输出到JSON
}

View File

@@ -43,4 +43,4 @@ func ProcessFLXG54F5Request(ctx context.Context, params []byte, deps *processors
}
return respBytes, nil
}
}

View File

@@ -0,0 +1,40 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/westdex"
)
// ProcessFLXGbc21Request FLXGbc21 API处理方法
func ProcessFLXGbc21Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXG21Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
}
reqData := map[string]interface{}{
"mobile": paramsDto.MobileNo,
}
respBytes, err := deps.YushanService.CallAPI("MOB032", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,163 @@
package entities
import (
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"tyapi-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,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,213 @@
package events
import (
"encoding/json"
"time"
"tyapi-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,26 @@
package repositories
import (
"context"
"time"
"tyapi-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,30 @@
package repositories
import (
"context"
"tyapi-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

@@ -25,4 +25,7 @@ type WalletTransactionRepository interface {
// 新增:统计用户钱包交易次数
CountByUserId(ctx context.Context, userId string) (int64, error)
// 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error)
}

View File

@@ -0,0 +1,277 @@
package services
import (
"context"
"fmt"
"io"
"mime/multipart"
"time"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/events"
"tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/finance/value_objects"
"tyapi-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"
"tyapi-server/internal/domains/finance/entities"
"tyapi-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

@@ -379,6 +379,15 @@ func (s *RechargeRecordServiceImpl) GetByTransferOrderID(ctx context.Context, tr
// 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)
}

View File

@@ -0,0 +1,250 @@
package services
import (
"context"
"fmt"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/repositories"
"tyapi-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,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 "未知类型"
}
}

View File

@@ -8,6 +8,12 @@ type ListSubscriptionsQuery struct {
Keyword string `json:"keyword"`
SortBy string `json:"sort_by"`
SortOrder string `json:"sort_order"`
// 新增筛选字段
CompanyName string `json:"company_name"` // 企业名称
ProductName string `json:"product_name"` // 产品名称
StartTime string `json:"start_time"` // 订阅开始时间
EndTime string `json:"end_time"` // 订阅结束时间
}
// GetSubscriptionQuery 获取订阅详情查询

View File

@@ -22,6 +22,7 @@ type SubscriptionRepository interface {
// 统计方法
CountByUser(ctx context.Context, userID string) (int64, error)
CountByProduct(ctx context.Context, productID string) (int64, error)
GetTotalRevenue(ctx context.Context) (float64, error)
// 乐观锁更新方法
IncrementAPIUsageWithOptimisticLock(ctx context.Context, subscriptionID string, increment int64) error

View File

@@ -13,6 +13,9 @@ import (
"tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/domains/product/repositories/queries"
"tyapi-server/internal/shared/interfaces"
"github.com/shopspring/decimal"
)
// ProductSubscriptionService 产品订阅领域服务
@@ -246,3 +249,74 @@ func (s *ProductSubscriptionService) IncrementSubscriptionAPIUsage(ctx context.C
return fmt.Errorf("更新失败,已重试%d次", maxRetries)
}
// GetSubscriptionStats 获取订阅统计信息
func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (map[string]interface{}, error) {
stats := make(map[string]interface{})
// 获取总订阅数
totalSubscriptions, err := s.subscriptionRepo.Count(ctx, interfaces.CountOptions{})
if err != nil {
s.logger.Error("获取订阅总数失败", zap.Error(err))
return nil, fmt.Errorf("获取订阅总数失败: %w", err)
}
stats["total_subscriptions"] = totalSubscriptions
// 获取总收入
totalRevenue, err := s.subscriptionRepo.GetTotalRevenue(ctx)
if err != nil {
s.logger.Error("获取总收入失败", zap.Error(err))
return nil, fmt.Errorf("获取总收入失败: %w", err)
}
stats["total_revenue"] = totalRevenue
return stats, nil
}
// GetUserSubscriptionStats 获取用户订阅统计信息
func (s *ProductSubscriptionService) GetUserSubscriptionStats(ctx context.Context, userID string) (map[string]interface{}, error) {
stats := make(map[string]interface{})
// 获取用户订阅数
userSubscriptions, err := s.subscriptionRepo.FindByUserID(ctx, userID)
if err != nil {
s.logger.Error("获取用户订阅失败", zap.Error(err))
return nil, fmt.Errorf("获取用户订阅失败: %w", err)
}
// 计算用户总收入
var totalRevenue float64
for _, subscription := range userSubscriptions {
totalRevenue += subscription.Price.InexactFloat64()
}
stats["total_subscriptions"] = int64(len(userSubscriptions))
stats["total_revenue"] = totalRevenue
return stats, nil
}
// UpdateSubscriptionPrice 更新订阅价格
func (s *ProductSubscriptionService) UpdateSubscriptionPrice(ctx context.Context, subscriptionID string, newPrice float64) error {
// 获取订阅
subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID)
if err != nil {
return fmt.Errorf("订阅不存在: %w", err)
}
// 更新价格
subscription.Price = decimal.NewFromFloat(newPrice)
subscription.Version++ // 增加版本号
// 保存更新
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
s.logger.Error("更新订阅价格失败", zap.Error(err))
return fmt.Errorf("更新订阅价格失败: %w", err)
}
s.logger.Info("订阅价格更新成功",
zap.String("subscription_id", subscriptionID),
zap.Float64("new_price", newPrice))
return nil
}

View File

@@ -235,4 +235,98 @@ func (r *GormApiCallRepository) FindByTransactionId(ctx context.Context, transac
return nil, err
}
return &call, nil
}
// ListWithFiltersAndProductName 管理端根据条件筛选所有API调用记录包含产品名称
func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.ApiCall, int64, error) {
var callsWithProduct []*ApiCallWithProduct
var total int64
// 构建基础查询条件
whereCondition := "1=1"
whereArgs := []interface{}{}
// 应用筛选条件
if filters != nil {
// 用户ID筛选
if userId, ok := filters["user_id"].(string); ok && userId != "" {
whereCondition += " AND ac.user_id = ?"
whereArgs = append(whereArgs, userId)
}
// 时间范围筛选
if startTime, ok := filters["start_time"].(time.Time); ok {
whereCondition += " AND ac.created_at >= ?"
whereArgs = append(whereArgs, startTime)
}
if endTime, ok := filters["end_time"].(time.Time); ok {
whereCondition += " AND ac.created_at <= ?"
whereArgs = append(whereArgs, endTime)
}
// TransactionID筛选
if transactionId, ok := filters["transaction_id"].(string); ok && transactionId != "" {
whereCondition += " AND ac.transaction_id LIKE ?"
whereArgs = append(whereArgs, "%"+transactionId+"%")
}
// 产品名称筛选
if productName, ok := filters["product_name"].(string); ok && productName != "" {
whereCondition += " AND p.name LIKE ?"
whereArgs = append(whereArgs, "%"+productName+"%")
}
// 状态筛选
if status, ok := filters["status"].(string); ok && status != "" {
whereCondition += " AND ac.status = ?"
whereArgs = append(whereArgs, status)
}
}
// 构建JOIN查询
query := r.GetDB(ctx).Table("api_calls ac").
Select("ac.*, p.name as product_name").
Joins("LEFT JOIN product p ON ac.product_id = p.id").
Where(whereCondition, whereArgs...)
// 获取总数
var count int64
err := query.Count(&count).Error
if err != nil {
return nil, nil, 0, err
}
total = count
// 应用排序和分页
if options.Sort != "" {
query = query.Order("ac." + options.Sort + " " + options.Order)
} else {
query = query.Order("ac.created_at DESC")
}
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
// 执行查询
err = query.Find(&callsWithProduct).Error
if err != nil {
return nil, nil, 0, err
}
// 转换为entities.ApiCall并构建产品名称映射
var calls []*entities.ApiCall
productNameMap := make(map[string]string)
for _, c := range callsWithProduct {
call := c.ApiCall
calls = append(calls, &call)
// 构建产品ID到产品名称的映射
if c.ProductName != "" {
productNameMap[call.ID] = c.ProductName
}
}
return productNameMap, calls, total, nil
}

View File

@@ -292,5 +292,103 @@ func (r *GormWalletTransactionRepository) ListByUserIdWithFiltersAndProductName(
}
}
return productNameMap, transactions, total, nil
}
// ListWithFiltersAndProductName 管理端:根据条件筛选所有钱包交易记录(包含产品名称)
func (r *GormWalletTransactionRepository) ListWithFiltersAndProductName(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (map[string]string, []*entities.WalletTransaction, int64, error) {
var transactionsWithProduct []*WalletTransactionWithProduct
var total int64
// 构建基础查询条件
whereCondition := "1=1"
whereArgs := []interface{}{}
// 应用筛选条件
if filters != nil {
// 用户ID筛选
if userId, ok := filters["user_id"].(string); ok && userId != "" {
whereCondition += " AND wt.user_id = ?"
whereArgs = append(whereArgs, userId)
}
// 时间范围筛选
if startTime, ok := filters["start_time"].(time.Time); ok {
whereCondition += " AND wt.created_at >= ?"
whereArgs = append(whereArgs, startTime)
}
if endTime, ok := filters["end_time"].(time.Time); ok {
whereCondition += " AND wt.created_at <= ?"
whereArgs = append(whereArgs, endTime)
}
// 交易ID筛选
if transactionId, ok := filters["transaction_id"].(string); ok && transactionId != "" {
whereCondition += " AND wt.transaction_id LIKE ?"
whereArgs = append(whereArgs, "%"+transactionId+"%")
}
// 产品名称筛选
if productName, ok := filters["product_name"].(string); ok && productName != "" {
whereCondition += " AND p.name LIKE ?"
whereArgs = append(whereArgs, "%"+productName+"%")
}
// 金额范围筛选
if minAmount, ok := filters["min_amount"].(string); ok && minAmount != "" {
whereCondition += " AND wt.amount >= ?"
whereArgs = append(whereArgs, minAmount)
}
if maxAmount, ok := filters["max_amount"].(string); ok && maxAmount != "" {
whereCondition += " AND wt.amount <= ?"
whereArgs = append(whereArgs, maxAmount)
}
}
// 构建JOIN查询
query := r.GetDB(ctx).Table("wallet_transactions wt").
Select("wt.*, p.name as product_name").
Joins("LEFT JOIN product p ON wt.product_id = p.id").
Where(whereCondition, whereArgs...)
// 获取总数
var count int64
err := query.Count(&count).Error
if err != nil {
return nil, nil, 0, err
}
total = count
// 应用排序和分页
if options.Sort != "" {
query = query.Order("wt." + options.Sort + " " + options.Order)
} else {
query = query.Order("wt.created_at DESC")
}
if options.Page > 0 && options.PageSize > 0 {
offset := (options.Page - 1) * options.PageSize
query = query.Offset(offset).Limit(options.PageSize)
}
// 执行查询
err = query.Find(&transactionsWithProduct).Error
if err != nil {
return nil, nil, 0, err
}
// 转换为entities.WalletTransaction并构建产品名称映射
var transactions []*entities.WalletTransaction
productNameMap := make(map[string]string)
for _, t := range transactionsWithProduct {
transaction := t.WalletTransaction
transactions = append(transactions, &transaction)
// 构建产品ID到产品名称的映射
if t.ProductName != "" {
productNameMap[transaction.ProductID] = t.ProductName
}
}
return productNameMap, transactions, total, nil
}

View File

@@ -0,0 +1,342 @@
package repositories
import (
"context"
"fmt"
"time"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/finance/value_objects"
"gorm.io/gorm"
)
// GormInvoiceApplicationRepository 发票申请仓储的GORM实现
type GormInvoiceApplicationRepository struct {
db *gorm.DB
}
// NewGormInvoiceApplicationRepository 创建发票申请仓储
func NewGormInvoiceApplicationRepository(db *gorm.DB) repositories.InvoiceApplicationRepository {
return &GormInvoiceApplicationRepository{
db: db,
}
}
// Create 创建发票申请
func (r *GormInvoiceApplicationRepository) Create(ctx context.Context, application *entities.InvoiceApplication) error {
return r.db.WithContext(ctx).Create(application).Error
}
// Update 更新发票申请
func (r *GormInvoiceApplicationRepository) Update(ctx context.Context, application *entities.InvoiceApplication) error {
return r.db.WithContext(ctx).Save(application).Error
}
// Save 保存发票申请
func (r *GormInvoiceApplicationRepository) Save(ctx context.Context, application *entities.InvoiceApplication) error {
return r.db.WithContext(ctx).Save(application).Error
}
// FindByID 根据ID查找发票申请
func (r *GormInvoiceApplicationRepository) FindByID(ctx context.Context, id string) (*entities.InvoiceApplication, error) {
var application entities.InvoiceApplication
err := r.db.WithContext(ctx).Where("id = ?", id).First(&application).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &application, nil
}
// FindByUserID 根据用户ID查找发票申请列表
func (r *GormInvoiceApplicationRepository) FindByUserID(ctx context.Context, userID string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
// 获取总数
err := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ?", userID).Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = r.db.WithContext(ctx).Where("user_id = ?", userID).
Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindPendingApplications 查找待处理的发票申请
func (r *GormInvoiceApplicationRepository) FindPendingApplications(ctx context.Context, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
// 获取总数
err := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).
Where("status = ?", entities.ApplicationStatusPending).
Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = r.db.WithContext(ctx).
Where("status = ?", entities.ApplicationStatusPending).
Order("created_at ASC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindByUserIDAndStatus 根据用户ID和状态查找发票申请
func (r *GormInvoiceApplicationRepository) FindByUserIDAndStatus(ctx context.Context, userID string, status entities.ApplicationStatus, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ?", userID)
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindByUserIDAndStatusWithTimeRange 根据用户ID、状态和时间范围查找发票申请列表
func (r *GormInvoiceApplicationRepository) FindByUserIDAndStatusWithTimeRange(ctx context.Context, userID string, status entities.ApplicationStatus, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ?", userID)
// 添加状态筛选
if status != "" {
query = query.Where("status = ?", status)
}
// 添加时间范围筛选
if startTime != nil {
query = query.Where("created_at >= ?", startTime)
}
if endTime != nil {
query = query.Where("created_at <= ?", endTime)
}
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindByStatus 根据状态查找发票申请
func (r *GormInvoiceApplicationRepository) FindByStatus(ctx context.Context, status entities.ApplicationStatus) ([]*entities.InvoiceApplication, error) {
var applications []*entities.InvoiceApplication
err := r.db.WithContext(ctx).
Where("status = ?", status).
Order("created_at DESC").
Find(&applications).Error
return applications, err
}
// GetUserInvoiceInfo 获取用户发票信息
// GetUserTotalInvoicedAmount 获取用户已开票总金额
func (r *GormInvoiceApplicationRepository) GetUserTotalInvoicedAmount(ctx context.Context, userID string) (string, error) {
var total string
err := r.db.WithContext(ctx).
Model(&entities.InvoiceApplication{}).
Select("COALESCE(SUM(CAST(amount AS DECIMAL(10,2))), '0')").
Where("user_id = ? AND status = ?", userID, entities.ApplicationStatusCompleted).
Scan(&total).Error
return total, err
}
// GetUserTotalAppliedAmount 获取用户申请开票总金额
func (r *GormInvoiceApplicationRepository) GetUserTotalAppliedAmount(ctx context.Context, userID string) (string, error) {
var total string
err := r.db.WithContext(ctx).
Model(&entities.InvoiceApplication{}).
Select("COALESCE(SUM(CAST(amount AS DECIMAL(10,2))), '0')").
Where("user_id = ?", userID).
Scan(&total).Error
return total, err
}
// FindByUserIDAndInvoiceType 根据用户ID和发票类型查找申请
func (r *GormInvoiceApplicationRepository) FindByUserIDAndInvoiceType(ctx context.Context, userID string, invoiceType value_objects.InvoiceType, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("user_id = ? AND invoice_type = ?", userID, invoiceType)
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindByDateRange 根据日期范围查找申请
func (r *GormInvoiceApplicationRepository) FindByDateRange(ctx context.Context, startDate, endDate string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{})
if startDate != "" {
query = query.Where("DATE(created_at) >= ?", startDate)
}
if endDate != "" {
query = query.Where("DATE(created_at) <= ?", endDate)
}
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// SearchApplications 搜索发票申请
func (r *GormInvoiceApplicationRepository) SearchApplications(ctx context.Context, keyword string, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).
Where("company_name LIKE ? OR email LIKE ? OR tax_number LIKE ?",
fmt.Sprintf("%%%s%%", keyword),
fmt.Sprintf("%%%s%%", keyword),
fmt.Sprintf("%%%s%%", keyword))
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindByStatusWithTimeRange 根据状态和时间范围查找发票申请
func (r *GormInvoiceApplicationRepository) FindByStatusWithTimeRange(ctx context.Context, status entities.ApplicationStatus, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{}).Where("status = ?", status)
// 添加时间范围筛选
if startTime != nil {
query = query.Where("created_at >= ?", startTime)
}
if endTime != nil {
query = query.Where("created_at <= ?", endTime)
}
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}
// FindAllWithTimeRange 根据时间范围查找所有发票申请
func (r *GormInvoiceApplicationRepository) FindAllWithTimeRange(ctx context.Context, startTime, endTime *time.Time, page, pageSize int) ([]*entities.InvoiceApplication, int64, error) {
var applications []*entities.InvoiceApplication
var total int64
query := r.db.WithContext(ctx).Model(&entities.InvoiceApplication{})
// 添加时间范围筛选
if startTime != nil {
query = query.Where("created_at >= ?", startTime)
}
if endTime != nil {
query = query.Where("created_at <= ?", endTime)
}
// 获取总数
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
offset := (page - 1) * pageSize
err = query.Order("created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&applications).Error
return applications, total, err
}

View File

@@ -0,0 +1,74 @@
package repositories
import (
"context"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/repositories"
"gorm.io/gorm"
)
// GormUserInvoiceInfoRepository 用户开票信息仓储的GORM实现
type GormUserInvoiceInfoRepository struct {
db *gorm.DB
}
// NewGormUserInvoiceInfoRepository 创建用户开票信息仓储
func NewGormUserInvoiceInfoRepository(db *gorm.DB) repositories.UserInvoiceInfoRepository {
return &GormUserInvoiceInfoRepository{
db: db,
}
}
// Create 创建用户开票信息
func (r *GormUserInvoiceInfoRepository) Create(ctx context.Context, info *entities.UserInvoiceInfo) error {
return r.db.WithContext(ctx).Create(info).Error
}
// Update 更新用户开票信息
func (r *GormUserInvoiceInfoRepository) Update(ctx context.Context, info *entities.UserInvoiceInfo) error {
return r.db.WithContext(ctx).Save(info).Error
}
// Save 保存用户开票信息(创建或更新)
func (r *GormUserInvoiceInfoRepository) Save(ctx context.Context, info *entities.UserInvoiceInfo) error {
return r.db.WithContext(ctx).Save(info).Error
}
// FindByUserID 根据用户ID查找开票信息
func (r *GormUserInvoiceInfoRepository) FindByUserID(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error) {
var info entities.UserInvoiceInfo
err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&info).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &info, nil
}
// FindByID 根据ID查找开票信息
func (r *GormUserInvoiceInfoRepository) FindByID(ctx context.Context, id string) (*entities.UserInvoiceInfo, error) {
var info entities.UserInvoiceInfo
err := r.db.WithContext(ctx).Where("id = ?", id).First(&info).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &info, nil
}
// Delete 删除用户开票信息
func (r *GormUserInvoiceInfoRepository) Delete(ctx context.Context, userID string) error {
return r.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.UserInvoiceInfo{}).Error
}
// Exists 检查用户开票信息是否存在
func (r *GormUserInvoiceInfoRepository) Exists(ctx context.Context, userID string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&entities.UserInvoiceInfo{}).Where("user_id = ?", userID).Count(&count).Error
return count > 0, err
}

View File

@@ -10,6 +10,7 @@ import (
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/interfaces"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"gorm.io/gorm"
)
@@ -113,13 +114,39 @@ func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, quer
// 应用筛选条件
if query.UserID != "" {
dbQuery = dbQuery.Where("user_id = ?", query.UserID)
dbQuery = dbQuery.Where("subscription.user_id = ?", query.UserID)
}
// 这里筛选的是关联的Product实体里的name或code字段只有当keyword匹配关联Product的name或code时才返回
// 关键词搜索(产品名称或编码)
if query.Keyword != "" {
dbQuery = dbQuery.Joins("LEFT JOIN product ON product.id = subscription.product_id").
Where("product.name LIKE ? OR product.code LIKE ?", "%"+query.Keyword+"%", "%"+query.Keyword+"%")
}
// 产品名称筛选
if query.ProductName != "" {
dbQuery = dbQuery.Joins("LEFT JOIN product ON product.id = subscription.product_id").
Where("product.name LIKE ?", "%"+query.ProductName+"%")
}
// 企业名称筛选(需要关联用户和企业信息)
if query.CompanyName != "" {
dbQuery = dbQuery.Joins("LEFT JOIN users ON users.id = subscription.user_id").
Joins("LEFT JOIN enterprise_infos ON enterprise_infos.user_id = users.id").
Where("enterprise_infos.company_name LIKE ?", "%"+query.CompanyName+"%")
}
// 时间范围筛选
if query.StartTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", query.StartTime); err == nil {
dbQuery = dbQuery.Where("subscription.created_at >= ?", t)
}
}
if query.EndTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", query.EndTime); err == nil {
dbQuery = dbQuery.Where("subscription.created_at <= ?", t)
}
}
// 获取总数
if err := dbQuery.Count(&total).Error; err != nil {
@@ -136,7 +163,7 @@ func (r *GormSubscriptionRepository) ListSubscriptions(ctx context.Context, quer
}
dbQuery = dbQuery.Order(order)
} else {
dbQuery = dbQuery.Order("created_at DESC")
dbQuery = dbQuery.Order("subscription.created_at DESC")
}
// 应用分页
@@ -173,13 +200,23 @@ func (r *GormSubscriptionRepository) CountByUser(ctx context.Context, userID str
return count, err
}
// CountByProduct 统计产品订阅数量
// CountByProduct 统计产品订阅数量
func (r *GormSubscriptionRepository) CountByProduct(ctx context.Context, productID string) (int64, error) {
var count int64
err := r.GetDB(ctx).WithContext(ctx).Model(&entities.Subscription{}).Where("product_id = ?", productID).Count(&count).Error
return count, err
}
// GetTotalRevenue 获取总收入
func (r *GormSubscriptionRepository) GetTotalRevenue(ctx context.Context) (float64, error) {
var total decimal.Decimal
err := r.GetDB(ctx).WithContext(ctx).Model(&entities.Subscription{}).Select("COALESCE(SUM(price), 0)").Scan(&total).Error
if err != nil {
return 0, err
}
return total.InexactFloat64(), nil
}
// 基础Repository接口方法
// Count 返回订阅总数

View File

@@ -0,0 +1,230 @@
package events
import (
"context"
"encoding/json"
"fmt"
"time"
"go.uber.org/zap"
"tyapi-server/internal/domains/finance/events"
"tyapi-server/internal/infrastructure/external/email"
"tyapi-server/internal/shared/interfaces"
)
// InvoiceEventHandler 发票事件处理器
type InvoiceEventHandler struct {
logger *zap.Logger
emailService *email.QQEmailService
name string
eventTypes []string
isAsync bool
}
// NewInvoiceEventHandler 创建发票事件处理器
func NewInvoiceEventHandler(logger *zap.Logger, emailService *email.QQEmailService) *InvoiceEventHandler {
return &InvoiceEventHandler{
logger: logger,
emailService: emailService,
name: "invoice-event-handler",
eventTypes: []string{
"InvoiceApplicationCreated",
"InvoiceApplicationApproved",
"InvoiceApplicationRejected",
"InvoiceFileUploaded",
},
isAsync: true,
}
}
// GetName 获取处理器名称
func (h *InvoiceEventHandler) GetName() string {
return h.name
}
// GetEventTypes 获取支持的事件类型
func (h *InvoiceEventHandler) GetEventTypes() []string {
return h.eventTypes
}
// IsAsync 是否为异步处理器
func (h *InvoiceEventHandler) IsAsync() bool {
return h.isAsync
}
// GetRetryConfig 获取重试配置
func (h *InvoiceEventHandler) GetRetryConfig() interfaces.RetryConfig {
return interfaces.RetryConfig{
MaxRetries: 3,
RetryDelay: 5 * time.Second,
BackoffFactor: 2.0,
MaxDelay: 30 * time.Second,
}
}
// Handle 处理事件
func (h *InvoiceEventHandler) Handle(ctx context.Context, event interfaces.Event) error {
h.logger.Info("🔄 开始处理发票事件",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
zap.String("aggregate_id", event.GetAggregateID()),
zap.String("handler_name", h.GetName()),
zap.Time("event_timestamp", event.GetTimestamp()),
)
switch event.GetType() {
case "InvoiceApplicationCreated":
h.logger.Info("📝 处理发票申请创建事件")
return h.handleInvoiceApplicationCreated(ctx, event)
case "InvoiceApplicationApproved":
h.logger.Info("✅ 处理发票申请通过事件")
return h.handleInvoiceApplicationApproved(ctx, event)
case "InvoiceApplicationRejected":
h.logger.Info("❌ 处理发票申请拒绝事件")
return h.handleInvoiceApplicationRejected(ctx, event)
case "InvoiceFileUploaded":
h.logger.Info("📎 处理发票文件上传事件")
return h.handleInvoiceFileUploaded(ctx, event)
default:
h.logger.Warn("⚠️ 未知的发票事件类型", zap.String("event_type", event.GetType()))
return nil
}
}
// handleInvoiceApplicationCreated 处理发票申请创建事件
func (h *InvoiceEventHandler) handleInvoiceApplicationCreated(ctx context.Context, event interfaces.Event) error {
h.logger.Info("发票申请已创建",
zap.String("application_id", event.GetAggregateID()),
)
// 这里可以发送通知给管理员,告知有新的发票申请
// 暂时只记录日志
return nil
}
// handleInvoiceApplicationApproved 处理发票申请通过事件
func (h *InvoiceEventHandler) handleInvoiceApplicationApproved(ctx context.Context, event interfaces.Event) error {
h.logger.Info("发票申请已通过",
zap.String("application_id", event.GetAggregateID()),
)
// 这里可以发送通知给用户,告知发票申请已通过
// 暂时只记录日志
return nil
}
// handleInvoiceApplicationRejected 处理发票申请拒绝事件
func (h *InvoiceEventHandler) handleInvoiceApplicationRejected(ctx context.Context, event interfaces.Event) error {
h.logger.Info("发票申请被拒绝",
zap.String("application_id", event.GetAggregateID()),
)
// 这里可以发送邮件通知用户,告知发票申请被拒绝
// 暂时只记录日志
return nil
}
// handleInvoiceFileUploaded 处理发票文件上传事件
func (h *InvoiceEventHandler) handleInvoiceFileUploaded(ctx context.Context, event interfaces.Event) error {
h.logger.Info("📎 发票文件已上传事件开始处理",
zap.String("invoice_id", event.GetAggregateID()),
zap.String("event_id", event.GetID()),
)
// 解析事件数据
payload := event.GetPayload()
if payload == nil {
h.logger.Error("❌ 事件数据为空")
return fmt.Errorf("事件数据为空")
}
h.logger.Info("📋 事件数据解析开始",
zap.Any("payload_type", fmt.Sprintf("%T", payload)),
)
// 将payload转换为JSON然后解析为InvoiceFileUploadedEvent
payloadBytes, err := json.Marshal(payload)
if err != nil {
h.logger.Error("❌ 序列化事件数据失败", zap.Error(err))
return fmt.Errorf("序列化事件数据失败: %w", err)
}
h.logger.Info("📄 事件数据序列化成功",
zap.String("payload_json", string(payloadBytes)),
)
var fileUploadedEvent events.InvoiceFileUploadedEvent
err = json.Unmarshal(payloadBytes, &fileUploadedEvent)
if err != nil {
h.logger.Error("❌ 解析发票文件上传事件失败", zap.Error(err))
return fmt.Errorf("解析发票文件上传事件失败: %w", err)
}
h.logger.Info("✅ 事件数据解析成功",
zap.String("invoice_id", fileUploadedEvent.InvoiceID),
zap.String("user_id", fileUploadedEvent.UserID),
zap.String("receiving_email", fileUploadedEvent.ReceivingEmail),
zap.String("file_name", fileUploadedEvent.FileName),
zap.String("file_url", fileUploadedEvent.FileURL),
zap.String("company_name", fileUploadedEvent.CompanyName),
zap.String("amount", fileUploadedEvent.Amount.String()),
zap.String("invoice_type", string(fileUploadedEvent.InvoiceType)),
)
// 发送发票邮件给用户
return h.sendInvoiceEmail(ctx, &fileUploadedEvent)
}
// sendInvoiceEmail 发送发票邮件
func (h *InvoiceEventHandler) sendInvoiceEmail(ctx context.Context, event *events.InvoiceFileUploadedEvent) error {
h.logger.Info("📧 开始发送发票邮件",
zap.String("invoice_id", event.InvoiceID),
zap.String("user_id", event.UserID),
zap.String("receiving_email", event.ReceivingEmail),
zap.String("file_name", event.FileName),
zap.String("file_url", event.FileURL),
)
// 构建邮件数据
emailData := &email.InvoiceEmailData{
CompanyName: event.CompanyName,
Amount: event.Amount.String(),
InvoiceType: event.InvoiceType.GetDisplayName(),
FileURL: event.FileURL,
FileName: event.FileName,
ReceivingEmail: event.ReceivingEmail,
ApprovedAt: event.UploadedAt.Format("2006-01-02 15:04:05"),
}
h.logger.Info("📋 邮件数据构建完成",
zap.String("company_name", emailData.CompanyName),
zap.String("amount", emailData.Amount),
zap.String("invoice_type", emailData.InvoiceType),
zap.String("file_url", emailData.FileURL),
zap.String("file_name", emailData.FileName),
zap.String("receiving_email", emailData.ReceivingEmail),
zap.String("approved_at", emailData.ApprovedAt),
)
// 发送邮件
h.logger.Info("🚀 开始调用邮件服务发送邮件")
err := h.emailService.SendInvoiceEmail(ctx, emailData)
if err != nil {
h.logger.Error("❌ 发送发票邮件失败",
zap.String("invoice_id", event.InvoiceID),
zap.String("receiving_email", event.ReceivingEmail),
zap.Error(err),
)
return fmt.Errorf("发送发票邮件失败: %w", err)
}
h.logger.Info("✅ 发票邮件发送成功",
zap.String("invoice_id", event.InvoiceID),
zap.String("receiving_email", event.ReceivingEmail),
)
return nil
}

View File

@@ -0,0 +1,115 @@
package events
import (
"context"
"go.uber.org/zap"
"tyapi-server/internal/domains/finance/events"
"tyapi-server/internal/shared/interfaces"
)
// InvoiceEventPublisher 发票事件发布器实现
type InvoiceEventPublisher struct {
logger *zap.Logger
eventBus interfaces.EventBus
}
// NewInvoiceEventPublisher 创建发票事件发布器
func NewInvoiceEventPublisher(logger *zap.Logger, eventBus interfaces.EventBus) *InvoiceEventPublisher {
return &InvoiceEventPublisher{
logger: logger,
eventBus: eventBus,
}
}
// PublishInvoiceApplicationCreated 发布发票申请创建事件
func (p *InvoiceEventPublisher) PublishInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error {
p.logger.Info("发布发票申请创建事件",
zap.String("application_id", event.ApplicationID),
zap.String("user_id", event.UserID),
zap.String("invoice_type", string(event.InvoiceType)),
zap.String("amount", event.Amount.String()),
zap.String("company_name", event.CompanyName),
zap.String("receiving_email", event.ReceivingEmail),
)
// TODO: 实现实际的事件发布逻辑
// 例如:发送到消息队列、调用外部服务等
return nil
}
// PublishInvoiceApplicationApproved 发布发票申请通过事件
func (p *InvoiceEventPublisher) PublishInvoiceApplicationApproved(ctx context.Context, event *events.InvoiceApplicationApprovedEvent) error {
p.logger.Info("发布发票申请通过事件",
zap.String("application_id", event.ApplicationID),
zap.String("user_id", event.UserID),
zap.String("amount", event.Amount.String()),
zap.String("receiving_email", event.ReceivingEmail),
zap.Time("approved_at", event.ApprovedAt),
)
// TODO: 实现实际的事件发布逻辑
// 例如:发送邮件通知用户、更新统计数据等
return nil
}
// PublishInvoiceApplicationRejected 发布发票申请拒绝事件
func (p *InvoiceEventPublisher) PublishInvoiceApplicationRejected(ctx context.Context, event *events.InvoiceApplicationRejectedEvent) error {
p.logger.Info("发布发票申请拒绝事件",
zap.String("application_id", event.ApplicationID),
zap.String("user_id", event.UserID),
zap.String("reason", event.Reason),
zap.String("receiving_email", event.ReceivingEmail),
zap.Time("rejected_at", event.RejectedAt),
)
// TODO: 实现实际的事件发布逻辑
// 例如:发送邮件通知用户、记录拒绝原因等
return nil
}
// PublishInvoiceFileUploaded 发布发票文件上传事件
func (p *InvoiceEventPublisher) PublishInvoiceFileUploaded(ctx context.Context, event *events.InvoiceFileUploadedEvent) error {
p.logger.Info("📤 开始发布发票文件上传事件",
zap.String("invoice_id", event.InvoiceID),
zap.String("user_id", event.UserID),
zap.String("file_id", event.FileID),
zap.String("file_name", event.FileName),
zap.String("file_url", event.FileURL),
zap.String("receiving_email", event.ReceivingEmail),
zap.Time("uploaded_at", event.UploadedAt),
)
// 发布到事件总线
if p.eventBus != nil {
p.logger.Info("🚀 准备发布事件到事件总线",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
)
if err := p.eventBus.Publish(ctx, event); err != nil {
p.logger.Error("❌ 发布发票文件上传事件到事件总线失败",
zap.String("invoice_id", event.InvoiceID),
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
zap.Error(err),
)
return err
}
p.logger.Info("✅ 发票文件上传事件已发布到事件总线",
zap.String("invoice_id", event.InvoiceID),
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
)
} else {
p.logger.Warn("⚠️ 事件总线未初始化,无法发布事件",
zap.String("invoice_id", event.InvoiceID),
)
}
return nil
}

View File

@@ -0,0 +1,712 @@
package email
import (
"context"
"crypto/tls"
"fmt"
"html/template"
"net"
"net/smtp"
"strings"
"time"
"go.uber.org/zap"
"tyapi-server/internal/config"
)
// QQEmailService QQ邮箱服务
type QQEmailService struct {
config config.EmailConfig
logger *zap.Logger
}
// EmailData 邮件数据
type EmailData struct {
To string `json:"to"`
Subject string `json:"subject"`
Content string `json:"content"`
Data map[string]interface{} `json:"data"`
}
// InvoiceEmailData 发票邮件数据
type InvoiceEmailData struct {
CompanyName string `json:"company_name"`
Amount string `json:"amount"`
InvoiceType string `json:"invoice_type"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
ReceivingEmail string `json:"receiving_email"`
ApprovedAt string `json:"approved_at"`
}
// NewQQEmailService 创建QQ邮箱服务
func NewQQEmailService(config config.EmailConfig, logger *zap.Logger) *QQEmailService {
return &QQEmailService{
config: config,
logger: logger,
}
}
// SendEmail 发送邮件
func (s *QQEmailService) SendEmail(ctx context.Context, data *EmailData) error {
s.logger.Info("开始发送邮件",
zap.String("to", data.To),
zap.String("subject", data.Subject),
)
// 构建邮件内容
message := s.buildEmailMessage(data)
// 发送邮件
err := s.sendSMTP(data.To, data.Subject, message)
if err != nil {
s.logger.Error("发送邮件失败",
zap.String("to", data.To),
zap.String("subject", data.Subject),
zap.Error(err),
)
return fmt.Errorf("发送邮件失败: %w", err)
}
s.logger.Info("邮件发送成功",
zap.String("to", data.To),
zap.String("subject", data.Subject),
)
return nil
}
// SendInvoiceEmail 发送发票邮件
func (s *QQEmailService) SendInvoiceEmail(ctx context.Context, data *InvoiceEmailData) error {
s.logger.Info("开始发送发票邮件",
zap.String("to", data.ReceivingEmail),
zap.String("company_name", data.CompanyName),
zap.String("amount", data.Amount),
)
// 构建邮件内容
subject := "您的发票已开具成功"
content := s.buildInvoiceEmailContent(data)
emailData := &EmailData{
To: data.ReceivingEmail,
Subject: subject,
Content: content,
Data: map[string]interface{}{
"company_name": data.CompanyName,
"amount": data.Amount,
"invoice_type": data.InvoiceType,
"file_url": data.FileURL,
"file_name": data.FileName,
"approved_at": data.ApprovedAt,
},
}
return s.SendEmail(ctx, emailData)
}
// buildEmailMessage 构建邮件消息
func (s *QQEmailService) buildEmailMessage(data *EmailData) string {
headers := make(map[string]string)
headers["From"] = s.config.FromEmail
headers["To"] = data.To
headers["Subject"] = data.Subject
headers["MIME-Version"] = "1.0"
headers["Content-Type"] = "text/html; charset=UTF-8"
var message strings.Builder
for key, value := range headers {
message.WriteString(fmt.Sprintf("%s: %s\r\n", key, value))
}
message.WriteString("\r\n")
message.WriteString(data.Content)
return message.String()
}
// buildInvoiceEmailContent 构建发票邮件内容
func (s *QQEmailService) buildInvoiceEmailContent(data *InvoiceEmailData) string {
htmlTemplate := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>发票开具成功通知</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', 'Microsoft YaHei', Arial, sans-serif;
line-height: 1.6;
color: #2d3748;
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 650px;
margin: 0 auto;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #e2e8f0;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 50px 40px 40px;
text-align: center;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%);
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(180deg); }
}
.success-icon {
font-size: 48px;
margin-bottom: 16px;
position: relative;
z-index: 1;
opacity: 0.9;
}
.header h1 {
font-size: 24px;
font-weight: 500;
margin: 0;
position: relative;
z-index: 1;
letter-spacing: 0.5px;
}
.content {
padding: 0;
}
.greeting {
padding: 40px 40px 20px;
text-align: center;
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
}
.greeting p {
font-size: 16px;
color: #4a5568;
margin-bottom: 8px;
font-weight: 400;
}
.access-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px;
text-align: center;
position: relative;
overflow: hidden;
margin: 0 20px 30px;
border-radius: 20px;
}
.access-section::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: shimmer 8s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { transform: translateX(-100%) translateY(-100%) rotate(0deg); }
50% { transform: translateX(100%) translateY(100%) rotate(180deg); }
}
.access-section h3 {
color: white;
font-size: 22px;
font-weight: 600;
margin-bottom: 12px;
position: relative;
z-index: 1;
}
.access-section p {
color: rgba(255, 255, 255, 0.9);
margin-bottom: 25px;
position: relative;
z-index: 1;
font-size: 15px;
}
.access-btn {
display: inline-block;
background: rgba(255, 255, 255, 0.15);
color: white;
padding: 16px 32px;
text-decoration: none;
border-radius: 50px;
font-weight: 600;
font-size: 15px;
border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
position: relative;
z-index: 1;
backdrop-filter: blur(10px);
letter-spacing: 0.3px;
}
.access-btn:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-3px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.info-section {
padding: 0 40px 40px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.info-item {
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
padding: 24px;
border-radius: 16px;
border: 1px solid #e2e8f0;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.info-item:hover {
transform: translateY(-2px);
box-shadow: 0 12px 25px -5px rgba(102, 126, 234, 0.1);
border-color: #cbd5e0;
}
.info-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 0 2px 2px 0;
}
.info-label {
font-weight: 600;
color: #718096;
display: block;
margin-bottom: 8px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.info-value {
color: #2d3748;
font-size: 16px;
font-weight: 500;
position: relative;
z-index: 1;
}
.notes-section {
background: linear-gradient(135deg, #f0fff4 0%, #ffffff 100%);
padding: 30px;
border-radius: 16px;
margin: 30px 0;
border: 1px solid #c6f6d5;
position: relative;
}
.notes-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
border-radius: 0 2px 2px 0;
}
.notes-section h4 {
color: #2f855a;
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.notes-section h4::before {
content: '📋';
margin-right: 8px;
font-size: 18px;
}
.notes-section ul {
list-style: none;
padding: 0;
}
.notes-section li {
color: #4a5568;
margin-bottom: 10px;
padding-left: 24px;
position: relative;
font-size: 14px;
}
.notes-section li::before {
content: '✓';
color: #48bb78;
font-weight: bold;
position: absolute;
left: 0;
font-size: 16px;
}
.footer {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
color: rgba(255, 255, 255, 0.8);
padding: 35px 40px;
text-align: center;
font-size: 14px;
}
.footer p {
margin-bottom: 8px;
line-height: 1.5;
}
.footer-divider {
width: 60px;
height: 2px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 20px auto;
border-radius: 1px;
}
@media (max-width: 600px) {
.container {
margin: 10px;
border-radius: 20px;
}
.header {
padding: 40px 30px 30px;
}
.greeting {
padding: 30px 30px 20px;
}
.access-section {
margin: 0 15px 25px;
padding: 30px 25px;
}
.info-section {
padding: 0 30px 30px;
}
.info-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.footer {
padding: 30px 30px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="success-icon">✓</div>
<h1>发票已开具完成</h1>
</div>
<div class="content">
<div class="greeting">
<p>尊敬的用户,您好!</p>
<p>您的发票申请已审核通过,发票已成功开具。</p>
</div>
<div class="access-section">
<h3>📄 发票访问链接</h3>
<p>您的发票已准备就绪,请点击下方按钮访问查看页面</p>
<a href="{{.FileURL}}" class="access-btn" target="_blank">
🔗 访问发票页面
</a>
</div>
<div class="info-section">
<div class="info-grid">
<div class="info-item">
<span class="info-label">公司名称</span>
<span class="info-value">{{.CompanyName}}</span>
</div>
<div class="info-item">
<span class="info-label">发票金额</span>
<span class="info-value">¥{{.Amount}}</span>
</div>
<div class="info-item">
<span class="info-label">发票类型</span>
<span class="info-value">{{.InvoiceType}}</span>
</div>
<div class="info-item">
<span class="info-label">开具时间</span>
<span class="info-value">{{.ApprovedAt}}</span>
</div>
</div>
<div class="notes-section">
<h4>注意事项</h4>
<ul>
<li>访问页面后可在页面内下载发票文件</li>
<li>请妥善保管发票文件,建议打印存档</li>
<li>如有疑问,请回到我们平台进行下载</li>
</ul>
</div>
</div>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复</p>
<div class="footer-divider"></div>
<p>天远数据 API 服务平台</p>
<p>发送时间:{{.CurrentTime}}</p>
</div>
</div>
</body>
</html>`
// 解析模板
tmpl, err := template.New("invoice_email").Parse(htmlTemplate)
if err != nil {
s.logger.Error("解析邮件模板失败", zap.Error(err))
return s.buildSimpleInvoiceEmail(data)
}
// 准备模板数据
templateData := struct {
CompanyName string
Amount string
InvoiceType string
FileURL string
FileName string
ApprovedAt string
CurrentTime string
Domain string
}{
CompanyName: data.CompanyName,
Amount: data.Amount,
InvoiceType: data.InvoiceType,
FileURL: data.FileURL,
FileName: data.FileName,
ApprovedAt: data.ApprovedAt,
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
Domain: s.config.Domain,
}
// 执行模板
var content strings.Builder
err = tmpl.Execute(&content, templateData)
if err != nil {
s.logger.Error("执行邮件模板失败", zap.Error(err))
return s.buildSimpleInvoiceEmail(data)
}
return content.String()
}
// buildSimpleInvoiceEmail 构建简单的发票邮件内容(备用方案)
func (s *QQEmailService) buildSimpleInvoiceEmail(data *InvoiceEmailData) string {
return fmt.Sprintf(`
发票开具成功通知
尊敬的用户,您好!
您的发票申请已审核通过,发票已成功开具。
发票信息:
- 公司名称:%s
- 发票金额:¥%s
- 发票类型:%s
- 开具时间:%s
发票文件下载链接:%s
文件名:%s
如有疑问请访问控制台查看详细信息https://%s
天远数据 API 服务平台
%s
`, data.CompanyName, data.Amount, data.InvoiceType, data.ApprovedAt, data.FileURL, data.FileName, s.config.Domain, time.Now().Format("2006-01-02 15:04:05"))
}
// sendSMTP 通过SMTP发送邮件
func (s *QQEmailService) sendSMTP(to, subject, message string) error {
// 构建认证信息
auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host)
// 构建收件人列表
toList := []string{to}
// 发送邮件
if s.config.UseSSL {
// QQ邮箱587端口使用STARTTLS465端口使用直接SSL
if s.config.Port == 587 {
// 使用STARTTLS (587端口)
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port))
if err != nil {
return fmt.Errorf("连接SMTP服务器失败: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, s.config.Host)
if err != nil {
return fmt.Errorf("创建SMTP客户端失败: %w", err)
}
defer client.Close()
// 启用STARTTLS
if err = client.StartTLS(&tls.Config{
ServerName: s.config.Host,
InsecureSkipVerify: false,
}); err != nil {
return fmt.Errorf("启用STARTTLS失败: %w", err)
}
// 认证
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP认证失败: %w", err)
}
// 设置发件人
if err = client.Mail(s.config.FromEmail); err != nil {
return fmt.Errorf("设置发件人失败: %w", err)
}
// 设置收件人
for _, recipient := range toList {
if err = client.Rcpt(recipient); err != nil {
return fmt.Errorf("设置收件人失败: %w", err)
}
}
// 发送邮件内容
writer, err := client.Data()
if err != nil {
return fmt.Errorf("准备发送邮件内容失败: %w", err)
}
defer writer.Close()
_, err = writer.Write([]byte(message))
if err != nil {
return fmt.Errorf("发送邮件内容失败: %w", err)
}
} else {
// 使用直接SSL连接 (465端口)
tlsConfig := &tls.Config{
ServerName: s.config.Host,
InsecureSkipVerify: false,
}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), tlsConfig)
if err != nil {
return fmt.Errorf("连接SMTP服务器失败: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, s.config.Host)
if err != nil {
return fmt.Errorf("创建SMTP客户端失败: %w", err)
}
defer client.Close()
// 认证
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP认证失败: %w", err)
}
// 设置发件人
if err = client.Mail(s.config.FromEmail); err != nil {
return fmt.Errorf("设置发件人失败: %w", err)
}
// 设置收件人
for _, recipient := range toList {
if err = client.Rcpt(recipient); err != nil {
return fmt.Errorf("设置收件人失败: %w", err)
}
}
// 发送邮件内容
writer, err := client.Data()
if err != nil {
return fmt.Errorf("准备发送邮件内容失败: %w", err)
}
defer writer.Close()
_, err = writer.Write([]byte(message))
if err != nil {
return fmt.Errorf("发送邮件内容失败: %w", err)
}
}
} else {
// 使用普通连接
err := smtp.SendMail(
fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
auth,
s.config.FromEmail,
toList,
[]byte(message),
)
if err != nil {
return fmt.Errorf("发送邮件失败: %w", err)
}
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/base64"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"time"
@@ -279,3 +280,56 @@ func (s *QiNiuStorageService) UploadFromReader(ctx context.Context, reader io.Re
return s.UploadFile(ctx, fileBytes, fileName)
}
// DownloadFile 从七牛云下载文件
func (s *QiNiuStorageService) DownloadFile(ctx context.Context, fileURL string) ([]byte, error) {
s.logger.Info("开始从七牛云下载文件", zap.String("file_url", fileURL))
// 创建HTTP客户端
client := &http.Client{
Timeout: 30 * time.Second,
}
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
// 发送请求
resp, err := client.Do(req)
if err != nil {
s.logger.Error("下载文件失败",
zap.String("file_url", fileURL),
zap.Error(err),
)
return nil, fmt.Errorf("下载文件失败: %w", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusOK {
s.logger.Error("下载文件失败,状态码异常",
zap.String("file_url", fileURL),
zap.Int("status_code", resp.StatusCode),
)
return nil, fmt.Errorf("下载文件失败,状态码: %d", resp.StatusCode)
}
// 读取文件内容
fileContent, err := io.ReadAll(resp.Body)
if err != nil {
s.logger.Error("读取文件内容失败",
zap.String("file_url", fileURL),
zap.Error(err),
)
return nil, fmt.Errorf("读取文件内容失败: %w", err)
}
s.logger.Info("文件下载成功",
zap.String("file_url", fileURL),
zap.Int("file_size", len(fileContent)),
)
return fileContent, nil
}

View File

@@ -130,7 +130,7 @@ func (h *ApiHandler) AddWhiteListIP(c *gin.Context) {
}
var req dto.WhiteListRequest
if err := c.ShouldBindJSON(&req); err != nil {
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
@@ -311,6 +311,86 @@ func (h *ApiHandler) GetUserApiCalls(c *gin.Context) {
h.responseBuilder.Success(c, result, "获取API调用记录成功")
}
// GetAdminApiCalls 获取管理端API调用记录
// @Summary 获取管理端API调用记录
// @Description 管理员获取API调用记录支持筛选和分页
// @Tags API管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID"
// @Param transaction_id query string false "交易ID"
// @Param product_name query string false "产品名称"
// @Param status query string false "状态"
// @Param start_time query string false "开始时间" format(date-time)
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.ApiCallListResponse "获取API调用记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/api-calls [get]
func (h *ApiHandler) GetAdminApiCalls(c *gin.Context) {
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 用户ID筛选
if userId := c.Query("user_id"); userId != "" {
filters["user_id"] = userId
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 交易ID筛选
if transactionId := c.Query("transaction_id"); transactionId != "" {
filters["transaction_id"] = transactionId
}
// 产品名称筛选
if productName := c.Query("product_name"); productName != "" {
filters["product_name"] = productName
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.appService.GetAdminApiCalls(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端API调用记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取API调用记录失败")
return
}
h.responseBuilder.Success(c, result, "获取API调用记录成功")
}
// getIntQuery 获取整数查询参数
func (h *ApiHandler) getIntQuery(c *gin.Context, key string, defaultValue int) int {
if value := c.Query(key); value != "" {

View File

@@ -17,24 +17,30 @@ import (
// FinanceHandler 财务HTTP处理器
type FinanceHandler struct {
appService finance.FinanceApplicationService
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
appService finance.FinanceApplicationService
invoiceAppService finance.InvoiceApplicationService
adminInvoiceAppService finance.AdminInvoiceApplicationService
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
}
// NewFinanceHandler 创建财务HTTP处理器
func NewFinanceHandler(
appService finance.FinanceApplicationService,
invoiceAppService finance.InvoiceApplicationService,
adminInvoiceAppService finance.AdminInvoiceApplicationService,
responseBuilder interfaces.ResponseBuilder,
validator interfaces.RequestValidator,
logger *zap.Logger,
) *FinanceHandler {
return &FinanceHandler{
appService: appService,
responseBuilder: responseBuilder,
validator: validator,
logger: logger,
appService: appService,
invoiceAppService: invoiceAppService,
adminInvoiceAppService: adminInvoiceAppService,
responseBuilder: responseBuilder,
validator: validator,
logger: logger,
}
}
@@ -554,3 +560,381 @@ func (h *FinanceHandler) GetAlipayOrderStatus(c *gin.Context) {
h.responseBuilder.Success(c, result, "获取订单状态成功")
}
// ==================== 发票相关Handler方法 ====================
// ApplyInvoice 申请开票
// @Summary 申请开票
// @Description 用户申请开票
// @Tags 发票管理
// @Accept json
// @Produce json
// @Param request body finance.ApplyInvoiceRequest true "申请开票请求"
// @Success 200 {object} response.Response{data=finance.InvoiceApplicationResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/invoices/apply [post]
func (h *FinanceHandler) ApplyInvoice(c *gin.Context) {
var req finance.ApplyInvoiceRequest
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
userID := c.GetString("user_id") // 从JWT中获取用户ID
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
result, err := h.invoiceAppService.ApplyInvoice(c.Request.Context(), userID, req)
if err != nil {
h.responseBuilder.InternalError(c, err.Error())
return
}
h.responseBuilder.Success(c, result, "申请开票成功")
}
// GetUserInvoiceInfo 获取用户发票信息
// @Summary 获取用户发票信息
// @Description 获取用户的发票信息
// @Tags 发票管理
// @Produce json
// @Success 200 {object} response.Response{data=finance.InvoiceInfoResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/invoices/info [get]
func (h *FinanceHandler) GetUserInvoiceInfo(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
result, err := h.invoiceAppService.GetUserInvoiceInfo(c.Request.Context(), userID)
if err != nil {
h.responseBuilder.InternalError(c, "获取发票信息失败")
return
}
h.responseBuilder.Success(c, result, "获取发票信息成功")
}
// UpdateUserInvoiceInfo 更新用户发票信息
// @Summary 更新用户发票信息
// @Description 更新用户的发票信息
// @Tags 发票管理
// @Accept json
// @Produce json
// @Param request body finance.UpdateInvoiceInfoRequest true "更新发票信息请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/invoices/info [put]
func (h *FinanceHandler) UpdateUserInvoiceInfo(c *gin.Context) {
var req finance.UpdateInvoiceInfoRequest
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
err := h.invoiceAppService.UpdateUserInvoiceInfo(c.Request.Context(), userID, req)
if err != nil {
h.responseBuilder.InternalError(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "更新发票信息成功")
}
// GetUserInvoiceRecords 获取用户开票记录
// @Summary 获取用户开票记录
// @Description 获取用户的开票记录列表
// @Tags 发票管理
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query string false "状态筛选"
// @Success 200 {object} response.Response{data=finance.InvoiceRecordsResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/invoices/records [get]
func (h *FinanceHandler) GetUserInvoiceRecords(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
status := c.Query("status")
startTime := c.Query("start_time")
endTime := c.Query("end_time")
req := finance.GetInvoiceRecordsRequest{
Page: page,
PageSize: pageSize,
Status: status,
StartTime: startTime,
EndTime: endTime,
}
result, err := h.invoiceAppService.GetUserInvoiceRecords(c.Request.Context(), userID, req)
if err != nil {
h.responseBuilder.InternalError(c, "获取开票记录失败")
return
}
h.responseBuilder.Success(c, result, "获取开票记录成功")
}
// DownloadInvoiceFile 下载发票文件
// @Summary 下载发票文件
// @Description 下载指定发票的文件
// @Tags 发票管理
// @Produce application/octet-stream
// @Param application_id path string true "申请ID"
// @Success 200 {file} file
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/invoices/{application_id}/download [get]
func (h *FinanceHandler) DownloadInvoiceFile(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
applicationID := c.Param("application_id")
if applicationID == "" {
h.responseBuilder.BadRequest(c, "申请ID不能为空")
return
}
result, err := h.invoiceAppService.DownloadInvoiceFile(c.Request.Context(), userID, applicationID)
if err != nil {
h.responseBuilder.InternalError(c, "下载发票文件失败")
return
}
// 设置响应头
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", result.FileName))
c.Header("Content-Length", fmt.Sprintf("%d", len(result.FileContent)))
// 直接返回文件内容
c.Data(http.StatusOK, "application/pdf", result.FileContent)
}
// GetAvailableAmount 获取可开票金额
// @Summary 获取可开票金额
// @Description 获取用户当前可开票的金额
// @Tags 发票管理
// @Produce json
// @Success 200 {object} response.Response{data=finance.AvailableAmountResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/invoices/available-amount [get]
func (h *FinanceHandler) GetAvailableAmount(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
result, err := h.invoiceAppService.GetAvailableAmount(c.Request.Context(), userID)
if err != nil {
h.responseBuilder.InternalError(c, "获取可开票金额失败")
return
}
h.responseBuilder.Success(c, result, "获取可开票金额成功")
}
// ==================== 管理员发票相关Handler方法 ====================
// GetPendingApplications 获取发票申请列表(支持筛选)
// @Summary 获取发票申请列表
// @Description 管理员获取发票申请列表,支持状态和时间范围筛选
// @Tags 管理员-发票管理
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query string false "状态筛选pending/completed/rejected"
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
// @Success 200 {object} response.Response{data=finance.PendingApplicationsResponse}
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/admin/invoices/pending [get]
func (h *FinanceHandler) GetPendingApplications(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
status := c.Query("status")
startTime := c.Query("start_time")
endTime := c.Query("end_time")
req := finance.GetPendingApplicationsRequest{
Page: page,
PageSize: pageSize,
Status: status,
StartTime: startTime,
EndTime: endTime,
}
result, err := h.adminInvoiceAppService.GetPendingApplications(c.Request.Context(), req)
if err != nil {
h.responseBuilder.InternalError(c, err.Error())
return
}
h.responseBuilder.Success(c, result, "获取发票申请列表成功")
}
// ApproveInvoiceApplication 通过发票申请(上传发票)
// @Summary 通过发票申请
// @Description 管理员通过发票申请并上传发票文件
// @Tags 管理员-发票管理
// @Accept multipart/form-data
// @Produce json
// @Param application_id path string true "申请ID"
// @Param file formData file true "发票文件"
// @Param admin_notes formData string false "管理员备注"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/admin/invoices/{application_id}/approve [post]
func (h *FinanceHandler) ApproveInvoiceApplication(c *gin.Context) {
applicationID := c.Param("application_id")
if applicationID == "" {
h.responseBuilder.BadRequest(c, "申请ID不能为空")
return
}
// 获取上传的文件
file, err := c.FormFile("file")
if err != nil {
h.responseBuilder.BadRequest(c, "请选择要上传的发票文件")
return
}
// 打开文件
fileHandle, err := file.Open()
if err != nil {
h.responseBuilder.InternalError(c, "文件打开失败")
return
}
defer fileHandle.Close()
// 获取管理员备注
adminNotes := c.PostForm("admin_notes")
req := finance.ApproveInvoiceRequest{
AdminNotes: adminNotes,
}
err = h.adminInvoiceAppService.ApproveInvoiceApplication(c.Request.Context(), applicationID, fileHandle, req)
if err != nil {
h.responseBuilder.InternalError(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "通过发票申请成功")
}
// RejectInvoiceApplication 拒绝发票申请
// @Summary 拒绝发票申请
// @Description 管理员拒绝发票申请
// @Tags 管理员-发票管理
// @Accept json
// @Produce json
// @Param application_id path string true "申请ID"
// @Param request body finance.RejectInvoiceRequest true "拒绝申请请求"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/admin/invoices/{application_id}/reject [post]
func (h *FinanceHandler) RejectInvoiceApplication(c *gin.Context) {
applicationID := c.Param("application_id")
if applicationID == "" {
h.responseBuilder.BadRequest(c, "申请ID不能为空")
return
}
var req finance.RejectInvoiceRequest
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
err := h.adminInvoiceAppService.RejectInvoiceApplication(c.Request.Context(), applicationID, req)
if err != nil {
h.responseBuilder.InternalError(c, err.Error())
return
}
h.responseBuilder.Success(c, nil, "拒绝发票申请成功")
}
// AdminDownloadInvoiceFile 管理员下载发票文件
// @Summary 管理员下载发票文件
// @Description 管理员下载指定发票的文件
// @Tags 管理员-发票管理
// @Produce application/octet-stream
// @Param application_id path string true "申请ID"
// @Success 200 {file} file
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /api/v1/admin/invoices/{application_id}/download [get]
func (h *FinanceHandler) AdminDownloadInvoiceFile(c *gin.Context) {
applicationID := c.Param("application_id")
if applicationID == "" {
h.responseBuilder.BadRequest(c, "申请ID不能为空")
return
}
result, err := h.adminInvoiceAppService.DownloadInvoiceFile(c.Request.Context(), applicationID)
if err != nil {
h.responseBuilder.InternalError(c, "下载发票文件失败")
return
}
// 设置响应头
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", result.FileName))
c.Header("Content-Length", fmt.Sprintf("%d", len(result.FileContent)))
// 直接返回文件内容
c.Data(http.StatusOK, "application/pdf", result.FileContent)
}
// DebugEventSystem 调试事件系统状态
// @Summary 调试事件系统状态
// @Description 获取事件系统的调试信息
// @Tags 调试
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/debug/events [get]
func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
h.logger.Info("🔍 请求事件系统调试信息")
// 这里可以添加事件系统的状态信息
// 暂时返回基本信息
debugInfo := map[string]interface{}{
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
"message": "事件系统调试端点已启用",
"handler": "FinanceHandler",
}
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
}

View File

@@ -2,6 +2,9 @@ package handlers
import (
"strconv"
"time"
"tyapi-server/internal/application/api"
"tyapi-server/internal/application/finance"
"tyapi-server/internal/application/product"
"tyapi-server/internal/application/product/dto/commands"
"tyapi-server/internal/application/product/dto/queries"
@@ -18,6 +21,8 @@ type ProductAdminHandler struct {
categoryAppService product.CategoryApplicationService
subscriptionAppService product.SubscriptionApplicationService
documentationAppService product.DocumentationApplicationServiceInterface
apiAppService api.ApiApplicationService
financeAppService finance.FinanceApplicationService
responseBuilder interfaces.ResponseBuilder
validator interfaces.RequestValidator
logger *zap.Logger
@@ -29,6 +34,8 @@ func NewProductAdminHandler(
categoryAppService product.CategoryApplicationService,
subscriptionAppService product.SubscriptionApplicationService,
documentationAppService product.DocumentationApplicationServiceInterface,
apiAppService api.ApiApplicationService,
financeAppService finance.FinanceApplicationService,
responseBuilder interfaces.ResponseBuilder,
validator interfaces.RequestValidator,
logger *zap.Logger,
@@ -38,6 +45,8 @@ func NewProductAdminHandler(
categoryAppService: categoryAppService,
subscriptionAppService: subscriptionAppService,
documentationAppService: documentationAppService,
apiAppService: apiAppService,
financeAppService: financeAppService,
responseBuilder: responseBuilder,
validator: validator,
logger: logger,
@@ -710,7 +719,13 @@ func (h *ProductAdminHandler) GetCategoryDetail(c *gin.Context) {
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query string false "订阅状态"
// @Param keyword query string false "搜索关键词"
// @Param company_name query string false "企业名称"
// @Param product_name query string false "产品名称"
// @Param start_time query string false "订阅开始时间" format(date-time)
// @Param end_time query string false "订阅结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} responses.SubscriptionListResponse "获取订阅列表成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
@@ -719,7 +734,7 @@ func (h *ProductAdminHandler) GetCategoryDetail(c *gin.Context) {
func (h *ProductAdminHandler) ListSubscriptions(c *gin.Context) {
var query queries.ListSubscriptionsQuery
if err := c.ShouldBindQuery(&query); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
h.responseBuilder.BadRequest(c, err.Error())
return
}
@@ -734,6 +749,14 @@ func (h *ProductAdminHandler) ListSubscriptions(c *gin.Context) {
query.PageSize = 100
}
// 设置默认排序
if query.SortBy == "" {
query.SortBy = "created_at"
}
if query.SortOrder == "" {
query.SortOrder = "desc"
}
result, err := h.subscriptionAppService.ListSubscriptions(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取订阅列表失败", zap.Error(err))
@@ -1053,3 +1076,251 @@ func (h *ProductAdminHandler) DeleteProductDocumentation(c *gin.Context) {
h.responseBuilder.Success(c, nil, "文档删除成功")
}
// GetAdminApiCalls 获取管理端API调用记录
// @Summary 获取管理端API调用记录
// @Description 管理员获取API调用记录支持筛选和分页
// @Tags API管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID"
// @Param transaction_id query string false "交易ID"
// @Param product_name query string false "产品名称"
// @Param status query string false "状态"
// @Param start_time query string false "开始时间" format(date-time)
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.ApiCallListResponse "获取API调用记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/api-calls [get]
func (h *ProductAdminHandler) GetAdminApiCalls(c *gin.Context) {
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 用户ID筛选
if userId := c.Query("user_id"); userId != "" {
filters["user_id"] = userId
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 交易ID筛选
if transactionId := c.Query("transaction_id"); transactionId != "" {
filters["transaction_id"] = transactionId
}
// 产品名称筛选
if productName := c.Query("product_name"); productName != "" {
filters["product_name"] = productName
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.apiAppService.GetAdminApiCalls(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端API调用记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取API调用记录失败")
return
}
h.responseBuilder.Success(c, result, "获取API调用记录成功")
}
// GetAdminWalletTransactions 获取管理端消费记录
// @Summary 获取管理端消费记录
// @Description 管理员获取消费记录,支持筛选和分页
// @Tags 财务管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID"
// @Param transaction_id query string false "交易ID"
// @Param product_name query string false "产品名称"
// @Param min_amount query string false "最小金额"
// @Param max_amount query string false "最大金额"
// @Param start_time query string false "开始时间" format(date-time)
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.WalletTransactionListResponse "获取消费记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/wallet-transactions [get]
func (h *ProductAdminHandler) GetAdminWalletTransactions(c *gin.Context) {
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 用户ID筛选
if userId := c.Query("user_id"); userId != "" {
filters["user_id"] = userId
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 交易ID筛选
if transactionId := c.Query("transaction_id"); transactionId != "" {
filters["transaction_id"] = transactionId
}
// 产品名称筛选
if productName := c.Query("product_name"); productName != "" {
filters["product_name"] = productName
}
// 金额范围筛选
if minAmount := c.Query("min_amount"); minAmount != "" {
filters["min_amount"] = minAmount
}
if maxAmount := c.Query("max_amount"); maxAmount != "" {
filters["max_amount"] = maxAmount
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.financeAppService.GetAdminWalletTransactions(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端消费记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取消费记录失败")
return
}
h.responseBuilder.Success(c, result, "获取消费记录成功")
}
// GetAdminRechargeRecords 获取管理端充值记录
// @Summary 获取管理端充值记录
// @Description 管理员获取充值记录,支持筛选和分页
// @Tags 财务管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID"
// @Param recharge_type query string false "充值类型" Enums(alipay, transfer, gift)
// @Param status query string false "状态" Enums(pending, success, failed)
// @Param min_amount query string false "最小金额"
// @Param max_amount query string false "最大金额"
// @Param start_time query string false "开始时间" format(date-time)
// @Param end_time query string false "结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} dto.RechargeRecordListResponse "获取充值记录成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/recharge-records [get]
func (h *ProductAdminHandler) GetAdminRechargeRecords(c *gin.Context) {
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 用户ID筛选
if userId := c.Query("user_id"); userId != "" {
filters["user_id"] = userId
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 充值类型筛选
if rechargeType := c.Query("recharge_type"); rechargeType != "" {
filters["recharge_type"] = rechargeType
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 金额范围筛选
if minAmount := c.Query("min_amount"); minAmount != "" {
filters["min_amount"] = minAmount
}
if maxAmount := c.Query("max_amount"); maxAmount != "" {
filters["max_amount"] = maxAmount
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.financeAppService.GetAdminRechargeRecords(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端充值记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取充值记录失败")
return
}
h.responseBuilder.Success(c, result, "获取充值记录成功")
}

View File

@@ -415,7 +415,10 @@ func (h *ProductHandler) GetCategoryDetail(c *gin.Context) {
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param status query string false "订阅状态"
// @Param keyword query string false "搜索关键词"
// @Param product_name query string false "产品名称"
// @Param start_time query string false "订阅开始时间" format(date-time)
// @Param end_time query string false "订阅结束时间" format(date-time)
// @Param sort_by query string false "排序字段"
// @Param sort_order query string false "排序方向" Enums(asc, desc)
// @Success 200 {object} responses.SubscriptionListResponse "获取订阅列表成功"
@@ -432,7 +435,7 @@ func (h *ProductHandler) ListMySubscriptions(c *gin.Context) {
var query queries.ListSubscriptionsQuery
if err := h.validator.ValidateQuery(c, &query); err != nil {
return
return
}
// 设置默认值
@@ -446,6 +449,17 @@ func (h *ProductHandler) ListMySubscriptions(c *gin.Context) {
query.PageSize = 100
}
// 设置默认排序
if query.SortBy == "" {
query.SortBy = "created_at"
}
if query.SortOrder == "" {
query.SortOrder = "desc"
}
// 用户端不支持企业名称筛选,清空该字段
query.CompanyName = ""
result, err := h.subAppService.ListMySubscriptions(c.Request.Context(), userID, &query)
if err != nil {
h.logger.Error("获取我的订阅列表失败", zap.Error(err), zap.String("user_id", userID))
@@ -521,6 +535,13 @@ func (h *ProductHandler) GetMySubscriptionDetail(c *gin.Context) {
return
}
// 验证订阅是否属于当前用户
if result.UserID != userID {
h.logger.Error("用户尝试访问不属于自己的订阅", zap.String("user_id", userID), zap.String("subscription_user_id", result.UserID), zap.String("subscription_id", subscriptionID))
h.responseBuilder.Forbidden(c, "无权访问此订阅")
return
}
h.responseBuilder.Success(c, result, "获取我的订阅详情成功")
}
@@ -539,16 +560,33 @@ func (h *ProductHandler) GetMySubscriptionDetail(c *gin.Context) {
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/my/subscriptions/{id}/usage [get]
func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
subscriptionID := c.Param("id")
if subscriptionID == "" {
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
return
}
// 获取当前用户ID
userID := h.getCurrentUserID(c)
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未认证")
// 获取订阅信息以验证权限
var query queries.GetSubscriptionQuery
query.ID = subscriptionID
subscription, err := h.subAppService.GetSubscriptionByID(c.Request.Context(), &query)
if err != nil {
h.logger.Error("获取订阅信息失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
h.responseBuilder.NotFound(c, "订阅不存在")
return
}
// 验证订阅是否属于当前用户
if subscription.UserID != userID {
h.logger.Error("用户尝试访问不属于自己的订阅使用情况", zap.String("user_id", userID), zap.String("subscription_user_id", subscription.UserID), zap.String("subscription_id", subscriptionID))
h.responseBuilder.Forbidden(c, "无权访问此订阅")
return
}

View File

@@ -322,6 +322,46 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
h.response.Success(c, resp, "获取用户列表成功")
}
// GetUserDetail 管理员获取用户详情
// @Summary 管理员获取用户详情
// @Description 管理员获取指定用户的详细信息
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param user_id path string true "用户ID"
// @Success 200 {object} responses.UserDetailResponse "用户详情"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 403 {object} map[string]interface{} "权限不足"
// @Failure 404 {object} map[string]interface{} "用户不存在"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/users/admin/{user_id} [get]
func (h *UserHandler) GetUserDetail(c *gin.Context) {
// 检查管理员权限
userID := h.getCurrentUserID(c)
if userID == "" {
h.response.Unauthorized(c, "用户未登录")
return
}
// 获取路径参数中的用户ID
targetUserID := c.Param("user_id")
if targetUserID == "" {
h.response.BadRequest(c, "用户ID不能为空")
return
}
// 调用应用服务
resp, err := h.appService.GetUserDetail(c.Request.Context(), targetUserID)
if err != nil {
h.logger.Error("获取用户详情失败", zap.Error(err), zap.String("target_user_id", targetUserID))
h.response.BadRequest(c, "获取用户详情失败")
return
}
h.response.Success(c, resp, "获取用户详情成功")
}
// GetUserStats 管理员获取用户统计信息
// @Summary 管理员获取用户统计信息
// @Description 管理员获取用户统计信息,包括总用户数、活跃用户数、已认证用户数

View File

@@ -58,6 +58,18 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
}
}
// 发票相关路由,需要用户认证
invoiceGroup := engine.Group("/api/v1/invoices")
invoiceGroup.Use(r.authMiddleware.Handle())
{
invoiceGroup.POST("/apply", r.financeHandler.ApplyInvoice) // 申请开票
invoiceGroup.GET("/info", r.financeHandler.GetUserInvoiceInfo) // 获取用户发票信息
invoiceGroup.PUT("/info", r.financeHandler.UpdateUserInvoiceInfo) // 更新用户发票信息
invoiceGroup.GET("/records", r.financeHandler.GetUserInvoiceRecords) // 获取用户开票记录
invoiceGroup.GET("/available-amount", r.financeHandler.GetAvailableAmount) // 获取可开票金额
invoiceGroup.GET("/:application_id/download", r.financeHandler.DownloadInvoiceFile) // 下载发票文件
}
// 管理员财务路由组
adminFinanceGroup := engine.Group("/api/v1/admin/finance")
adminFinanceGroup.Use(r.adminAuthMiddleware.Handle())
@@ -67,5 +79,15 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
}
// 管理员发票相关路由组
adminInvoiceGroup := engine.Group("/api/v1/admin/invoices")
adminInvoiceGroup.Use(r.adminAuthMiddleware.Handle())
{
adminInvoiceGroup.GET("/pending", r.financeHandler.GetPendingApplications) // 获取待处理申请列表
adminInvoiceGroup.POST("/:application_id/approve", r.financeHandler.ApproveInvoiceApplication) // 通过发票申请
adminInvoiceGroup.POST("/:application_id/reject", r.financeHandler.RejectInvoiceApplication) // 拒绝发票申请
adminInvoiceGroup.GET("/:application_id/download", r.financeHandler.AdminDownloadInvoiceFile) // 下载发票文件
}
r.logger.Info("财务路由注册完成")
}

View File

@@ -79,5 +79,23 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
subscriptions.GET("/stats", r.handler.GetSubscriptionStats)
subscriptions.PUT("/:id/price", r.handler.UpdateSubscriptionPrice)
}
// API调用记录管理
apiCalls := adminGroup.Group("/api-calls")
{
apiCalls.GET("", r.handler.GetAdminApiCalls)
}
// 消费记录管理
walletTransactions := adminGroup.Group("/wallet-transactions")
{
walletTransactions.GET("", r.handler.GetAdminWalletTransactions)
}
// 充值记录管理
rechargeRecords := adminGroup.Group("/recharge-records")
{
rechargeRecords.GET("", r.handler.GetAdminRechargeRecords)
}
}
}

View File

@@ -57,6 +57,7 @@ func (r *UserRoutes) Register(router *sharedhttp.GinRouter) {
adminGroup.Use(r.adminAuthMiddleware.Handle())
{
adminGroup.GET("/list", r.handler.ListUsers) // 管理员查看用户列表
adminGroup.GET("/:user_id", r.handler.GetUserDetail) // 管理员获取用户详情
adminGroup.GET("/stats", r.handler.GetUserStats) // 管理员获取用户统计信息
}
}

View File

@@ -119,36 +119,80 @@ func (bus *MemoryEventBus) Stop(ctx context.Context) error {
// Publish 发布事件(同步)
func (bus *MemoryEventBus) Publish(ctx context.Context, event interfaces.Event) error {
bus.logger.Info("📤 开始发布事件",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
zap.String("aggregate_id", event.GetAggregateID()),
)
bus.mutex.RLock()
handlers := bus.subscribers[event.GetType()]
bus.mutex.RUnlock()
if len(handlers) == 0 {
bus.logger.Debug("No handlers for event type", zap.String("type", event.GetType()))
bus.logger.Warn("⚠️ 没有找到事件处理器",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
)
return nil
}
for _, handler := range handlers {
bus.logger.Info("📋 找到事件处理器",
zap.String("event_type", event.GetType()),
zap.Int("handler_count", len(handlers)),
)
for i, handler := range handlers {
bus.logger.Info("🔄 处理事件",
zap.String("event_type", event.GetType()),
zap.String("handler_name", handler.GetName()),
zap.Int("handler_index", i),
zap.Bool("is_async", handler.IsAsync()),
)
if handler.IsAsync() {
// 异步处理
select {
case bus.eventQueue <- eventTask{event: event, handler: handler, retries: 0}:
bus.logger.Info("✅ 事件已加入异步队列",
zap.String("event_type", event.GetType()),
zap.String("handler_name", handler.GetName()),
zap.Int("queue_length", len(bus.eventQueue)),
)
default:
bus.logger.Warn("Event queue is full, dropping event",
zap.String("type", event.GetType()),
zap.String("handler", handler.GetName()))
bus.logger.Error("❌ 事件队列已满,丢弃事件",
zap.String("event_type", event.GetType()),
zap.String("handler_name", handler.GetName()),
zap.Int("queue_length", len(bus.eventQueue)),
zap.Int("queue_capacity", cap(bus.eventQueue)),
)
}
} else {
// 同步处理
bus.logger.Info("⚡ 开始同步处理事件",
zap.String("event_type", event.GetType()),
zap.String("handler_name", handler.GetName()),
)
if err := bus.handleEventWithRetry(ctx, event, handler); err != nil {
bus.logger.Error("Failed to handle event synchronously",
zap.String("type", event.GetType()),
zap.String("handler", handler.GetName()),
zap.Error(err))
bus.logger.Error("❌ 同步处理事件失败",
zap.String("event_type", event.GetType()),
zap.String("handler_name", handler.GetName()),
zap.Error(err),
)
} else {
bus.logger.Info("✅ 同步处理事件成功",
zap.String("event_type", event.GetType()),
zap.String("handler_name", handler.GetName()),
)
}
}
}
bus.logger.Info("✅ 事件发布完成",
zap.String("event_type", event.GetType()),
zap.String("event_id", event.GetID()),
)
return nil
}
@@ -221,14 +265,19 @@ func (bus *MemoryEventBus) GetSubscribers(eventType string) []interfaces.EventHa
// worker 工作协程
func (bus *MemoryEventBus) worker(id int) {
bus.logger.Debug("Event worker started", zap.Int("worker_id", id))
bus.logger.Info("👷 事件工作协程启动", zap.Int("worker_id", id))
for {
select {
case task := <-bus.eventQueue:
bus.logger.Info("📥 工作协程接收到事件任务",
zap.Int("worker_id", id),
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
)
bus.processEventTask(task)
case <-bus.stopCh:
bus.logger.Debug("Event worker stopped", zap.Int("worker_id", id))
bus.logger.Info("🛑 事件工作协程停止", zap.Int("worker_id", id))
return
}
}
@@ -238,8 +287,20 @@ func (bus *MemoryEventBus) worker(id int) {
func (bus *MemoryEventBus) processEventTask(task eventTask) {
ctx := context.Background()
bus.logger.Info("🔧 开始处理事件任务",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
zap.Int("retries", task.retries),
)
err := bus.handleEventWithRetry(ctx, task.event, task.handler)
if err != nil {
bus.logger.Error("❌ 事件任务处理失败",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
zap.Error(err),
)
retryConfig := task.handler.GetRetryConfig()
if task.retries < retryConfig.MaxRetries {
@@ -251,26 +312,46 @@ func (bus *MemoryEventBus) processEventTask(task eventTask) {
delay = retryConfig.MaxDelay
}
bus.logger.Info("🔄 准备重试事件任务",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
zap.Int("retries", task.retries),
zap.Int("max_retries", retryConfig.MaxRetries),
zap.Duration("delay", delay),
)
go func() {
time.Sleep(delay)
task.retries++
select {
case bus.eventQueue <- task:
bus.logger.Info("✅ 事件任务重新加入队列",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
zap.Int("retries", task.retries),
)
default:
bus.logger.Error("Failed to requeue event for retry",
zap.String("type", task.event.GetType()),
zap.String("handler", task.handler.GetName()),
zap.Int("retries", task.retries))
bus.logger.Error("❌ 事件队列已满,无法重新加入任务",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
zap.Int("retries", task.retries),
)
}
}()
} else {
bus.logger.Error("Event processing failed after max retries",
zap.String("type", task.event.GetType()),
zap.String("handler", task.handler.GetName()),
bus.logger.Error("💥 事件处理失败,已达到最大重试次数",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
zap.Int("retries", task.retries),
zap.Error(err))
zap.Error(err),
)
}
} else {
bus.logger.Info("✅ 事件任务处理成功",
zap.String("event_type", task.event.GetType()),
zap.String("handler_name", task.handler.GetName()),
)
}
}