v1.0.0
This commit is contained in:
@@ -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{},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
121
internal/application/finance/dto/invoice_responses.go
Normal file
121
internal/application/finance/dto/invoice_responses.go
Normal 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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
750
internal/application/finance/invoice_application_service.go
Normal file
750
internal/application/finance/invoice_application_service.go
Normal 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
|
||||
}
|
||||
@@ -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 获取订阅详情查询
|
||||
|
||||
@@ -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:"创建时间"`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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("所有事件处理器已注册")
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"` // 子接口响应列表
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -43,4 +43,4 @@ func ProcessFLXG54F5Request(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
163
internal/domains/finance/entities/invoice_application.go
Normal file
163
internal/domains/finance/entities/invoice_application.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"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,
|
||||
)
|
||||
}
|
||||
71
internal/domains/finance/entities/user_invoice_info.go
Normal file
71
internal/domains/finance/entities/user_invoice_info.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserInvoiceInfo 用户开票信息实体
|
||||
type UserInvoiceInfo struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
UserID string `gorm:"uniqueIndex;type:varchar(36);not null" json:"user_id"`
|
||||
|
||||
// 开票信息字段
|
||||
CompanyName string `gorm:"type:varchar(200);not null" json:"company_name"` // 公司名称
|
||||
TaxpayerID string `gorm:"type:varchar(50);not null" json:"taxpayer_id"` // 纳税人识别号
|
||||
BankName string `gorm:"type:varchar(100)" json:"bank_name"` // 开户银行
|
||||
BankAccount string `gorm:"type:varchar(50)" json:"bank_account"` // 银行账号
|
||||
CompanyAddress string `gorm:"type:varchar(500)" json:"company_address"` // 企业地址
|
||||
CompanyPhone string `gorm:"type:varchar(20)" json:"company_phone"` // 企业电话
|
||||
ReceivingEmail string `gorm:"type:varchar(100);not null" json:"receiving_email"` // 发票接收邮箱
|
||||
|
||||
// 元数据
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (UserInvoiceInfo) TableName() string {
|
||||
return "user_invoice_info"
|
||||
}
|
||||
|
||||
// IsComplete 检查开票信息是否完整
|
||||
func (u *UserInvoiceInfo) IsComplete() bool {
|
||||
return u.CompanyName != "" && u.TaxpayerID != "" && u.ReceivingEmail != ""
|
||||
}
|
||||
|
||||
// IsCompleteForSpecialInvoice 检查专票信息是否完整
|
||||
func (u *UserInvoiceInfo) IsCompleteForSpecialInvoice() bool {
|
||||
return u.CompanyName != "" && u.TaxpayerID != "" && u.BankName != "" &&
|
||||
u.BankAccount != "" && u.CompanyAddress != "" && u.CompanyPhone != "" &&
|
||||
u.ReceivingEmail != ""
|
||||
}
|
||||
|
||||
// GetMissingFields 获取缺失的字段
|
||||
func (u *UserInvoiceInfo) GetMissingFields() []string {
|
||||
var missing []string
|
||||
if u.CompanyName == "" {
|
||||
missing = append(missing, "公司名称")
|
||||
}
|
||||
if u.TaxpayerID == "" {
|
||||
missing = append(missing, "纳税人识别号")
|
||||
}
|
||||
if u.BankName == "" {
|
||||
missing = append(missing, "开户银行")
|
||||
}
|
||||
if u.BankAccount == "" {
|
||||
missing = append(missing, "银行账号")
|
||||
}
|
||||
if u.CompanyAddress == "" {
|
||||
missing = append(missing, "企业地址")
|
||||
}
|
||||
if u.CompanyPhone == "" {
|
||||
missing = append(missing, "企业电话")
|
||||
}
|
||||
if u.ReceivingEmail == "" {
|
||||
missing = append(missing, "发票接收邮箱")
|
||||
}
|
||||
return missing
|
||||
}
|
||||
213
internal/domains/finance/events/invoice_events.go
Normal file
213
internal/domains/finance/events/invoice_events.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
277
internal/domains/finance/services/invoice_aggregate_service.go
Normal file
277
internal/domains/finance/services/invoice_aggregate_service.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
152
internal/domains/finance/services/invoice_domain_service.go
Normal file
152
internal/domains/finance/services/invoice_domain_service.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
250
internal/domains/finance/services/user_invoice_info_service.go
Normal file
250
internal/domains/finance/services/user_invoice_info_service.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
105
internal/domains/finance/value_objects/invoice_info.go
Normal file
105
internal/domains/finance/value_objects/invoice_info.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package value_objects
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InvoiceInfo 发票信息值对象
|
||||
type InvoiceInfo struct {
|
||||
CompanyName string `json:"company_name"` // 公司名称
|
||||
TaxpayerID string `json:"taxpayer_id"` // 纳税人识别号
|
||||
BankName string `json:"bank_name"` // 基本开户银行
|
||||
BankAccount string `json:"bank_account"` // 基本开户账号
|
||||
CompanyAddress string `json:"company_address"` // 企业注册地址
|
||||
CompanyPhone string `json:"company_phone"` // 企业注册电话
|
||||
ReceivingEmail string `json:"receiving_email"` // 发票接收邮箱
|
||||
}
|
||||
|
||||
// NewInvoiceInfo 创建发票信息值对象
|
||||
func NewInvoiceInfo(companyName, taxpayerID, bankName, bankAccount, companyAddress, companyPhone, receivingEmail string) *InvoiceInfo {
|
||||
return &InvoiceInfo{
|
||||
CompanyName: strings.TrimSpace(companyName),
|
||||
TaxpayerID: strings.TrimSpace(taxpayerID),
|
||||
BankName: strings.TrimSpace(bankName),
|
||||
BankAccount: strings.TrimSpace(bankAccount),
|
||||
CompanyAddress: strings.TrimSpace(companyAddress),
|
||||
CompanyPhone: strings.TrimSpace(companyPhone),
|
||||
ReceivingEmail: strings.TrimSpace(receivingEmail),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateForGeneralInvoice 验证普票信息
|
||||
func (ii *InvoiceInfo) ValidateForGeneralInvoice() error {
|
||||
if ii.CompanyName == "" {
|
||||
return errors.New("公司名称不能为空")
|
||||
}
|
||||
if ii.TaxpayerID == "" {
|
||||
return errors.New("纳税人识别号不能为空")
|
||||
}
|
||||
if ii.ReceivingEmail == "" {
|
||||
return errors.New("发票接收邮箱不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateForSpecialInvoice 验证专票信息
|
||||
func (ii *InvoiceInfo) ValidateForSpecialInvoice() error {
|
||||
// 先验证普票必填项
|
||||
if err := ii.ValidateForGeneralInvoice(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 专票额外必填项
|
||||
if ii.BankName == "" {
|
||||
return errors.New("基本开户银行不能为空")
|
||||
}
|
||||
if ii.BankAccount == "" {
|
||||
return errors.New("基本开户账号不能为空")
|
||||
}
|
||||
if ii.CompanyAddress == "" {
|
||||
return errors.New("企业注册地址不能为空")
|
||||
}
|
||||
if ii.CompanyPhone == "" {
|
||||
return errors.New("企业注册电话不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsComplete 检查信息是否完整(专票要求)
|
||||
func (ii *InvoiceInfo) IsComplete() bool {
|
||||
return ii.CompanyName != "" &&
|
||||
ii.TaxpayerID != "" &&
|
||||
ii.BankName != "" &&
|
||||
ii.BankAccount != "" &&
|
||||
ii.CompanyAddress != "" &&
|
||||
ii.CompanyPhone != "" &&
|
||||
ii.ReceivingEmail != ""
|
||||
}
|
||||
|
||||
// GetMissingFields 获取缺失的字段(专票要求)
|
||||
func (ii *InvoiceInfo) GetMissingFields() []string {
|
||||
var missing []string
|
||||
if ii.CompanyName == "" {
|
||||
missing = append(missing, "公司名称")
|
||||
}
|
||||
if ii.TaxpayerID == "" {
|
||||
missing = append(missing, "纳税人识别号")
|
||||
}
|
||||
if ii.BankName == "" {
|
||||
missing = append(missing, "基本开户银行")
|
||||
}
|
||||
if ii.BankAccount == "" {
|
||||
missing = append(missing, "基本开户账号")
|
||||
}
|
||||
if ii.CompanyAddress == "" {
|
||||
missing = append(missing, "企业注册地址")
|
||||
}
|
||||
if ii.CompanyPhone == "" {
|
||||
missing = append(missing, "企业注册电话")
|
||||
}
|
||||
if ii.ReceivingEmail == "" {
|
||||
missing = append(missing, "发票接收邮箱")
|
||||
}
|
||||
return missing
|
||||
}
|
||||
36
internal/domains/finance/value_objects/invoice_type.go
Normal file
36
internal/domains/finance/value_objects/invoice_type.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package value_objects
|
||||
|
||||
// InvoiceType 发票类型枚举
|
||||
type InvoiceType string
|
||||
|
||||
const (
|
||||
InvoiceTypeGeneral InvoiceType = "general" // 增值税普通发票 (普票)
|
||||
InvoiceTypeSpecial InvoiceType = "special" // 增值税专用发票 (专票)
|
||||
)
|
||||
|
||||
// String 返回发票类型的字符串表示
|
||||
func (it InvoiceType) String() string {
|
||||
return string(it)
|
||||
}
|
||||
|
||||
// IsValid 验证发票类型是否有效
|
||||
func (it InvoiceType) IsValid() bool {
|
||||
switch it {
|
||||
case InvoiceTypeGeneral, InvoiceTypeSpecial:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetDisplayName 获取发票类型的显示名称
|
||||
func (it InvoiceType) GetDisplayName() string {
|
||||
switch it {
|
||||
case InvoiceTypeGeneral:
|
||||
return "增值税普通发票 (普票)"
|
||||
case InvoiceTypeSpecial:
|
||||
return "增值税专用发票 (专票)"
|
||||
default:
|
||||
return "未知类型"
|
||||
}
|
||||
}
|
||||
@@ -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 获取订阅详情查询
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 返回订阅总数
|
||||
|
||||
230
internal/infrastructure/events/invoice_event_handler.go
Normal file
230
internal/infrastructure/events/invoice_event_handler.go
Normal 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
|
||||
}
|
||||
|
||||
|
||||
115
internal/infrastructure/events/invoice_event_publisher.go
Normal file
115
internal/infrastructure/events/invoice_event_publisher.go
Normal 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
|
||||
}
|
||||
712
internal/infrastructure/external/email/qq_email_service.go
vendored
Normal file
712
internal/infrastructure/external/email/qq_email_service.go
vendored
Normal 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端口使用STARTTLS,465端口使用直接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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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, "事件系统调试信息")
|
||||
}
|
||||
|
||||
@@ -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, "获取充值记录成功")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 管理员获取用户统计信息,包括总用户数、活跃用户数、已认证用户数
|
||||
|
||||
@@ -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("财务路由注册完成")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) // 管理员获取用户统计信息
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user