v1.0.0
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user