Compare commits

..

30 Commits

Author SHA1 Message Date
8098c13de3 f 2026-01-06 17:06:33 +08:00
e61f03a2dd add 极光数据 2026-01-06 16:37:31 +08:00
4fcf370dcd fix 2026-01-05 17:19:55 +08:00
b0ed5b04ee fix add 2026-01-05 16:41:05 +08:00
269ff38604 fix add 2026-01-05 16:36:50 +08:00
23909c44f1 Resolve merge conflicts 2026-01-05 14:34:58 +08:00
aabe34b7d5 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-05 14:31:11 +08:00
c262894372 add 全国企业司法模型服务查询_V1 2026-01-05 14:26:26 +08:00
39f799bc41 fix 2025-12-31 18:06:59 +08:00
1d4411a940 add 2025-12-31 17:46:03 +08:00
fe44b452e3 新增:极光config 2025-12-31 16:12:09 +08:00
f1ec9bfe7f 新增:新增极光源,极光婚姻接口(测试) 2025-12-31 15:42:05 +08:00
a70e736cdd fix 2025-12-27 19:18:12 +08:00
53d2c75a9c 更改:给yysy09cd无缝切换换源 2025-12-26 18:48:53 +08:00
0bfa7b4f50 移除王起航 2025-12-26 14:41:31 +08:00
e2e729eec8 add 人脸123 2025-12-25 18:25:37 +08:00
5f7fb43804 fix 2025-12-25 12:40:40 +08:00
89c5c0f9ad 1 2025-12-24 17:52:51 +08:00
6c949a4a1c fix id_car 2025-12-24 17:52:33 +08:00
8556e7331d add fix id_car 2025-12-24 17:50:33 +08:00
311d7a9b01 fix 1 2025-12-24 12:32:25 +08:00
ce45ce3ed0 fix 2025-12-23 18:54:38 +08:00
34e2c1bc41 fixdele 2025-12-23 17:17:41 +08:00
2618105140 fix 2025-12-23 16:16:45 +08:00
6b41f3833a fix 2025-12-23 15:04:53 +08:00
446a5c7661 fix 2025-12-23 11:26:25 +08:00
7f8554fa12 add购买记录功能 2025-12-22 18:32:34 +08:00
65a61d0336 fix2 2025-12-19 18:16:22 +08:00
8dd6f71baf fix 2025-12-19 18:13:31 +08:00
e96653751d fix 2025-12-19 17:50:29 +08:00
380 changed files with 7670 additions and 83810 deletions

5
.gitignore vendored
View File

@@ -40,8 +40,13 @@ internal/shared/pdf/fonts/*.ttf
internal/shared/pdf/fonts/*.ttc internal/shared/pdf/fonts/*.ttc
internal/shared/pdf/fonts/*.otf internal/shared/pdf/fonts/*.otf
# Pure Component 目录(用于持久化存储,不进行版本控制)
resources/Pure_Component/
# 其他 # 其他
*.exe *.exe
*.exe*
*.dll *.dll
*.so *.so
*.dylib *.dylib
cmd/api/__debug_bin*

View File

@@ -54,7 +54,8 @@ COPY config.yaml .
COPY configs/ ./configs/ COPY configs/ ./configs/
# 复制资源文件(直接从构建上下文复制,与配置文件一致) # 复制资源文件(直接从构建上下文复制,与配置文件一致)
COPY resources ./resources COPY resources/etc ./resources/etc
COPY resources/pdf ./resources/pdf
# 暴露端口 # 暴露端口
EXPOSE 8080 EXPOSE 8080

View File

@@ -124,7 +124,7 @@ sms:
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9" access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65" access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
endpoint_url: "dysmsapi.aliyuncs.com" endpoint_url: "dysmsapi.aliyuncs.com"
sign_name: "天远查" sign_name: "海南海宇大数据"
template_code: "SMS_302641455" template_code: "SMS_302641455"
code_length: 6 code_length: 6
expire_time: 5m expire_time: 5m
@@ -437,7 +437,7 @@ zhicha:
# 🌐 木子数据配置 # 🌐 木子数据配置
# =========================================== # ===========================================
muzi: muzi:
url: "https://carv.m0101.com/magic/carv/pubin/service/academic" url: "https://carv.m0101.com/magic/carv/pubin/service"
app_id: "713014138179585" app_id: "713014138179585"
app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d" app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d"
timeout: 60s timeout: 60s
@@ -494,3 +494,39 @@ xingwei:
max_backups: 5 max_backups: 5
max_age: 30 max_age: 30
compress: true compress: true
# ===========================================
# ✨ 极光配置
# ===========================================
jiguang:
url: "http://api.jiguangcloud.com/jg-open-api-gateway/api"
app_id: "66ZA28w5" # 请替换为实际的 appId
app_secret: "e5261d0f6f003ae7b9fc1b0255b21761bb618d56" # 请替换为实际的 appSecret
sign_method: "hmac" # 签名方法md5 或 hmac默认 hmac
timeout: 60s # 请求超时时间,默认 60 秒
# 极光日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "jiguang"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true

View File

@@ -122,38 +122,38 @@ wallet:
# 🚦 频率限制配置 - 生产环境 # 🚦 频率限制配置 - 生产环境
# =========================================== # ===========================================
daily_ratelimit: daily_ratelimit:
max_requests_per_day: 50000 # 生产环境每日最大请求次数 max_requests_per_day: 50000 # 生产环境每日最大请求次数
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数 max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
max_concurrent: 200 # 生产环境最大并发请求数 max_concurrent: 200 # 生产环境最大并发请求数
# 排除频率限制的路径 # 排除频率限制的路径
exclude_paths: exclude_paths:
- "/health" # 健康检查接口 - "/health" # 健康检查接口
- "/metrics" # 监控指标接口 - "/metrics" # 监控指标接口
# 排除频率限制的域名 # 排除频率限制的域名
exclude_domains: exclude_domains:
- "api.*" # API二级域名不受频率限制 - "api.*" # API二级域名不受频率限制
- "*.api.*" # 支持多级API域名 - "*.api.*" # 支持多级API域名
# 生产环境安全配置(严格限制) # 生产环境安全配置(严格限制)
enable_ip_whitelist: false # 生产环境不启用IP白名单 enable_ip_whitelist: false # 生产环境不启用IP白名单
enable_ip_blacklist: true # 启用IP黑名单 enable_ip_blacklist: true # 启用IP黑名单
ip_blacklist: # 生产环境IP黑名单 ip_blacklist: # 生产环境IP黑名单
- "192.168.1.100" # 示例被禁止的IP - "192.168.1.100" # 示例被禁止的IP
- "10.0.0.50" # 示例被禁止的IP - "10.0.0.50" # 示例被禁止的IP
enable_user_agent: true # 启用User-Agent检查 enable_user_agent: true # 启用User-Agent检查
blocked_user_agents: # 被阻止的User-Agent blocked_user_agents: # 被阻止的User-Agent
- "curl" # 阻止curl请求 - "curl" # 阻止curl请求
- "wget" # 阻止wget请求 - "wget" # 阻止wget请求
- "python-requests" # 阻止Python requests - "python-requests" # 阻止Python requests
enable_referer: true # 启用Referer检查 enable_referer: true # 启用Referer检查
allowed_referers: # 允许的Referer allowed_referers: # 允许的Referer
- "https://console.tianyuanapi.com" - "https://console.tianyuanapi.com"
- "https://consoletest.tianyuanapi.com" - "https://consoletest.tianyuanapi.com"
enable_geo_block: false # 生产环境暂时不启用地理位置阻止 enable_geo_block: false # 生产环境暂时不启用地理位置阻止
enable_proxy_check: true # 启用代理检查 enable_proxy_check: true # 启用代理检查

View File

@@ -20,7 +20,8 @@ services:
networks: networks:
- tyapi-network - tyapi-network
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U tyapi_user -d tyapi -h localhost"] test:
["CMD-SHELL", "pg_isready -U tyapi_user -d tyapi -h localhost"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 5 retries: 5
@@ -88,6 +89,7 @@ services:
- "25000:8080" - "25000:8080"
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
- ./resources/Pure_Component:/app/resources/Pure_Component
# user: "1001:1001" # 注释掉使用root权限运行 # user: "1001:1001" # 注释掉使用root权限运行
networks: networks:
- tyapi-network - tyapi-network
@@ -169,7 +171,15 @@ services:
redis: redis:
condition: service_healthy condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:8080/health",
]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 5 retries: 5
@@ -189,6 +199,8 @@ volumes:
driver: local driver: local
redis_data: redis_data:
driver: local driver: local
pure_component:
driver: local
networks: networks:
tyapi-network: tyapi-network:

View File

@@ -2,13 +2,13 @@
## 一、功能概述 ## 一、功能概述
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure Component/src/ui` 目录下通过产品编号product_code匹配对应的文件夹或文件。 在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure_Component/src/ui` 目录下通过产品编号product_code匹配对应的文件夹或文件。
## 二、核心需求 ## 二、核心需求
### 2.1 基本功能 ### 2.1 基本功能
1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure Component/src/ui` 下的文件夹或文件 1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure_Component/src/ui` 下的文件夹或文件
- 支持前缀匹配(如产品编号为 `DWBG6A2C`,文件夹可能是 `DWBG6A2C``多cDWBG6A2C` - 支持前缀匹配(如产品编号为 `DWBG6A2C`,文件夹可能是 `DWBG6A2C``多cDWBG6A2C`
- 匹配规则:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分 - 匹配规则:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
@@ -537,7 +537,7 @@ func (s *ComponentReportServiceImpl) MatchProductCodeToPath(ctx context.Context,
} }
// 2. 扫描目录 // 2. 扫描目录
basePath := "resources/Pure Component/src/ui" basePath := "resources/Pure_Component/src/ui"
entries, err := os.ReadDir(basePath) entries, err := os.ReadDir(basePath)
if err != nil { if err != nil {
return "", "", err return "", "", err
@@ -807,7 +807,7 @@ func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, produc
defer zipWriter.Close() defer zipWriter.Close()
// 3. 遍历子产品添加UI组件文件到ZIP // 3. 遍历子产品添加UI组件文件到ZIP
basePath := "resources/Pure Component/src/ui" basePath := "resources/Pure_Component/src/ui"
for _, productCode := range subProductCodes { for _, productCode := range subProductCodes {
path, fileType, err := s.MatchProductCodeToPath(ctx, productCode) path, fileType, err := s.MatchProductCodeToPath(ctx, productCode)
if err != nil { if err != nil {
@@ -847,7 +847,7 @@ func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, produc
// 5. 添加其他必要的文件(如果需要) // 5. 添加其他必要的文件(如果需要)
// 例如:复制 public 目录下的其他文件(如果有) // 例如:复制 public 目录下的其他文件(如果有)
publicBasePath := "resources/Pure Component/public" publicBasePath := "resources/Pure_Component/public"
publicFiles, err := os.ReadDir(publicBasePath) publicFiles, err := os.ReadDir(publicBasePath)
if err == nil { if err == nil {
for _, file := range publicFiles { for _, file := range publicFiles {

View File

@@ -231,6 +231,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
&financeEntities.AlipayOrder{}, &financeEntities.AlipayOrder{},
&financeEntities.InvoiceApplication{}, &financeEntities.InvoiceApplication{},
&financeEntities.UserInvoiceInfo{}, &financeEntities.UserInvoiceInfo{},
&financeEntities.PurchaseOrder{}, //购买组件订单表
// 产品域 // 产品域
&productEntities.Product{}, &productEntities.Product{},

View File

@@ -218,6 +218,12 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
return nil, ErrFrozenAccount return nil, ErrFrozenAccount
} }
// 验证产品是否启用
if !product.IsEnabled {
s.logger.Error("产品未启用", zap.String("product_code", product.Code))
return nil, ErrProductDisabled
}
// 4. 验证IP白名单非开发环境 // 4. 验证IP白名单非开发环境
if !s.config.App.IsDevelopment() && !cmd.Options.IsDebug { if !s.config.App.IsDevelopment() && !cmd.Options.IsDebug {
if !apiUser.IsWhiteListed(cmd.ClientIP) { if !apiUser.IsWhiteListed(cmd.ClientIP) {
@@ -583,12 +589,50 @@ func (s *ApiApplicationServiceImpl) GetUserApiCalls(ctx context.Context, userID
// 转换为响应DTO // 转换为响应DTO
var items []dto.ApiCallRecordResponse var items []dto.ApiCallRecordResponse
for _, call := range calls { for _, call := range calls {
// 解密请求参数
var requestParamsStr string = call.RequestParams // 默认使用原始值
if call.UserId != nil && *call.UserId != "" {
// 获取用户的API密钥信息
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, *call.UserId)
if err != nil {
s.logger.Error("获取用户API信息失败",
zap.Error(err),
zap.String("call_id", call.ID),
zap.String("user_id", *call.UserId))
// 获取失败时使用原始值
} else if apiUser.SecretKey != "" {
// 使用用户的SecretKey解密请求参数
decryptedParams, err := s.DecryptParams(ctx, *call.UserId, &commands.DecryptCommand{
EncryptedData: call.RequestParams,
SecretKey: apiUser.SecretKey,
})
if err != nil {
s.logger.Error("解密请求参数失败",
zap.Error(err),
zap.String("call_id", call.ID),
zap.String("user_id", *call.UserId))
// 解密失败时使用原始值
} else {
// 将解密后的数据转换为JSON字符串
if jsonBytes, err := json.Marshal(decryptedParams); err == nil {
requestParamsStr = string(jsonBytes)
} else {
s.logger.Error("序列化解密参数失败",
zap.Error(err),
zap.String("call_id", call.ID))
// 序列化失败时使用原始值
}
}
}
}
item := dto.ApiCallRecordResponse{ item := dto.ApiCallRecordResponse{
ID: call.ID, ID: call.ID,
AccessId: call.AccessId, AccessId: call.AccessId,
UserId: *call.UserId, UserId: *call.UserId,
TransactionId: call.TransactionId, TransactionId: call.TransactionId,
ClientIp: call.ClientIp, ClientIp: call.ClientIp,
RequestParams: requestParamsStr,
Status: call.Status, Status: call.Status,
StartAt: call.StartAt.Format("2006-01-02 15:04:05"), StartAt: call.StartAt.Format("2006-01-02 15:04:05"),
CreatedAt: call.CreatedAt.Format("2006-01-02 15:04:05"), CreatedAt: call.CreatedAt.Format("2006-01-02 15:04:05"),
@@ -649,11 +693,49 @@ func (s *ApiApplicationServiceImpl) GetAdminApiCalls(ctx context.Context, filter
continue continue
} }
// 解密请求参数
var requestParamsStr string = call.RequestParams // 默认使用原始值
if call.UserId != nil && *call.UserId != "" {
// 获取用户的API密钥信息
apiUser, err := s.apiUserService.LoadApiUserByUserId(ctx, *call.UserId)
if err != nil {
s.logger.Error("获取用户API信息失败",
zap.Error(err),
zap.String("call_id", call.ID),
zap.String("user_id", *call.UserId))
// 获取失败时使用原始值
} else if apiUser.SecretKey != "" {
// 使用用户的SecretKey解密请求参数
decryptedParams, err := s.DecryptParams(ctx, *call.UserId, &commands.DecryptCommand{
EncryptedData: call.RequestParams,
SecretKey: apiUser.SecretKey,
})
if err != nil {
s.logger.Error("解密请求参数失败",
zap.Error(err),
zap.String("call_id", call.ID),
zap.String("user_id", *call.UserId))
// 解密失败时使用原始值
} else {
// 将解密后的数据转换为JSON字符串
if jsonBytes, err := json.Marshal(decryptedParams); err == nil {
requestParamsStr = string(jsonBytes)
} else {
s.logger.Error("序列化解密参数失败",
zap.Error(err),
zap.String("call_id", call.ID))
// 序列化失败时使用原始值
}
}
}
}
item := dto.ApiCallRecordResponse{ item := dto.ApiCallRecordResponse{
ID: call.ID, ID: call.ID,
AccessId: call.AccessId, AccessId: call.AccessId,
TransactionId: call.TransactionId, TransactionId: call.TransactionId,
ClientIp: call.ClientIp, ClientIp: call.ClientIp,
RequestParams: requestParamsStr,
Status: call.Status, Status: call.Status,
} }
@@ -1292,7 +1374,7 @@ func (s *ApiApplicationServiceImpl) UpdateUserBalanceAlertSettings(ctx context.C
// TestBalanceAlertSms 测试余额预警短信 // TestBalanceAlertSms 测试余额预警短信
func (s *ApiApplicationServiceImpl) TestBalanceAlertSms(ctx context.Context, userID string, phone string, balance float64, alertType string) error { func (s *ApiApplicationServiceImpl) TestBalanceAlertSms(ctx context.Context, userID string, phone string, balance float64, alertType string) error {
// 获取用户信息以获取企业名称 // 获取用户信息以获取企业名称
user, err := s.userRepo.GetByID(ctx, userID) user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, userID)
if err != nil { if err != nil {
s.logger.Error("获取用户信息失败", s.logger.Error("获取用户信息失败",
zap.String("user_id", userID), zap.String("user_id", userID),

View File

@@ -49,6 +49,7 @@ type ApiCallRecordResponse struct {
ProductName *string `json:"product_name,omitempty"` ProductName *string `json:"product_name,omitempty"`
TransactionId string `json:"transaction_id"` TransactionId string `json:"transaction_id"`
ClientIp string `json:"client_ip"` ClientIp string `json:"client_ip"`
RequestParams string `json:"request_params"`
Status string `json:"status"` Status string `json:"status"`
StartAt string `json:"start_at"` StartAt string `json:"start_at"`
EndAt *string `json:"end_at,omitempty"` EndAt *string `json:"end_at,omitempty"`

View File

@@ -109,13 +109,16 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
) )
// 验证验证码 // 验证验证码
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil { // 特殊验证码"768005"直接跳过验证环节
record.MarkAsFailed(err.Error()) if cmd.VerificationCode != "768005" {
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
if saveErr != nil { record.MarkAsFailed(err.Error())
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
if saveErr != nil {
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
return nil, fmt.Errorf("验证码错误或已过期")
} }
return nil, fmt.Errorf("验证码错误或已过期")
} }
s.logger.Info("开始处理企业信息提交", s.logger.Info("开始处理企业信息提交",
zap.String("user_id", cmd.UserID)) zap.String("user_id", cmd.UserID))

View File

@@ -125,3 +125,45 @@ type UserSimpleResponse struct {
CompanyName string `json:"company_name"` CompanyName string `json:"company_name"`
Phone string `json:"phone"` Phone string `json:"phone"`
} }
// PurchaseRecordResponse 购买记录响应
type PurchaseRecordResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
OrderNo string `json:"order_no"`
TradeNo *string `json:"trade_no,omitempty"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
ProductName string `json:"product_name"`
Category string `json:"category,omitempty"`
Subject string `json:"subject"`
Amount decimal.Decimal `json:"amount"`
PayAmount *decimal.Decimal `json:"pay_amount,omitempty"`
Status string `json:"status"`
Platform string `json:"platform"`
PayChannel string `json:"pay_channel"`
PaymentType string `json:"payment_type"`
BuyerID string `json:"buyer_id,omitempty"`
SellerID string `json:"seller_id,omitempty"`
ReceiptAmount decimal.Decimal `json:"receipt_amount,omitempty"`
NotifyTime *time.Time `json:"notify_time,omitempty"`
ReturnTime *time.Time `json:"return_time,omitempty"`
PayTime *time.Time `json:"pay_time,omitempty"`
FilePath *string `json:"file_path,omitempty"`
FileSize *int64 `json:"file_size,omitempty"`
Remark string `json:"remark,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
CompanyName string `json:"company_name,omitempty"`
User *UserSimpleResponse `json:"user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PurchaseRecordListResponse 购买记录列表响应
type PurchaseRecordListResponse struct {
Items []PurchaseRecordResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}

View File

@@ -43,6 +43,10 @@ type FinanceApplicationService interface {
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error) GetAdminRechargeRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
// 购买记录
GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error)
// 获取充值配置 // 获取充值配置
GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error) GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error)
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"tyapi-server/internal/application/finance/dto/commands" "tyapi-server/internal/application/finance/dto/commands"
"tyapi-server/internal/application/finance/dto/queries" "tyapi-server/internal/application/finance/dto/queries"
@@ -15,6 +14,7 @@ import (
finance_services "tyapi-server/internal/domains/finance/services" finance_services "tyapi-server/internal/domains/finance/services"
product_repositories "tyapi-server/internal/domains/product/repositories" product_repositories "tyapi-server/internal/domains/product/repositories"
user_repositories "tyapi-server/internal/domains/user/repositories" user_repositories "tyapi-server/internal/domains/user/repositories"
"tyapi-server/internal/shared/component_report"
"tyapi-server/internal/shared/database" "tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/export" "tyapi-server/internal/shared/export"
"tyapi-server/internal/shared/interfaces" "tyapi-server/internal/shared/interfaces"
@@ -36,6 +36,7 @@ type FinanceApplicationServiceImpl struct {
alipayOrderRepo finance_repositories.AlipayOrderRepository alipayOrderRepo finance_repositories.AlipayOrderRepository
wechatOrderRepo finance_repositories.WechatOrderRepository wechatOrderRepo finance_repositories.WechatOrderRepository
rechargeRecordRepo finance_repositories.RechargeRecordRepository rechargeRecordRepo finance_repositories.RechargeRecordRepository
purchaseOrderRepo finance_repositories.PurchaseOrderRepository
componentReportRepo product_repositories.ComponentReportRepository componentReportRepo product_repositories.ComponentReportRepository
userRepo user_repositories.UserRepository userRepo user_repositories.UserRepository
txManager *database.TransactionManager txManager *database.TransactionManager
@@ -54,6 +55,7 @@ func NewFinanceApplicationService(
alipayOrderRepo finance_repositories.AlipayOrderRepository, alipayOrderRepo finance_repositories.AlipayOrderRepository,
wechatOrderRepo finance_repositories.WechatOrderRepository, wechatOrderRepo finance_repositories.WechatOrderRepository,
rechargeRecordRepo finance_repositories.RechargeRecordRepository, rechargeRecordRepo finance_repositories.RechargeRecordRepository,
purchaseOrderRepo finance_repositories.PurchaseOrderRepository,
componentReportRepo product_repositories.ComponentReportRepository, componentReportRepo product_repositories.ComponentReportRepository,
userRepo user_repositories.UserRepository, userRepo user_repositories.UserRepository,
txManager *database.TransactionManager, txManager *database.TransactionManager,
@@ -70,6 +72,7 @@ func NewFinanceApplicationService(
alipayOrderRepo: alipayOrderRepo, alipayOrderRepo: alipayOrderRepo,
wechatOrderRepo: wechatOrderRepo, wechatOrderRepo: wechatOrderRepo,
rechargeRecordRepo: rechargeRecordRepo, rechargeRecordRepo: rechargeRecordRepo,
purchaseOrderRepo: purchaseOrderRepo,
componentReportRepo: componentReportRepo, componentReportRepo: componentReportRepo,
userRepo: userRepo, userRepo: userRepo,
txManager: txManager, txManager: txManager,
@@ -854,13 +857,7 @@ func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context
zap.String("trade_no", notification.TradeNo), zap.String("trade_no", notification.TradeNo),
) )
// 先检查是否是组件报告下载的支付订单 // 处理支付宝支付成功逻辑
s.logger.Info("步骤1: 检查是否是组件报告下载订单",
zap.String("out_trade_no", notification.OutTradeNo),
)
// 使用公共方法处理支付成功逻辑(包括更新充值记录状态)
// 无论是组件报告下载订单还是普通充值订单,都需要更新充值记录状态
err = s.processAlipayPaymentSuccess(ctx, notification.OutTradeNo, notification.TradeNo, notification.TotalAmount, notification.BuyerId, notification.SellerId) err = s.processAlipayPaymentSuccess(ctx, notification.OutTradeNo, notification.TradeNo, notification.TotalAmount, notification.BuyerId, notification.SellerId)
if err != nil { if err != nil {
s.logger.Error("处理支付宝支付成功失败", s.logger.Error("处理支付宝支付成功失败",
@@ -886,20 +883,52 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
return err return err
} }
// 直接调用充值记录服务处理支付成功逻辑 // 查找支付宝订单
// 该服务内部会处理所有必要的检查、事务和更新操作 alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
// 如果是组件报告下载订单,服务会自动跳过钱包余额增加
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
if err != nil { if err != nil {
s.logger.Error("处理支付宝支付成功失败", s.logger.Error("查找支付宝订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
zap.String("out_trade_no", outTradeNo),
zap.Error(err),
)
return err return err
} }
// 检查并更新组件报告下载记录状态(如果存在) if alipayOrder == nil {
s.updateComponentReportDownloadStatus(ctx, outTradeNo) s.logger.Error("支付宝订单不存在", zap.String("out_trade_no", outTradeNo))
return fmt.Errorf("支付宝订单不存在")
}
// 判断是否为充值订单还是购买订单
_, err = s.rechargeRecordRepo.GetByID(ctx, alipayOrder.RechargeID)
if err == nil {
// 这是充值订单,调用充值记录服务处理支付成功逻辑
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
if err != nil {
s.logger.Error("处理支付宝充值支付成功失败",
zap.String("out_trade_no", outTradeNo),
zap.Error(err),
)
return err
}
} else {
// 尝试查找购买订单
_, err = s.purchaseOrderRepo.GetByID(ctx, alipayOrder.RechargeID)
if err == nil {
// 这是购买订单(可能是示例报告购买订单),调用处理购买订单支付成功逻辑
err = s.processPurchaseOrderPaymentSuccess(ctx, alipayOrder.RechargeID, tradeNo, amount, buyerID, sellerID)
if err != nil {
s.logger.Error("处理支付宝购买订单支付成功失败",
zap.String("out_trade_no", outTradeNo),
zap.String("purchase_order_id", alipayOrder.RechargeID),
zap.Error(err),
)
return err
}
} else {
s.logger.Error("无法确定订单类型",
zap.String("out_trade_no", outTradeNo),
zap.String("recharge_id", alipayOrder.RechargeID),
)
return fmt.Errorf("无法确定订单类型")
}
}
s.logger.Info("支付宝支付成功处理完成", s.logger.Info("支付宝支付成功处理完成",
zap.String("out_trade_no", outTradeNo), zap.String("out_trade_no", outTradeNo),
@@ -1477,30 +1506,7 @@ func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Cont
zap.String("transaction_id", transactionID), zap.String("transaction_id", transactionID),
) )
// 先检查是否是组件报告下载的支付订单 // 处理微信支付成功逻辑(充值流程)
s.logger.Info("步骤1: 检查是否是组件报告下载订单",
zap.String("out_trade_no", outTradeNo),
)
// 检查组件报告下载记录
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, outTradeNo)
if err == nil && download != nil {
s.logger.Info("步骤2: 发现组件报告下载订单,直接更新下载记录状态",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
zap.String("product_id", download.ProductID),
zap.String("current_status", download.PaymentStatus),
)
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
s.logger.Info("========== 组件报告下载订单处理完成 ==========")
return nil
}
s.logger.Info("步骤3: 不是组件报告下载订单,按充值流程处理",
zap.String("out_trade_no", outTradeNo),
)
// 处理支付成功逻辑(充值流程)
err = s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, totalAmount) err = s.processWechatPaymentSuccess(ctx, outTradeNo, transactionID, totalAmount)
if err != nil { if err != nil {
s.logger.Error("处理微信支付成功失败", s.logger.Error("处理微信支付成功失败",
@@ -1535,26 +1541,34 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
return fmt.Errorf("微信订单不存在") return fmt.Errorf("微信订单不存在")
} }
// 查找对应的充值记录 // 判断是否为充值订单还是购买订单
rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID) rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID)
if err != nil { if err == nil {
s.logger.Error("查找充值记录失败", // 这是充值订单,继续原有的处理逻辑
zap.String("out_trade_no", outTradeNo), } else {
zap.String("recharge_id", wechatOrder.RechargeID), // 尝试查找购买订单
zap.Error(err), _, err = s.purchaseOrderRepo.GetByID(ctx, wechatOrder.RechargeID)
) if err == nil {
return fmt.Errorf("查找充值记录失败: %w", err) // 这是购买订单(可能是示例报告购买订单),调用处理购买订单支付成功逻辑
err = s.processPurchaseOrderPaymentSuccess(ctx, wechatOrder.RechargeID, transactionID, amount, "", "")
if err != nil {
s.logger.Error("处理微信购买订单支付成功失败",
zap.String("out_trade_no", outTradeNo),
zap.String("purchase_order_id", wechatOrder.RechargeID),
zap.Error(err),
)
return err
}
return nil
} else {
s.logger.Error("无法确定订单类型",
zap.String("out_trade_no", outTradeNo),
zap.String("recharge_id", wechatOrder.RechargeID),
)
return fmt.Errorf("无法确定订单类型")
}
} }
s.logger.Info("步骤4: 检查充值记录备注,判断是否为组件报告下载订单",
zap.String("out_trade_no", outTradeNo),
zap.String("recharge_id", rechargeRecord.ID),
zap.String("notes", rechargeRecord.Notes),
)
// 检查是否是组件报告下载订单(通过备注判断)
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
// 检查订单和充值记录状态,如果都已成功则跳过(只记录一次日志) // 检查订单和充值记录状态,如果都已成功则跳过(只记录一次日志)
if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess && rechargeRecord.Status == finance_entities.RechargeStatusSuccess { if wechatOrder.Status == finance_entities.WechatOrderStatusSuccess && rechargeRecord.Status == finance_entities.RechargeStatusSuccess {
s.logger.Info("微信支付订单已处理成功,跳过重复处理", s.logger.Info("微信支付订单已处理成功,跳过重复处理",
@@ -1562,12 +1576,7 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
zap.String("transaction_id", transactionID), zap.String("transaction_id", transactionID),
zap.String("order_id", wechatOrder.ID), zap.String("order_id", wechatOrder.ID),
zap.String("recharge_id", rechargeRecord.ID), zap.String("recharge_id", rechargeRecord.ID),
zap.Bool("is_component_report", isComponentReportOrder),
) )
// 如果是组件报告下载订单,确保更新下载记录状态
if isComponentReportOrder {
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
}
return nil return nil
} }
@@ -1638,33 +1647,17 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
) )
} }
// 检查是否是组件报告下载订单(通过备注判断 // 充值到钱包(包含赠送金额
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例") totalRechargeAmount := amount.Add(bonusAmount)
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount)
if isComponentReportOrder { if err != nil {
s.logger.Info("步骤5: 检测到组件报告下载订单,不增加钱包余额", s.logger.Error("充值到钱包失败",
zap.String("out_trade_no", outTradeNo), zap.String("out_trade_no", outTradeNo),
zap.String("recharge_id", rechargeRecord.ID), zap.String("user_id", rechargeRecord.UserID),
zap.String("notes", rechargeRecord.Notes), zap.String("total_amount", totalRechargeAmount.String()),
zap.Error(err),
) )
// 组件报告下载订单不增加钱包余额,只更新订单和充值记录状态 return err
} else {
s.logger.Info("步骤5: 普通充值订单,增加钱包余额",
zap.String("out_trade_no", outTradeNo),
zap.String("recharge_id", rechargeRecord.ID),
)
// 充值到钱包(包含赠送金额)
totalRechargeAmount := amount.Add(bonusAmount)
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalRechargeAmount)
if err != nil {
s.logger.Error("充值到钱包失败",
zap.String("out_trade_no", outTradeNo),
zap.String("user_id", rechargeRecord.UserID),
zap.String("total_amount", totalRechargeAmount.String()),
zap.Error(err),
)
return err
}
} }
return nil return nil
@@ -1680,105 +1673,129 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
return err return err
} }
// 如果是组件报告下载订单,更新下载记录状态
if isComponentReportOrder {
s.logger.Info("步骤6: 更新组件报告下载记录状态",
zap.String("out_trade_no", outTradeNo),
)
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
}
s.logger.Info("微信支付成功处理完成", s.logger.Info("微信支付成功处理完成",
zap.String("out_trade_no", outTradeNo), zap.String("out_trade_no", outTradeNo),
zap.String("transaction_id", transactionID), zap.String("transaction_id", transactionID),
zap.String("amount", amount.String()), zap.String("amount", amount.String()),
zap.String("bonus_amount", bonusAmount.String()), zap.String("bonus_amount", bonusAmount.String()),
zap.String("user_id", rechargeRecord.UserID), zap.String("user_id", rechargeRecord.UserID),
zap.Bool("is_component_report", isComponentReportOrder),
) )
return nil return nil
} }
// updateComponentReportDownloadStatus 更新组件报告下载记录状态 // processPurchaseOrderPaymentSuccess 处理购买订单支付成功的逻辑
func (s *FinanceApplicationServiceImpl) updateComponentReportDownloadStatus(ctx context.Context, outTradeNo string) { func (s *FinanceApplicationServiceImpl) processPurchaseOrderPaymentSuccess(ctx context.Context, purchaseOrderID, tradeNo string, amount decimal.Decimal, buyerID, sellerID string) error {
s.logger.Info("========== 开始更新组件报告下载记录状态 ==========", // 查找购买订单
zap.String("out_trade_no", outTradeNo), purchaseOrder, err := s.purchaseOrderRepo.GetByID(ctx, purchaseOrderID)
)
if s.componentReportRepo == nil {
s.logger.Warn("组件报告下载Repository未初始化跳过更新")
return
}
// 根据支付订单号查找组件报告下载记录
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, outTradeNo)
if err != nil { if err != nil {
s.logger.Info("未找到组件报告下载记录,可能不是组件报告下载订单", s.logger.Error("查找购买订单失败",
zap.String("out_trade_no", outTradeNo), zap.String("purchase_order_id", purchaseOrderID),
zap.Error(err), zap.Error(err),
) )
return return fmt.Errorf("查找购买订单失败: %w", err)
} }
if download == nil { if purchaseOrder == nil {
s.logger.Info("组件报告下载记录为空,跳过更新", s.logger.Error("购买订单不存在",
zap.String("out_trade_no", outTradeNo), zap.String("purchase_order_id", purchaseOrderID),
) )
return return fmt.Errorf("购买订单不存在")
} }
s.logger.Info("步骤1: 找到组件报告下载记录", // 检查订单状态,如果已支付则跳过
zap.String("out_trade_no", outTradeNo), if purchaseOrder.Status == finance_entities.PurchaseOrderStatusPaid {
zap.String("download_id", download.ID), s.logger.Info("购买订单已支付,跳过处理",
zap.String("product_id", download.ProductID), zap.String("purchase_order_id", purchaseOrderID),
zap.String("current_status", download.PaymentStatus),
)
// 如果已经是成功状态,跳过
if download.PaymentStatus == "success" {
s.logger.Info("组件报告下载记录已是成功状态,跳过更新",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
) )
return return nil
} }
s.logger.Info("步骤2: 更新支付状态为成功", // 更新购买订单状态
zap.String("out_trade_no", outTradeNo), purchaseOrder.MarkPaid(tradeNo, buyerID, sellerID, amount, amount)
zap.String("download_id", download.ID), err = s.purchaseOrderRepo.Update(ctx, purchaseOrder)
)
// 更新支付状态为成功
download.PaymentStatus = "success"
// 设置过期时间30天后
expiresAt := time.Now().Add(30 * 24 * time.Hour)
download.ExpiresAt = &expiresAt
s.logger.Info("步骤3: 保存更新后的下载记录",
zap.String("out_trade_no", outTradeNo),
zap.String("download_id", download.ID),
zap.String("expires_at", expiresAt.Format("2006-01-02 15:04:05")),
)
// 更新记录
err = s.componentReportRepo.UpdateDownload(ctx, download)
if err != nil { if err != nil {
s.logger.Error("更新组件报告下载记录状态失败", s.logger.Error("更新购买订单状态失败",
zap.String("out_trade_no", outTradeNo), zap.String("purchase_order_id", purchaseOrderID),
zap.String("download_id", download.ID),
zap.Error(err), zap.Error(err),
) )
return return fmt.Errorf("更新购买订单状态失败: %w", err)
} }
s.logger.Info("========== 组件报告下载记录状态更新成功 ==========", // 更新对应的支付订单状态(微信或支付宝)
zap.String("out_trade_no", outTradeNo), if purchaseOrder.PayChannel == "alipay" {
zap.String("download_id", download.ID), alipayOrder, err := s.alipayOrderRepo.GetByRechargeID(ctx, purchaseOrderID)
zap.String("product_id", download.ProductID), if err == nil && alipayOrder != nil {
zap.String("payment_status", download.PaymentStatus), alipayOrder.MarkSuccess(tradeNo, buyerID, sellerID, amount, amount)
err = s.alipayOrderRepo.Update(ctx, *alipayOrder)
if err != nil {
s.logger.Error("更新支付宝订单状态失败",
zap.String("out_trade_no", alipayOrder.OutTradeNo),
zap.Error(err),
)
}
}
} else if purchaseOrder.PayChannel == "wechat" {
wechatOrder, err := s.wechatOrderRepo.GetByRechargeID(ctx, purchaseOrderID)
if err == nil && wechatOrder != nil {
wechatOrder.MarkSuccess(tradeNo, buyerID, sellerID, amount, amount)
err = s.wechatOrderRepo.Update(ctx, *wechatOrder)
if err != nil {
s.logger.Error("更新微信订单状态失败",
zap.String("out_trade_no", wechatOrder.OutTradeNo),
zap.Error(err),
)
}
}
}
// 如果是组件报告购买,需要生成并更新报告文件
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, purchaseOrderID)
if err == nil && download != nil {
// 创建报告生成器
zipGenerator := component_report.NewZipGenerator(s.logger)
// 生成报告文件
zipPath, err := zipGenerator.GenerateZipFile(
ctx,
download.ProductID,
[]string{download.ProductCode}, // 使用简化后的只包含主产品编号的列表
nil, // 使用默认的JSON生成器
"", // 使用默认路径
)
if err != nil {
s.logger.Error("生成组件报告文件失败",
zap.String("download_id", download.ID),
zap.String("purchase_order_id", purchaseOrderID),
zap.Error(err),
)
// 不中断流程,即使生成文件失败也继续处理
} else {
// 更新下载记录的文件路径
download.FilePath = &zipPath
err = s.componentReportRepo.UpdateDownload(ctx, download)
if err != nil {
s.logger.Error("更新下载记录文件路径失败",
zap.String("download_id", download.ID),
zap.Error(err),
)
} else {
s.logger.Info("组件报告文件生成成功",
zap.String("download_id", download.ID),
zap.String("file_path", zipPath),
)
}
}
}
s.logger.Info("购买订单支付成功处理完成",
zap.String("purchase_order_id", purchaseOrderID),
zap.String("trade_no", tradeNo),
zap.String("amount", amount.String()),
) )
return nil
} }
// HandleWechatRefundCallback 处理微信退款回调 // HandleWechatRefundCallback 处理微信退款回调
@@ -1842,3 +1859,163 @@ func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.C
return nil return nil
} }
// GetUserPurchaseRecords 获取用户购买记录
func (s *FinanceApplicationServiceImpl) GetUserPurchaseRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error) {
// 确保 filters 不为 nil
if filters == nil {
filters = make(map[string]interface{})
}
// 添加 user_id 筛选条件,确保只能查询当前用户的记录
filters["user_id"] = userID
// 获取总数
total, err := s.purchaseOrderRepo.CountByFilters(ctx, filters)
if err != nil {
s.logger.Error("统计用户购买记录失败", zap.Error(err), zap.String("userID", userID))
return nil, err
}
// 查询用户购买记录(使用筛选和分页功能)
orders, err := s.purchaseOrderRepo.GetByFilters(ctx, filters, options)
if err != nil {
s.logger.Error("查询用户购买记录失败", zap.Error(err), zap.String("userID", userID))
return nil, err
}
// 转换为响应DTO
var items []responses.PurchaseRecordResponse
for _, order := range orders {
item := responses.PurchaseRecordResponse{
ID: order.ID,
UserID: order.UserID,
OrderNo: order.OrderNo,
TradeNo: order.TradeNo,
ProductID: order.ProductID,
ProductCode: order.ProductCode,
ProductName: order.ProductName,
Category: order.Category,
Subject: order.Subject,
Amount: order.Amount,
PayAmount: order.PayAmount,
Status: string(order.Status),
Platform: order.Platform,
PayChannel: order.PayChannel,
PaymentType: order.PaymentType,
BuyerID: order.BuyerID,
SellerID: order.SellerID,
ReceiptAmount: order.ReceiptAmount,
NotifyTime: order.NotifyTime,
ReturnTime: order.ReturnTime,
PayTime: order.PayTime,
FilePath: order.FilePath,
FileSize: order.FileSize,
Remark: order.Remark,
ErrorCode: order.ErrorCode,
ErrorMessage: order.ErrorMessage,
CreatedAt: order.CreatedAt,
UpdatedAt: order.UpdatedAt,
}
// 获取用户信息和企业名称
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, order.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.PurchaseRecordListResponse{
Items: items,
Total: total,
Page: options.Page,
Size: options.PageSize,
}, nil
}
// GetAdminPurchaseRecords 获取管理端购买记录
func (s *FinanceApplicationServiceImpl) GetAdminPurchaseRecords(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.PurchaseRecordListResponse, error) {
// 获取总数
total, err := s.purchaseOrderRepo.CountByFilters(ctx, filters)
if err != nil {
s.logger.Error("统计管理端购买记录失败", zap.Error(err))
return nil, err
}
// 查询购买记录
orders, err := s.purchaseOrderRepo.GetByFilters(ctx, filters, options)
if err != nil {
s.logger.Error("查询管理端购买记录失败", zap.Error(err))
return nil, err
}
// 转换为响应DTO
var items []responses.PurchaseRecordResponse
for _, order := range orders {
item := responses.PurchaseRecordResponse{
ID: order.ID,
UserID: order.UserID,
OrderNo: order.OrderNo,
TradeNo: order.TradeNo,
ProductID: order.ProductID,
ProductCode: order.ProductCode,
ProductName: order.ProductName,
Category: order.Category,
Subject: order.Subject,
Amount: order.Amount,
PayAmount: order.PayAmount,
Status: string(order.Status),
Platform: order.Platform,
PayChannel: order.PayChannel,
PaymentType: order.PaymentType,
BuyerID: order.BuyerID,
SellerID: order.SellerID,
ReceiptAmount: order.ReceiptAmount,
NotifyTime: order.NotifyTime,
ReturnTime: order.ReturnTime,
PayTime: order.PayTime,
FilePath: order.FilePath,
FileSize: order.FileSize,
Remark: order.Remark,
ErrorCode: order.ErrorCode,
ErrorMessage: order.ErrorMessage,
CreatedAt: order.CreatedAt,
UpdatedAt: order.UpdatedAt,
}
// 获取用户信息和企业名称
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, order.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.PurchaseRecordListResponse{
Items: items,
Total: total,
Page: options.Page,
Size: options.PageSize,
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,10 @@ type CreateProductCommand struct {
IsVisible bool `json:"is_visible" comment:"是否展示"` IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"` SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"` SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
@@ -35,6 +39,10 @@ type UpdateProductCommand struct {
IsVisible bool `json:"is_visible" comment:"是否展示"` IsVisible bool `json:"is_visible" comment:"是否展示"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"` SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"` SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`

View File

@@ -8,15 +8,16 @@ type CreateSubscriptionCommand struct {
// UpdateSubscriptionPriceCommand 更新订阅价格命令 // UpdateSubscriptionPriceCommand 更新订阅价格命令
type UpdateSubscriptionPriceCommand struct { type UpdateSubscriptionPriceCommand struct {
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"` ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"` Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件价格组合包使用"`
} }
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令 // BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
type BatchUpdateSubscriptionPricesCommand struct { type BatchUpdateSubscriptionPricesCommand struct {
UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"` UserID string `json:"user_id" binding:"required,uuid" comment:"用户ID"`
AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"` AdjustmentType string `json:"adjustment_type" binding:"required,oneof=discount cost_multiple" comment:"调整方式(discount:按售价折扣,cost_multiple:按成本价倍数)"`
Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"` Discount float64 `json:"discount,omitempty" binding:"omitempty,min=0.1,max=10" comment:"折扣比例(0.1-10折)"`
CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"` CostMultiple float64 `json:"cost_multiple,omitempty" binding:"omitempty,min=0.1" comment:"成本价倍数"`
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"` Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
} }

View File

@@ -15,17 +15,20 @@ type PackageItemResponse struct {
// ProductInfoResponse 产品详情响应 // ProductInfoResponse 产品详情响应
type ProductInfoResponse struct { type ProductInfoResponse struct {
ID string `json:"id" comment:"产品ID"` ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"` Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"` Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"` Description string `json:"description" comment:"产品简介"`
Content string `json:"content" comment:"产品内容"` Content string `json:"content" comment:"产品内容"`
CategoryID string `json:"category_id" comment:"产品分类ID"` CategoryID string `json:"category_id" comment:"产品分类ID"`
Price float64 `json:"price" comment:"产品价格"` Price float64 `json:"price" comment:"产品价格"`
IsEnabled bool `json:"is_enabled" comment:"是否启用"` IsEnabled bool `json:"is_enabled" comment:"是否启用"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"` SEOTitle string `json:"seo_title" comment:"SEO标题"`
@@ -60,21 +63,22 @@ type ProductSearchResponse struct {
// ProductSimpleResponse 产品简单信息响应 // ProductSimpleResponse 产品简单信息响应
type ProductSimpleResponse struct { type ProductSimpleResponse struct {
ID string `json:"id" comment:"产品ID"` ID string `json:"id" comment:"产品ID"`
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"` OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
Name string `json:"name" comment:"产品名称"` Name string `json:"name" comment:"产品名称"`
Code string `json:"code" comment:"产品编号"` Code string `json:"code" comment:"产品编号"`
Description string `json:"description" comment:"产品简介"` Description string `json:"description" comment:"产品简介"`
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"` Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
Price float64 `json:"price" comment:"产品价格"` Price float64 `json:"price" comment:"产品价格"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"` IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
} }
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价) // ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
type ProductSimpleAdminResponse struct { type ProductSimpleAdminResponse struct {
ProductSimpleResponse ProductSimpleResponse
CostPrice float64 `json:"cost_price" comment:"成本价"` CostPrice float64 `json:"cost_price" comment:"成本价"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格组合包使用"`
} }
// ProductStatsResponse 产品统计响应 // ProductStatsResponse 产品统计响应
@@ -101,6 +105,10 @@ type ProductAdminInfoResponse struct {
IsVisible bool `json:"is_visible" comment:"是否可见"` IsVisible bool `json:"is_visible" comment:"是否可见"`
IsPackage bool `json:"is_package" comment:"是否组合包"` IsPackage bool `json:"is_package" comment:"是否组合包"`
// UI组件相关字段
SellUIComponent bool `json:"sell_ui_component" comment:"是否出售UI组件"`
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件销售价格组合包使用"`
// SEO信息 // SEO信息
SEOTitle string `json:"seo_title" comment:"SEO标题"` SEOTitle string `json:"seo_title" comment:"SEO标题"`
SEODescription string `json:"seo_description" comment:"SEO描述"` SEODescription string `json:"seo_description" comment:"SEO描述"`

View File

@@ -13,47 +13,48 @@ type UserSimpleResponse struct {
// SubscriptionInfoResponse 订阅详情响应 // SubscriptionInfoResponse 订阅详情响应
type SubscriptionInfoResponse struct { type SubscriptionInfoResponse struct {
ID string `json:"id" comment:"订阅ID"` ID string `json:"id" comment:"订阅ID"`
UserID string `json:"user_id" comment:"用户ID"` UserID string `json:"user_id" comment:"用户ID"`
ProductID string `json:"product_id" comment:"产品ID"` ProductID string `json:"product_id" comment:"产品ID"`
Price float64 `json:"price" comment:"订阅价格"` Price float64 `json:"price" comment:"订阅价格"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"` UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格组合包使用"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
// 关联信息 // 关联信息
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"` User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"` Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
// 管理员端使用,包含成本价的产品信息 // 管理员端使用,包含成本价的产品信息
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"` ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
CreatedAt time.Time `json:"created_at" comment:"创建时间"` CreatedAt time.Time `json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"` UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
} }
// SubscriptionListResponse 订阅列表响应 // SubscriptionListResponse 订阅列表响应
type SubscriptionListResponse struct { type SubscriptionListResponse struct {
Total int64 `json:"total" comment:"总数"` Total int64 `json:"total" comment:"总数"`
Page int `json:"page" comment:"页码"` Page int `json:"page" comment:"页码"`
Size int `json:"size" comment:"每页数量"` Size int `json:"size" comment:"每页数量"`
Items []SubscriptionInfoResponse `json:"items" comment:"订阅列表"` Items []SubscriptionInfoResponse `json:"items" comment:"订阅列表"`
} }
// SubscriptionSimpleResponse 订阅简单信息响应 // SubscriptionSimpleResponse 订阅简单信息响应
type SubscriptionSimpleResponse struct { type SubscriptionSimpleResponse struct {
ID string `json:"id" comment:"订阅ID"` ID string `json:"id" comment:"订阅ID"`
ProductID string `json:"product_id" comment:"产品ID"` ProductID string `json:"product_id" comment:"产品ID"`
Price float64 `json:"price" comment:"订阅价格"` Price float64 `json:"price" comment:"订阅价格"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"` APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
} }
// SubscriptionUsageResponse 订阅使用情况响应 // SubscriptionUsageResponse 订阅使用情况响应
type SubscriptionUsageResponse struct { type SubscriptionUsageResponse struct {
ID string `json:"id" comment:"订阅ID"` ID string `json:"id" comment:"订阅ID"`
ProductID string `json:"product_id" comment:"产品ID"` ProductID string `json:"product_id" comment:"产品ID"`
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"` APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
} }
// SubscriptionStatsResponse 订阅统计响应 // SubscriptionStatsResponse 订阅统计响应
type SubscriptionStatsResponse struct { type SubscriptionStatsResponse struct {
TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"` TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"`
TotalRevenue float64 `json:"total_revenue" comment:"总收入"` TotalRevenue float64 `json:"total_revenue" comment:"总收入"`
} }

View File

@@ -54,20 +54,22 @@ func NewProductApplicationService(
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) { func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) {
// 1. 构建产品实体 // 1. 构建产品实体
product := &entities.Product{ product := &entities.Product{
Name: cmd.Name, Name: cmd.Name,
Code: cmd.Code, Code: cmd.Code,
Description: cmd.Description, Description: cmd.Description,
Content: cmd.Content, Content: cmd.Content,
CategoryID: cmd.CategoryID, CategoryID: cmd.CategoryID,
Price: decimal.NewFromFloat(cmd.Price), Price: decimal.NewFromFloat(cmd.Price),
CostPrice: decimal.NewFromFloat(cmd.CostPrice), CostPrice: decimal.NewFromFloat(cmd.CostPrice),
Remark: cmd.Remark, Remark: cmd.Remark,
IsEnabled: cmd.IsEnabled, IsEnabled: cmd.IsEnabled,
IsVisible: cmd.IsVisible, IsVisible: cmd.IsVisible,
IsPackage: cmd.IsPackage, IsPackage: cmd.IsPackage,
SEOTitle: cmd.SEOTitle, SellUIComponent: cmd.SellUIComponent,
SEODescription: cmd.SEODescription, UIComponentPrice: decimal.NewFromFloat(cmd.UIComponentPrice),
SEOKeywords: cmd.SEOKeywords, SEOTitle: cmd.SEOTitle,
SEODescription: cmd.SEODescription,
SEOKeywords: cmd.SEOKeywords,
} }
// 2. 创建产品 // 2. 创建产品
@@ -101,6 +103,8 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *
existingProduct.IsEnabled = cmd.IsEnabled existingProduct.IsEnabled = cmd.IsEnabled
existingProduct.IsVisible = cmd.IsVisible existingProduct.IsVisible = cmd.IsVisible
existingProduct.IsPackage = cmd.IsPackage existingProduct.IsPackage = cmd.IsPackage
existingProduct.SellUIComponent = cmd.SellUIComponent
existingProduct.UIComponentPrice = decimal.NewFromFloat(cmd.UIComponentPrice)
existingProduct.SEOTitle = cmd.SEOTitle existingProduct.SEOTitle = cmd.SEOTitle
existingProduct.SEODescription = cmd.SEODescription existingProduct.SEODescription = cmd.SEODescription
existingProduct.SEOKeywords = cmd.SEOKeywords existingProduct.SEOKeywords = cmd.SEOKeywords
@@ -486,21 +490,23 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
// convertToProductInfoResponse 转换为产品信息响应 // convertToProductInfoResponse 转换为产品信息响应
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse { func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
response := &responses.ProductInfoResponse{ response := &responses.ProductInfoResponse{
ID: product.ID, ID: product.ID,
OldID: product.OldID, OldID: product.OldID,
Name: product.Name, Name: product.Name,
Code: product.Code, Code: product.Code,
Description: product.Description, Description: product.Description,
Content: product.Content, Content: product.Content,
CategoryID: product.CategoryID, CategoryID: product.CategoryID,
Price: product.Price.InexactFloat64(), Price: product.Price.InexactFloat64(),
IsEnabled: product.IsEnabled, IsEnabled: product.IsEnabled,
IsPackage: product.IsPackage, IsPackage: product.IsPackage,
SEOTitle: product.SEOTitle, SellUIComponent: product.SellUIComponent,
SEODescription: product.SEODescription, UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
SEOKeywords: product.SEOKeywords, SEOTitle: product.SEOTitle,
CreatedAt: product.CreatedAt, SEODescription: product.SEODescription,
UpdatedAt: product.UpdatedAt, SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
} }
// 添加分类信息 // 添加分类信息
@@ -530,24 +536,26 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
// convertToProductAdminInfoResponse 转换为管理员产品信息响应 // convertToProductAdminInfoResponse 转换为管理员产品信息响应
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse { func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
response := &responses.ProductAdminInfoResponse{ response := &responses.ProductAdminInfoResponse{
ID: product.ID, ID: product.ID,
OldID: product.OldID, OldID: product.OldID,
Name: product.Name, Name: product.Name,
Code: product.Code, Code: product.Code,
Description: product.Description, Description: product.Description,
Content: product.Content, Content: product.Content,
CategoryID: product.CategoryID, CategoryID: product.CategoryID,
Price: product.Price.InexactFloat64(), Price: product.Price.InexactFloat64(),
CostPrice: product.CostPrice.InexactFloat64(), CostPrice: product.CostPrice.InexactFloat64(),
Remark: product.Remark, Remark: product.Remark,
IsEnabled: product.IsEnabled, IsEnabled: product.IsEnabled,
IsVisible: product.IsVisible, // 管理员可以看到可见状态 IsVisible: product.IsVisible, // 管理员可以看到可见状态
IsPackage: product.IsPackage, IsPackage: product.IsPackage,
SEOTitle: product.SEOTitle, SellUIComponent: product.SellUIComponent,
SEODescription: product.SEODescription, UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
SEOKeywords: product.SEOKeywords, SEOTitle: product.SEOTitle,
CreatedAt: product.CreatedAt, SEODescription: product.SEODescription,
UpdatedAt: product.UpdatedAt, SEOKeywords: product.SEOKeywords,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
} }
// 添加分类信息 // 添加分类信息

View File

@@ -44,7 +44,7 @@ func NewSubscriptionApplicationService(
// UpdateSubscriptionPrice 更新订阅价格 // UpdateSubscriptionPrice 更新订阅价格
// 业务流程1. 获取订阅 2. 更新价格 3. 保存订阅 // 业务流程1. 获取订阅 2. 更新价格 3. 保存订阅
func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error { func (s *SubscriptionApplicationServiceImpl) UpdateSubscriptionPrice(ctx context.Context, cmd *commands.UpdateSubscriptionPriceCommand) error {
return s.productSubscriptionService.UpdateSubscriptionPrice(ctx, cmd.ID, cmd.Price) return s.productSubscriptionService.UpdateSubscriptionPriceWithUIComponent(ctx, cmd.ID, cmd.Price, cmd.UIComponentPrice)
} }
// BatchUpdateSubscriptionPrices 一键改价 // BatchUpdateSubscriptionPrices 一键改价
@@ -377,16 +377,23 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s
productResponse = s.convertToProductSimpleResponse(subscription.Product) productResponse = s.convertToProductSimpleResponse(subscription.Product)
} }
// 获取UI组件价格如果订阅中没有设置则从产品中获取
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
}
return &responses.SubscriptionInfoResponse{ return &responses.SubscriptionInfoResponse{
ID: subscription.ID, ID: subscription.ID,
UserID: subscription.UserID, UserID: subscription.UserID,
ProductID: subscription.ProductID, ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(), Price: subscription.Price.InexactFloat64(),
User: userInfo, UIComponentPrice: uiComponentPrice,
Product: productResponse, User: userInfo,
APIUsed: subscription.APIUsed, Product: productResponse,
CreatedAt: subscription.CreatedAt, APIUsed: subscription.APIUsed,
UpdatedAt: subscription.UpdatedAt, CreatedAt: subscription.CreatedAt,
UpdatedAt: subscription.UpdatedAt,
} }
} }
@@ -433,16 +440,23 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponseFo
productAdminResponse = s.convertToProductSimpleAdminResponse(subscription.Product) productAdminResponse = s.convertToProductSimpleAdminResponse(subscription.Product)
} }
// 获取UI组件价格如果订阅中没有设置则从产品中获取
uiComponentPrice := subscription.UIComponentPrice.InexactFloat64()
if uiComponentPrice == 0 && subscription.Product != nil && (subscription.Product.IsPackage) {
uiComponentPrice = subscription.Product.UIComponentPrice.InexactFloat64()
}
return &responses.SubscriptionInfoResponse{ return &responses.SubscriptionInfoResponse{
ID: subscription.ID, ID: subscription.ID,
UserID: subscription.UserID, UserID: subscription.UserID,
ProductID: subscription.ProductID, ProductID: subscription.ProductID,
Price: subscription.Price.InexactFloat64(), Price: subscription.Price.InexactFloat64(),
User: userInfo, UIComponentPrice: uiComponentPrice,
ProductAdmin: productAdminResponse, User: userInfo,
APIUsed: subscription.APIUsed, ProductAdmin: productAdminResponse,
CreatedAt: subscription.CreatedAt, APIUsed: subscription.APIUsed,
UpdatedAt: subscription.UpdatedAt, CreatedAt: subscription.CreatedAt,
UpdatedAt: subscription.UpdatedAt,
} }
} }
@@ -464,7 +478,8 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleAdminResponse
Category: categoryResponse, Category: categoryResponse,
IsPackage: product.IsPackage, IsPackage: product.IsPackage,
}, },
CostPrice: product.CostPrice.InexactFloat64(), CostPrice: product.CostPrice.InexactFloat64(),
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
} }
} }

View File

@@ -7,11 +7,13 @@ import (
"mime/multipart" "mime/multipart"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"tyapi-server/internal/domains/product/entities" "tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories" "tyapi-server/internal/domains/product/repositories"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"go.uber.org/zap"
) )
// UIComponentApplicationService UI组件应用服务接口 // UIComponentApplicationService UI组件应用服务接口
@@ -93,6 +95,7 @@ type UIComponentApplicationServiceImpl struct {
productUIComponentRepo repositories.ProductUIComponentRepository productUIComponentRepo repositories.ProductUIComponentRepository
fileStorageService FileStorageService fileStorageService FileStorageService
fileService UIComponentFileService fileService UIComponentFileService
logger *zap.Logger
} }
// FileStorageService 文件存储服务接口 // FileStorageService 文件存储服务接口
@@ -108,12 +111,14 @@ func NewUIComponentApplicationService(
productUIComponentRepo repositories.ProductUIComponentRepository, productUIComponentRepo repositories.ProductUIComponentRepository,
fileStorageService FileStorageService, fileStorageService FileStorageService,
fileService UIComponentFileService, fileService UIComponentFileService,
logger *zap.Logger,
) UIComponentApplicationService { ) UIComponentApplicationService {
return &UIComponentApplicationServiceImpl{ return &UIComponentApplicationServiceImpl{
uiComponentRepo: uiComponentRepo, uiComponentRepo: uiComponentRepo,
productUIComponentRepo: productUIComponentRepo, productUIComponentRepo: productUIComponentRepo,
fileStorageService: fileStorageService, fileStorageService: fileStorageService,
fileService: fileService, fileService: fileService,
logger: logger,
} }
} }
@@ -182,10 +187,14 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFile(ctx contex
fileType := strings.ToLower(filepath.Ext(file.Filename)) fileType := strings.ToLower(filepath.Ext(file.Filename))
// 更新组件信息 // 更新组件信息
folderPath := "resources/Pure Component/src/ui" folderPath := "resources/Pure_Component/src/ui"
createdComponent.FolderPath = &folderPath createdComponent.FolderPath = &folderPath
createdComponent.FileType = &fileType createdComponent.FileType = &fileType
// 记录文件上传时间
now := time.Now()
createdComponent.FileUploadTime = &now
// 仅对ZIP文件设置已解压标记 // 仅对ZIP文件设置已解压标记
if fileType == ".zip" { if fileType == ".zip" {
createdComponent.IsExtracted = true createdComponent.IsExtracted = true
@@ -255,9 +264,13 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFiles(ctx conte
} }
// 更新组件信息 // 更新组件信息
folderPath := "resources/Pure Component/src/ui" folderPath := "resources/Pure_Component/src/ui"
createdComponent.FolderPath = &folderPath createdComponent.FolderPath = &folderPath
// 记录文件上传时间
now := time.Now()
createdComponent.FileUploadTime = &now
// 检查是否有ZIP文件 // 检查是否有ZIP文件
hasZipFile := false hasZipFile := false
for _, fileHeader := range files { for _, fileHeader := range files {
@@ -363,9 +376,13 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFilesAndPaths(c
} }
// 更新组件信息 // 更新组件信息
folderPath := "resources/Pure Component/src/ui" folderPath := "resources/Pure_Component/src/ui"
createdComponent.FolderPath = &folderPath createdComponent.FolderPath = &folderPath
// 记录文件上传时间
now := time.Now()
createdComponent.FileUploadTime = &now
// 检查是否有ZIP文件 // 检查是否有ZIP文件
hasZipFile := false hasZipFile := false
for _, fileHeader := range files { for _, fileHeader := range files {
@@ -442,20 +459,56 @@ func (s *UIComponentApplicationServiceImpl) UpdateUIComponent(ctx context.Contex
// DeleteUIComponent 删除UI组件 // DeleteUIComponent 删除UI组件
func (s *UIComponentApplicationServiceImpl) DeleteUIComponent(ctx context.Context, id string) error { func (s *UIComponentApplicationServiceImpl) DeleteUIComponent(ctx context.Context, id string) error {
// 获取组件信息
component, err := s.uiComponentRepo.GetByID(ctx, id) component, err := s.uiComponentRepo.GetByID(ctx, id)
if err != nil { if err != nil {
return err s.logger.Error("获取UI组件失败", zap.Error(err), zap.String("id", id))
return fmt.Errorf("获取UI组件失败: %w", err)
} }
if component == nil { if component == nil {
s.logger.Warn("UI组件不存在", zap.String("id", id))
return ErrComponentNotFound return ErrComponentNotFound
} }
// 删除关联的文件 // 记录组件信息
if component.FilePath != nil { s.logger.Info("开始删除UI组件",
_ = s.fileStorageService.DeleteFile(ctx, *component.FilePath) zap.String("id", id),
zap.String("componentCode", component.ComponentCode),
zap.String("componentName", component.ComponentName),
zap.Bool("isExtracted", component.IsExtracted),
zap.Any("filePath", component.FilePath),
zap.Any("folderPath", component.FolderPath))
// 使用智能删除方法,根据组件编码和上传时间删除相关文件
if err := s.fileService.DeleteFilesByComponentCode(component.ComponentCode, component.FileUploadTime); err != nil {
// 记录错误但不阻止删除数据库记录
s.logger.Error("删除组件文件失败",
zap.Error(err),
zap.String("componentCode", component.ComponentCode),
zap.Any("fileUploadTime", component.FileUploadTime))
} }
return s.uiComponentRepo.Delete(ctx, id) // 删除关联的文件(FilePath指向的文件)
if component.FilePath != nil {
if err := s.fileStorageService.DeleteFile(ctx, *component.FilePath); err != nil {
s.logger.Error("删除文件失败",
zap.Error(err),
zap.String("filePath", *component.FilePath))
}
}
// 删除数据库记录
if err := s.uiComponentRepo.Delete(ctx, id); err != nil {
s.logger.Error("删除UI组件数据库记录失败",
zap.Error(err),
zap.String("id", id))
return fmt.Errorf("删除UI组件数据库记录失败: %w", err)
}
s.logger.Info("UI组件删除成功",
zap.String("id", id),
zap.String("componentCode", component.ComponentCode))
return nil
} }
// ListUIComponents 获取UI组件列表 // ListUIComponents 获取UI组件列表
@@ -634,10 +687,14 @@ func (s *UIComponentApplicationServiceImpl) UploadAndExtractUIComponentFile(ctx
fileType := strings.ToLower(filepath.Ext(file.Filename)) fileType := strings.ToLower(filepath.Ext(file.Filename))
// 更新组件信息 // 更新组件信息
folderPath := "resources/Pure Component/src/ui" folderPath := "resources/Pure_Component/src/ui"
component.FolderPath = &folderPath component.FolderPath = &folderPath
component.FileType = &fileType component.FileType = &fileType
// 记录文件上传时间
now := time.Now()
component.FileUploadTime = &now
// 仅对ZIP文件设置已解压标记 // 仅对ZIP文件设置已解压标记
if fileType == ".zip" { if fileType == ".zip" {
component.IsExtracted = true component.IsExtracted = true

View File

@@ -32,6 +32,9 @@ type UIComponentFileService interface {
// 获取文件夹内容 // 获取文件夹内容
GetFolderContent(folderPath string) ([]FileInfo, error) GetFolderContent(folderPath string) ([]FileInfo, error)
// 根据组件编码和上传时间智能删除组件相关文件
DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error
} }
// FileInfo 文件信息 // FileInfo 文件信息
@@ -222,11 +225,34 @@ func (s *UIComponentFileServiceImpl) CreateFolderByCode(componentCode string) (s
// DeleteFolder 删除组件文件夹 // DeleteFolder 删除组件文件夹
func (s *UIComponentFileServiceImpl) DeleteFolder(folderPath string) error { func (s *UIComponentFileServiceImpl) DeleteFolder(folderPath string) error {
// 记录尝试删除的文件夹路径
s.logger.Info("尝试删除文件夹", zap.String("folderPath", folderPath))
// 获取文件夹信息,用于调试
if info, err := os.Stat(folderPath); err == nil {
s.logger.Info("文件夹信息",
zap.String("folderPath", folderPath),
zap.Bool("isDir", info.IsDir()),
zap.Int64("size", info.Size()),
zap.Time("modTime", info.ModTime()))
} else {
s.logger.Error("获取文件夹信息失败",
zap.Error(err),
zap.String("folderPath", folderPath))
}
// 检查文件夹是否存在
if !s.FolderExists(folderPath) { if !s.FolderExists(folderPath) {
s.logger.Info("文件夹不存在", zap.String("folderPath", folderPath))
return nil // 文件夹不存在,不视为错误 return nil // 文件夹不存在,不视为错误
} }
// 尝试删除文件夹
s.logger.Info("开始删除文件夹", zap.String("folderPath", folderPath))
if err := os.RemoveAll(folderPath); err != nil { if err := os.RemoveAll(folderPath); err != nil {
s.logger.Error("删除文件夹失败",
zap.Error(err),
zap.String("folderPath", folderPath))
return fmt.Errorf("删除文件夹失败: %w", err) return fmt.Errorf("删除文件夹失败: %w", err)
} }
@@ -339,3 +365,95 @@ func (s *UIComponentFileServiceImpl) extractZipFile(zipPath, destPath string) er
return nil return nil
} }
// DeleteFilesByComponentCode 根据组件编码和上传时间智能删除组件相关文件
func (s *UIComponentFileServiceImpl) DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error {
// 记录基础路径和组件编码
s.logger.Info("开始删除组件文件",
zap.String("basePath", s.basePath),
zap.String("componentCode", componentCode),
zap.Any("uploadTime", uploadTime))
// 1. 查找名为组件编码的文件夹
componentDir := filepath.Join(s.basePath, componentCode)
s.logger.Info("检查组件文件夹", zap.String("componentDir", componentDir))
if s.FolderExists(componentDir) {
s.logger.Info("找到组件文件夹,开始删除", zap.String("componentDir", componentDir))
if err := s.DeleteFolder(componentDir); err != nil {
s.logger.Error("删除组件文件夹失败",
zap.Error(err),
zap.String("componentCode", componentCode),
zap.String("componentDir", componentDir))
return fmt.Errorf("删除组件文件夹失败: %w", err)
}
s.logger.Info("成功删除组件文件夹", zap.String("componentCode", componentCode))
return nil
} else {
s.logger.Info("组件文件夹不存在", zap.String("componentDir", componentDir))
}
// 2. 查找文件名包含组件编码的文件
pattern := filepath.Join(s.basePath, "*"+componentCode+"*")
s.logger.Info("查找匹配文件", zap.String("pattern", pattern))
files, err := filepath.Glob(pattern)
if err != nil {
s.logger.Error("查找组件文件失败",
zap.Error(err),
zap.String("pattern", pattern))
return fmt.Errorf("查找组件文件失败: %w", err)
}
s.logger.Info("找到匹配文件",
zap.Strings("files", files),
zap.Int("count", len(files)))
// 3. 如果没有上传时间,删除所有匹配的文件
if uploadTime == nil {
for _, file := range files {
if err := os.Remove(file); err != nil {
s.logger.Error("删除文件失败", zap.String("file", file), zap.Error(err))
} else {
s.logger.Info("成功删除文件", zap.String("file", file))
}
}
return nil
}
// 4. 如果有上传时间,根据文件修改时间和上传时间的匹配度来删除文件
var deletedFiles []string
for _, file := range files {
// 获取文件信息
fileInfo, err := os.Stat(file)
if err != nil {
s.logger.Warn("获取文件信息失败", zap.String("file", file), zap.Error(err))
continue
}
// 计算文件修改时间与上传时间的差异(以秒为单位)
timeDiff := fileInfo.ModTime().Sub(*uploadTime).Seconds()
// 如果时间差在60秒内认为是最匹配的文件
if timeDiff < 60 && timeDiff > -60 {
if err := os.Remove(file); err != nil {
s.logger.Warn("删除文件失败", zap.String("file", file), zap.Error(err))
} else {
deletedFiles = append(deletedFiles, file)
s.logger.Info("成功删除文件", zap.String("file", file),
zap.Time("uploadTime", *uploadTime),
zap.Time("fileModTime", fileInfo.ModTime()))
}
}
}
// 如果没有找到匹配的文件,记录警告但返回成功
if len(deletedFiles) == 0 && len(files) > 0 {
s.logger.Warn("没有找到匹配时间戳的文件",
zap.String("componentCode", componentCode),
zap.Time("uploadTime", *uploadTime),
zap.Int("foundFiles", len(files)))
}
return nil
}

View File

@@ -2772,18 +2772,19 @@ func (s *StatisticsApplicationServiceImpl) AdminGetTodayCertifiedEnterprises(ctx
var enterprises []map[string]interface{} var enterprises []map[string]interface{}
for _, cert := range completedCertifications { for _, cert := range completedCertifications {
// 获取企业信息 // 获取企业信息
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, cert.UserID) // 使用预加载方法一次性获取用户和企业信息
if err != nil { user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, cert.UserID)
s.logger.Warn("获取企业信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
continue
}
// 获取用户基本信息(仅需要用户名)
user, err := s.userRepo.GetByID(ctx, cert.UserID)
if err != nil { if err != nil {
s.logger.Warn("获取用户信息失败", zap.String("user_id", cert.UserID), zap.Error(err)) s.logger.Warn("获取用户信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
continue continue
} }
// 获取企业信息
enterpriseInfo := user.EnterpriseInfo
if enterpriseInfo == nil {
s.logger.Warn("用户没有企业信息", zap.String("user_id", cert.UserID))
continue
}
enterprise := map[string]interface{}{ enterprise := map[string]interface{}{
"id": cert.ID, "id": cert.ID,

View File

@@ -38,6 +38,7 @@ type Config struct {
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"` TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"` Alicloud AlicloudConfig `mapstructure:"alicloud"`
Xingwei XingweiConfig `mapstructure:"xingwei"` Xingwei XingweiConfig `mapstructure:"xingwei"`
Jiguang JiguangConfig `mapstructure:"jiguang"`
} }
// ServerConfig HTTP服务器配置 // ServerConfig HTTP服务器配置
@@ -520,6 +521,35 @@ type XingweiLevelFileConfig struct {
Compress bool `mapstructure:"compress"` Compress bool `mapstructure:"compress"`
} }
// JiguangConfig 极光配置
type JiguangConfig struct {
URL string `mapstructure:"url"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
SignMethod string `mapstructure:"sign_method"` // md5 或 hmac默认 hmac
Timeout time.Duration `mapstructure:"timeout"`
// 极光日志配置
Logging JiguangLoggingConfig `mapstructure:"logging"`
}
// JiguangLoggingConfig 极光日志配置
type JiguangLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]JiguangLevelFileConfig `mapstructure:"level_configs"`
}
// JiguangLevelFileConfig 极光级别文件配置
type JiguangLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// DomainConfig 域名配置 // DomainConfig 域名配置
type DomainConfig struct { type DomainConfig struct {
API string `mapstructure:"api"` // API域名 API string `mapstructure:"api"` // API域名

View File

@@ -39,6 +39,7 @@ import (
infra_events "tyapi-server/internal/infrastructure/events" infra_events "tyapi-server/internal/infrastructure/events"
"tyapi-server/internal/infrastructure/external/alicloud" "tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/email" "tyapi-server/internal/infrastructure/external/email"
"tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/muzi"
"tyapi-server/internal/infrastructure/external/ocr" "tyapi-server/internal/infrastructure/external/ocr"
"tyapi-server/internal/infrastructure/external/sms" "tyapi-server/internal/infrastructure/external/sms"
@@ -366,6 +367,10 @@ func NewContainer() *Container {
func(cfg *config.Config) (*xingwei.XingweiService, error) { func(cfg *config.Config) (*xingwei.XingweiService, error) {
return xingwei.NewXingweiServiceWithConfig(cfg) return xingwei.NewXingweiServiceWithConfig(cfg)
}, },
// JiguangService - 极光服务
func(cfg *config.Config) (*jiguang.JiguangService, error) {
return jiguang.NewJiguangServiceWithConfig(cfg)
},
func(cfg *config.Config) *yushan.YushanService { func(cfg *config.Config) *yushan.YushanService {
return yushan.NewYushanService( return yushan.NewYushanService(
cfg.Yushan.URL, cfg.Yushan.URL,
@@ -571,6 +576,11 @@ func NewContainer() *Container {
product_repo.NewGormComponentReportRepository, product_repo.NewGormComponentReportRepository,
fx.As(new(domain_product_repo.ComponentReportRepository)), fx.As(new(domain_product_repo.ComponentReportRepository)),
), ),
// 购买订单仓储
fx.Annotate(
finance_repo.NewGormPurchaseOrderRepository,
fx.As(new(domain_finance_repo.PurchaseOrderRepository)),
),
// UI组件仓储 - 同时注册具体类型和接口类型 // UI组件仓储 - 同时注册具体类型和接口类型
fx.Annotate( fx.Annotate(
product_repo.NewGormUIComponentRepository, product_repo.NewGormUIComponentRepository,
@@ -893,6 +903,7 @@ func NewContainer() *Container {
alipayOrderRepo domain_finance_repo.AlipayOrderRepository, alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
wechatOrderRepo domain_finance_repo.WechatOrderRepository, wechatOrderRepo domain_finance_repo.WechatOrderRepository,
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository, rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
userRepo domain_user_repo.UserRepository, userRepo domain_user_repo.UserRepository,
txManager *shared_database.TransactionManager, txManager *shared_database.TransactionManager,
logger *zap.Logger, logger *zap.Logger,
@@ -909,6 +920,7 @@ func NewContainer() *Container {
alipayOrderRepo, alipayOrderRepo,
wechatOrderRepo, wechatOrderRepo,
rechargeRecordRepo, rechargeRecordRepo,
purchaseOrderRepo,
componentReportRepo, componentReportRepo,
userRepo, userRepo,
txManager, txManager,
@@ -950,6 +962,36 @@ func NewContainer() *Container {
}, },
fx.As(new(product.ProductApplicationService)), fx.As(new(product.ProductApplicationService)),
), ),
// 组件报告订单服务
func(
productRepo domain_product_repo.ProductRepository,
docRepo domain_product_repo.ProductDocumentationRepository,
apiConfigRepo domain_product_repo.ProductApiConfigRepository,
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
componentReportRepo domain_product_repo.ComponentReportRepository,
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
subscriptionRepo domain_product_repo.SubscriptionRepository,
aliPayService *payment.AliPayService,
wechatPayService *payment.WechatPayService,
logger *zap.Logger,
) *product.ComponentReportOrderService {
return product.NewComponentReportOrderService(
productRepo,
docRepo,
apiConfigRepo,
purchaseOrderRepo,
componentReportRepo,
rechargeRecordRepo,
alipayOrderRepo,
wechatOrderRepo,
subscriptionRepo,
aliPayService,
wechatPayService,
logger,
)
},
// 产品API配置应用服务 - 绑定到接口 // 产品API配置应用服务 - 绑定到接口
fx.Annotate( fx.Annotate(
product.NewProductApiConfigApplicationService, product.NewProductApiConfigApplicationService,
@@ -1055,7 +1097,7 @@ func NewContainer() *Container {
logger *zap.Logger, logger *zap.Logger,
) product.UIComponentApplicationService { ) product.UIComponentApplicationService {
// 创建UI组件文件服务 // 创建UI组件文件服务
basePath := "resources/Pure Component/src/ui" basePath := "resources/Pure_Component/src/ui"
fileService := product.NewUIComponentFileService(basePath, logger) fileService := product.NewUIComponentFileService(basePath, logger)
return product.NewUIComponentApplicationService( return product.NewUIComponentApplicationService(
@@ -1063,6 +1105,7 @@ func NewContainer() *Container {
productUIComponentRepo, productUIComponentRepo,
fileStorageService, fileStorageService,
fileService, fileService,
logger,
) )
}, },
fx.As(new(product.UIComponentApplicationService)), fx.As(new(product.UIComponentApplicationService)),
@@ -1183,6 +1226,7 @@ func NewContainer() *Container {
docRepo domain_product_repo.ProductDocumentationRepository, docRepo domain_product_repo.ProductDocumentationRepository,
apiConfigRepo domain_product_repo.ProductApiConfigRepository, apiConfigRepo domain_product_repo.ProductApiConfigRepository,
componentReportRepo domain_product_repo.ComponentReportRepository, componentReportRepo domain_product_repo.ComponentReportRepository,
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository, rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
alipayOrderRepo domain_finance_repo.AlipayOrderRepository, alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
wechatOrderRepo domain_finance_repo.WechatOrderRepository, wechatOrderRepo domain_finance_repo.WechatOrderRepository,
@@ -1190,7 +1234,14 @@ func NewContainer() *Container {
wechatPayService *payment.WechatPayService, wechatPayService *payment.WechatPayService,
logger *zap.Logger, logger *zap.Logger,
) *component_report.ComponentReportHandler { ) *component_report.ComponentReportHandler {
return component_report.NewComponentReportHandler(productRepo, docRepo, apiConfigRepo, componentReportRepo, rechargeRecordRepo, alipayOrderRepo, wechatOrderRepo, aliPayService, wechatPayService, logger) return component_report.NewComponentReportHandler(productRepo, docRepo, apiConfigRepo, componentReportRepo, purchaseOrderRepo, rechargeRecordRepo, alipayOrderRepo, wechatOrderRepo, aliPayService, wechatPayService, logger)
},
// 组件报告订单处理器
func(
componentReportOrderService *product.ComponentReportOrderService,
logger *zap.Logger,
) *handlers.ComponentReportOrderHandler {
return handlers.NewComponentReportOrderHandler(componentReportOrderService, logger)
}, },
// UI组件HTTP处理器 // UI组件HTTP处理器
func( func(
@@ -1215,6 +1266,8 @@ func NewContainer() *Container {
routes.NewProductRoutes, routes.NewProductRoutes,
// 产品管理员路由 // 产品管理员路由
routes.NewProductAdminRoutes, routes.NewProductAdminRoutes,
// 组件报告订单路由
routes.NewComponentReportOrderRoutes,
// UI组件路由 // UI组件路由
routes.NewUIComponentRoutes, routes.NewUIComponentRoutes,
// 文章路由 // 文章路由
@@ -1331,6 +1384,7 @@ func RegisterRoutes(
financeRoutes *routes.FinanceRoutes, financeRoutes *routes.FinanceRoutes,
productRoutes *routes.ProductRoutes, productRoutes *routes.ProductRoutes,
productAdminRoutes *routes.ProductAdminRoutes, productAdminRoutes *routes.ProductAdminRoutes,
componentReportOrderRoutes *routes.ComponentReportOrderRoutes,
uiComponentRoutes *routes.UIComponentRoutes, uiComponentRoutes *routes.UIComponentRoutes,
articleRoutes *routes.ArticleRoutes, articleRoutes *routes.ArticleRoutes,
announcementRoutes *routes.AnnouncementRoutes, announcementRoutes *routes.AnnouncementRoutes,
@@ -1352,12 +1406,8 @@ func RegisterRoutes(
financeRoutes.Register(router) financeRoutes.Register(router)
productRoutes.Register(router) productRoutes.Register(router)
productAdminRoutes.Register(router) productAdminRoutes.Register(router)
componentReportOrderRoutes.Register(router)
// UI组件路由需要特殊处理因为它需要管理员中间件 uiComponentRoutes.Register(router)
engine := router.GetEngine()
adminGroup := engine.Group("/api/v1/admin")
adminGroup.Use(adminAuth.Handle())
uiComponentRoutes.RegisterRoutes(adminGroup, adminAuth)
articleRoutes.Register(router) articleRoutes.Register(router)
announcementRoutes.Register(router) announcementRoutes.Register(router)

View File

@@ -170,6 +170,24 @@ type YYSYD50FReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"` IDCard string `json:"id_card" validate:"required,validIDCard"`
} }
type IVYZZQT3Req struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
}
type IVYZSFELReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
}
type IVYZBPQ2Req struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
}
type YYSYF7DBReq struct { type YYSYF7DBReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
StartDate string `json:"start_date" validate:"required,validDate" encrypt:"false"` StartDate string `json:"start_date" validate:"required,validDate" encrypt:"false"`
@@ -250,6 +268,90 @@ type QCXG7A2BReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"` IDCard string `json:"id_card" validate:"required,validIDCard"`
} }
type QCXG4896Req struct {
PlateNo string `json:"plate_no" validate:"required"`
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
}
type QCXG5F3AReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type QCXGGB2QReq struct {
PlateNo string `json:"plate_no" validate:"required"`
Name string `json:"name" validate:"required,min=1,validName"`
CarPlateType string `json:"carplate_type" validate:"required"`
}
type QCXGJJ2AReq struct {
VinCode string `json:"vin_code" validate:"required"`
EngineNumber string `json:"engine_number" validate:"omitempty"`
NoticeModel string `json:"notice_model" validate:"omitempty"`
}
type QCXG4I1ZReq struct {
VinCode string `json:"vin_code" validate:"required"`
}
type QCXG1H7YReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no" validate:"required"`
}
type QCXG1U4UReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no" validate:"omitempty"`
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
ImageURL string `json:"image_url" validate:"omitempty,url"`
RegURL string `json:"reg_url" validate:"omitempty,url"`
EngineNumber string `json:"engine_number" validate:"omitempty"`
}
type IVYZ0S0DReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type IVYZ1J7HReq struct {
PlateNo string `json:"plate_no" validate:"required"`
Name string `json:"name" validate:"required,min=1,validName"`
CarPlateType string `json:"carplate_type" validate:"required"`
}
type QCXG3Z3LReq struct {
VinCode string `json:"vin_code" validate:"required"`
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
ImageURL string `json:"image_url" validate:"omitempty,url"`
PlateNo string `json:"plate_no" validate:"omitempty"`
EngineNumber string `json:"engine_number" validate:"omitempty"`
}
type QCXG2T6SReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no" validate:"omitempty"`
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
ImageURL string `json:"image_url" validate:"required,url"`
}
type QCXGYTS2Req struct {
VinCode string `json:"vin_code" validate:"omitempty"`
PlateNo string `json:"plate_no" validate:"omitempty"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type QCXGGJ3AReq struct {
VinCode string `json:"vin_code" validate:"required"`
}
type QCXGP00WReq struct {
VinCode string `json:"vin_code" validate:"required"`
PlateNo string `json:"plate_no" validate:"required"`
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
VlPhotoData string `json:"vlphoto_data" validate:"required,validBase64Image"`
}
type QCXG4D2EReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
UserType string `json:"user_type" validate:"required,oneof=1 2 3"`
}
type COMENT01Req struct { type COMENT01Req struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
EntCode string `json:"ent_code" validate:"required,validUSCI"` EntCode string `json:"ent_code" validate:"required,validUSCI"`
@@ -361,6 +463,23 @@ type QYGL5A3CReq struct {
PageNum int64 `json:"page_num" validate:"omitempty,min=1"` PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
} }
type QYGL2naoReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
PageNum int64 `json:"page_num" validate:"omitempty,min=1"`
}
type QYGLNIO8Req struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
}
type QYGLP0HTReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"`
Flag string `json:"flag" validate:"omitempty"`
Dir string `json:"dir" validate:"omitempty,oneof=up down"`
MinPercent string `json:"min_percent" validate:"omitempty"`
MaxPercent string `json:"max_percent" validate:"omitempty"`
}
type QYGL8B4DReq struct { type QYGL8B4DReq struct {
EntCode string `json:"ent_code" validate:"required,validUSCI"` EntCode string `json:"ent_code" validate:"required,validUSCI"`
PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"` PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"`
@@ -614,6 +733,13 @@ type QYGL2S0WReq struct {
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"` EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
} }
// 全国企业司法模型服务查询_V1
type QYGL66SLReq struct {
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
AuthDate string `json:"auth_date" validate:"required,validAuthDate"`
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
}
type JRZQ2F8AReq struct { type JRZQ2F8AReq struct {
Name string `json:"name" validate:"required,min=1,validName"` Name string `json:"name" validate:"required,min=1,validName"`
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
@@ -701,6 +827,11 @@ type IVYZ6M8PReq struct {
Name string `json:"name" validate:"required,min=1,validName"` Name string `json:"name" validate:"required,min=1,validName"`
} }
type IVYZ9H2MReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type YYSY9E4AReq struct { type YYSY9E4AReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"` MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
} }

View File

@@ -18,6 +18,7 @@ import (
"tyapi-server/internal/domains/api/services/processors/yysy" "tyapi-server/internal/domains/api/services/processors/yysy"
"tyapi-server/internal/domains/product/services" "tyapi-server/internal/domains/product/services"
"tyapi-server/internal/infrastructure/external/alicloud" "tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/muzi"
"tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/westdex"
@@ -54,6 +55,7 @@ func NewApiRequestService(
alicloudService *alicloud.AlicloudService, alicloudService *alicloud.AlicloudService,
zhichaService *zhicha.ZhichaService, zhichaService *zhicha.ZhichaService,
xingweiService *xingwei.XingweiService, xingweiService *xingwei.XingweiService,
jiguangService *jiguang.JiguangService,
validator interfaces.RequestValidator, validator interfaces.RequestValidator,
productManagementService *services.ProductManagementService, productManagementService *services.ProductManagementService,
) *ApiRequestService { ) *ApiRequestService {
@@ -61,7 +63,7 @@ func NewApiRequestService(
combService := comb.NewCombService(productManagementService) combService := comb.NewCombService(productManagementService)
// 创建处理器依赖容器 // 创建处理器依赖容器
processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, validator, combService) processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, validator, combService)
// 统一注册所有处理器 // 统一注册所有处理器
registerAllProcessors(combService) registerAllProcessors(combService)
@@ -157,6 +159,10 @@ func registerAllProcessors(combService *comb.CombService) {
"QYGL5A9T": qygl.ProcessQYGL5A9TRequest, //全国企业各类工商风险统计数量查询 "QYGL5A9T": qygl.ProcessQYGL5A9TRequest, //全国企业各类工商风险统计数量查询
"QYGL2S0W": qygl.ProcessQYGL2S0WRequest, //失信被执行企业个人查询 "QYGL2S0W": qygl.ProcessQYGL2S0WRequest, //失信被执行企业个人查询
"QYGL5CMP": qygl.ProcessQYGL5CMPRequest, //企业五要素验证 "QYGL5CMP": qygl.ProcessQYGL5CMPRequest, //企业五要素验证
"QYGL66SL": qygl.ProcessQYGL66SLRequest, //全国企业司法模型服务查询_V1
"QYGL2NAO": qygl.ProcessQYGL2naoRequest, //股权变更
"QYGLNIO8": qygl.ProcessQYGLNIO8Request, //企业基本信息
"QYGLP0HT": qygl.ProcessQYGLP0HTRequest, //股权穿透
// YYSY系列处理器 // YYSY系列处理器
"YYSYD50F": yysy.ProcessYYSYD50FRequest, "YYSYD50F": yysy.ProcessYYSYD50FRequest,
@@ -206,6 +212,12 @@ func registerAllProcessors(combService *comb.CombService) {
"IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历) "IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历)
"IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型 "IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型
"IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书 "IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书
"IVYZ9H2M": ivyz.ProcessIVYZ9H2MRequest, //极光个人婚姻查询V2版
"IVYZZQT3": ivyz.ProcessIVYZZQT3Request, //人脸比对V3
"IVYZBPQ2": ivyz.ProcessIVYZBPQ2Request, //人脸比对V2
"IVYZSFEL": ivyz.ProcessIVYZSFELRequest, //全国自然人人像三要素核验_V1
"IVYZ0S0D": ivyz.ProcessIVYZ0S0DRequest, //劳动仲裁信息查询(个人版)
"IVYZ1J7H": ivyz.ProcessIVYZ1J7HRequest, //行驶证核查v2
// COMB系列处理器 - 只注册有自定义逻辑的组合包 // COMB系列处理器 - 只注册有自定义逻辑的组合包
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑重命名ApiCode "COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑重命名ApiCode
@@ -217,6 +229,20 @@ func registerAllProcessors(combService *comb.CombService) {
"QCXG9P1C": qcxg.ProcessQCXG9P1CRequest, "QCXG9P1C": qcxg.ProcessQCXG9P1CRequest,
"QCXG8A3D": qcxg.ProcessQCXG8A3DRequest, "QCXG8A3D": qcxg.ProcessQCXG8A3DRequest,
"QCXG6B4E": qcxg.ProcessQCXG6B4ERequest, "QCXG6B4E": qcxg.ProcessQCXG6B4ERequest,
"QCXG4896": qcxg.ProcessQCXG4896Request,
"QCXG5F3A": qcxg.ProcessQCXG5F3ARequest, // 极光个人车辆查询
"QCXG4D2E": qcxg.ProcessQCXG4D2ERequest, // 极光名下车辆数量查询
"QCXGJJ2A": qcxg.ProcessQCXGJJ2ARequest, // vin码查车辆信息(一对多)
"QCXGGJ3A": qcxg.ProcessQCXGGJ3ARequest, // 车辆vin码查询号牌
"QCXGYTS2": qcxg.ProcessQCXGYTS2Request, // 车辆二要素核验v2
"QCXGP00W": qcxg.ProcessQCXGP00WRequest, // 车辆出险详版查询
"QCXGGB2Q": qcxg.ProcessQCXGGB2QRequest, // 车辆二要素核验V1
"QCXG4I1Z": qcxg.ProcessQCXG4I1ZRequest, // 车辆过户详版查询
"QCXG1H7Y": qcxg.ProcessQCXG1H7YRequest, // 车辆过户简版查询
"QCXG3Z3L": qcxg.ProcessQCXG3Z3LRequest, // 车辆维保详细版查询
"QCXG3Y6B": qcxg.ProcessQCXG3Y6BRequest, // 车辆维保简版查询
"QCXG2T6S": qcxg.ProcessQCXG2T6SRequest, // 车辆里程记录(品牌查询)
"QCXG1U4U": qcxg.ProcessQCXG1U4URequest, // 车辆里程记录(混合查询)
// DWBG系列处理器 - 多维报告 // DWBG系列处理器 - 多维报告
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest, "DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,

View File

@@ -198,7 +198,33 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历) "IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型 "IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书 "IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
"IVYZ9H2M": &dto.IVYZ9H2MReq{}, //极光个人婚姻查询V2版
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证 "QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
"QCXG4896": &dto.QCXG4896Req{}, //网约车风险查询
"IVYZZQT3": &dto.IVYZZQT3Req{}, //人脸比对V3
"IVYZBPQ2": &dto.IVYZBPQ2Req{}, //人脸比对V2
"IVYZSFEL": &dto.IVYZSFELReq{}, //全国自然人人像三要素核验_V1
"QYGL66SL": &dto.QYGL66SLReq{}, //全国企业司法模型服务查询_V1
"QCXG5F3A": &dto.QCXG5F3AReq{}, //极光个人车辆查询
"QCXG4D2E": &dto.QCXG4D2EReq{}, //极光名下车辆数量查询
"QYGLP0HT": &dto.QYGLP0HTReq{}, //股权穿透
"QYGL2NAO": &dto.QYGL2naoReq{}, //股权变更
"QYGLNIO8": &dto.QYGLNIO8Req{}, //企业基本信息
"QYGL4B2E": &dto.QYGL5A3CReq{}, //税收违法
"QYGL7D9A": &dto.QYGL5A3CReq{}, //欠税公告
"IVYZ0S0D": &dto.IVYZ0S0DReq{}, //劳动仲裁信息查询(个人版)
"IVYZ1J7H": &dto.IVYZ1J7HReq{}, //行驶证核查v2
"QCXGJJ2A": &dto.QCXGJJ2AReq{}, //vin码查车辆信息(一对多)
"QCXGGJ3A": &dto.QCXGGJ3AReq{}, //车辆vin码查询号牌
"QCXGYTS2": &dto.QCXGYTS2Req{}, //车辆二要素核验v2
"QCXGP00W": &dto.QCXGP00WReq{}, //车辆出险详版查询
"QCXGGB2Q": &dto.QCXGGB2QReq{}, //车辆二要素核验V1
"QCXG4I1Z": &dto.QCXG4I1ZReq{}, //车辆过户详版查询
"QCXG1H7Y": &dto.QCXG1H7YReq{}, //车辆过户简版查询
"QCXG3Z3L": &dto.QCXG3Z3LReq{}, //车辆维保详细版查询
"QCXG3Y6B": &dto.QCXG1U4UReq{}, //车辆维保简版查询
"QCXG2T6S": &dto.QCXG2T6SReq{}, //车辆里程记录(品牌查询)
"QCXG1U4U": &dto.QCXG1U4UReq{}, //车辆里程记录(混合查询)
} }
// 优先返回已配置的DTO // 优先返回已配置的DTO
@@ -321,6 +347,7 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string
case strings.HasPrefix(rule, "oneof="): case strings.HasPrefix(rule, "oneof="):
values := strings.TrimPrefix(rule, "oneof=") values := strings.TrimPrefix(rule, "oneof=")
frontendRules = append(frontendRules, "可选值: "+values) frontendRules = append(frontendRules, "可选值: "+values)
} }
} }
@@ -350,6 +377,8 @@ func (s *FormConfigServiceImpl) getFieldType(fieldType reflect.Type, validation
return "select" return "select"
} else if strings.Contains(validation, "Base64图片") || strings.Contains(validation, "base64") { } else if strings.Contains(validation, "Base64图片") || strings.Contains(validation, "base64") {
return "textarea" return "textarea"
} else if strings.Contains(validation, "图片地址") {
return "url"
} }
return "text" return "text"
case reflect.Int64: case reflect.Int64:
@@ -399,6 +428,16 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
"owner_type": "企业主类型", "owner_type": "企业主类型",
"type": "查询类型", "type": "查询类型",
"query_reason_id": "查询原因ID", "query_reason_id": "查询原因ID",
"flag": "层次",
"dir": "方向",
"min_percent": "股权穿透比例下限",
"max_percent": "股权穿透比例上限",
"engine_number": "发动机号码",
"notice_model": "车辆型号",
"vlphoto_data": "行驶证图片",
"carplate_type": "车辆号牌类型",
"image_url": "行驶证图片地址",
"reg_url": "车辆登记证图片地址",
} }
if label, exists := labelMap[jsonTag]; exists { if label, exists := labelMap[jsonTag]; exists {
@@ -444,6 +483,16 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
"ownerType": "1", "ownerType": "1",
"type": "per", "type": "per",
"query_reason_id": "1", "query_reason_id": "1",
"flag": "4",
"dir": "down",
"min_percent": "0",
"max_percent": "1",
"engine_number": "1234567890",
"notice_model": "1",
"vlphoto_data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"carplate_type": "01",
"image_url": "https://example.com/images/driving_license.jpg",
"reg_url": "https://example.com/images/vehicle_registration.jpg",
} }
if example, exists := exampleMap[jsonTag]; exists { if example, exists := exampleMap[jsonTag]; exists {
@@ -498,6 +547,16 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
"ownerType": "请选择企业主类型", "ownerType": "请选择企业主类型",
"type": "请选择查询类型", "type": "请选择查询类型",
"query_reason_id": "请选择查询原因ID", "query_reason_id": "请选择查询原因ID",
"flag": "请输入层次最大4",
"dir": "请选择方向up-向上down-向下)",
"min_percent": "请输入股权穿透比例下限默认0",
"max_percent": "请输入股权穿透比例上限默认1",
"engine_number": "请输入发动机号码",
"notice_model": "请输入车辆型号",
"vlphoto_data": "请输入行驶证图片",
"carplate_type": "请选择车辆号牌类型01-大型汽车 02-小型汽车 03-使馆汽车 04-领馆汽车 05-境外汽车 06-外籍汽车 07-普通摩托车 08-轻便摩托车 09-使馆摩托车 10-领馆摩托车 11-境外摩托车 12-外籍摩托车 13-低速车 14-拖拉机 15-挂车 16-教练汽车 17-教练摩托车 20-临时入境汽车 21-临时入境摩托车 22-临时行驶车 23-警用汽车 24-警用摩托 51-新能源大型车 52-新能源小型车)",
"image_url": "请输入行驶证图片地址",
"reg_url": "请输入车辆登记证图片地址",
} }
if placeholder, exists := placeholderMap[jsonTag]; exists { if placeholder, exists := placeholderMap[jsonTag]; exists {
@@ -554,6 +613,16 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
"owner_type": "企业主类型编码1-法定代表人2-主要人员3-自然人股东4-法定代表人及自然人股东5-其他", "owner_type": "企业主类型编码1-法定代表人2-主要人员3-自然人股东4-法定代表人及自然人股东5-其他",
"type": "查询类型per-人员ent-企业 ", "type": "查询类型per-人员ent-企业 ",
"query_reason_id": "查询原因ID1-授信审批2-贷中管理3-贷后管理4-异议处理5-担保查询6-租赁资质审查7-融资租赁审批8-借贷撮合查询9-保险审批10-资质审核11-风控审核12-企业背调", "query_reason_id": "查询原因ID1-授信审批2-贷中管理3-贷后管理4-异议处理5-担保查询6-租赁资质审查7-融资租赁审批8-借贷撮合查询9-保险审批10-资质审核11-风控审核12-企业背调",
"flag": "层次最大4",
"dir": "方向up-向上穿透down-向下穿透",
"min_percent": "股权穿透比例下限大于等于默认为0支持小数点后两位以小数指代百分比",
"max_percent": "股权穿透比例上限小于等于默认为1支持小数点后两位以小数指代百分比",
"engine_number": "发动机号码",
"notice_model": "车辆型号",
"vlphoto_data": "行驶证图片:base64编码的图片数据仅支持JPG、BMP、PNG三种格式",
"carplate_type": "车辆号牌类型01-大型汽车02-小型汽车03-使馆汽车04-领馆汽车05-境外汽车06-外籍汽车07-普通摩托车08-轻便摩托车09-使馆摩托车10-领馆摩托车11-境外摩托车12-外籍摩托车13-低速车14-拖拉机15-挂车16-教练汽车17-教练摩托车20-临时入境汽车21-临时入境摩托车22-临时行驶车23-警用汽车24-警用摩托51-新能源大型车52-新能源小型车",
"image_url": "行驶证图片地址必填请提供行驶证的图片URL地址",
"reg_url": "车辆登记证图片地址非必填请提供车辆登记证的图片URL地址",
} }
if desc, exists := descMap[jsonTag]; exists { if desc, exists := descMap[jsonTag]; exists {

View File

@@ -5,6 +5,7 @@ import (
"tyapi-server/internal/application/api/commands" "tyapi-server/internal/application/api/commands"
"tyapi-server/internal/infrastructure/external/alicloud" "tyapi-server/internal/infrastructure/external/alicloud"
"tyapi-server/internal/infrastructure/external/muzi" "tyapi-server/internal/infrastructure/external/muzi"
"tyapi-server/internal/infrastructure/external/jiguang"
"tyapi-server/internal/infrastructure/external/tianyancha" "tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/xingwei" "tyapi-server/internal/infrastructure/external/xingwei"
@@ -32,6 +33,7 @@ type ProcessorDependencies struct {
AlicloudService *alicloud.AlicloudService AlicloudService *alicloud.AlicloudService
ZhichaService *zhicha.ZhichaService ZhichaService *zhicha.ZhichaService
XingweiService *xingwei.XingweiService XingweiService *xingwei.XingweiService
JiguangService *jiguang.JiguangService
Validator interfaces.RequestValidator Validator interfaces.RequestValidator
CombService CombServiceInterface // Changed to interface to break import cycle CombService CombServiceInterface // Changed to interface to break import cycle
Options *commands.ApiCallOptions // 添加Options支持 Options *commands.ApiCallOptions // 添加Options支持
@@ -47,6 +49,7 @@ func NewProcessorDependencies(
alicloudService *alicloud.AlicloudService, alicloudService *alicloud.AlicloudService,
zhichaService *zhicha.ZhichaService, zhichaService *zhicha.ZhichaService,
xingweiService *xingwei.XingweiService, xingweiService *xingwei.XingweiService,
jiguangService *jiguang.JiguangService,
validator interfaces.RequestValidator, validator interfaces.RequestValidator,
combService CombServiceInterface, // Changed to interface combService CombServiceInterface, // Changed to interface
) *ProcessorDependencies { ) *ProcessorDependencies {
@@ -58,6 +61,7 @@ func NewProcessorDependencies(
AlicloudService: alicloudService, AlicloudService: alicloudService,
ZhichaService: zhichaService, ZhichaService: zhichaService,
XingweiService: xingweiService, XingweiService: xingweiService,
JiguangService: jiguangService,
Validator: validator, Validator: validator,
CombService: combService, CombService: combService,
Options: nil, // 初始化为nil在调用时设置 Options: nil, // 初始化为nil在调用时设置

View File

@@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
if err := deps.Validator.ValidateStruct(paramsDto); err != nil { if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" { if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"{
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
} }
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/westdex"

View File

@@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
if err != nil { if err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" { if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"{
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
} }
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)

View File

@@ -0,0 +1,45 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessIVYZ0S0DRequest IVYZ0S0D API处理方法 - 劳动仲裁信息查询(个人版)
func ProcessIVYZ0S0DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZ0S0DReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"id": paramsDto.IDCard,
"name": paramsDto.Name,
}
// 调用极光API
respBytes, err := deps.JiguangService.CallAPI(ctx, "labor-arbitration-information", "labor-arbitration-information", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,46 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessIVYZ1J7HRequest IVYZ1J7H API处理方法 - 行驶证核查v2
func ProcessIVYZ1J7HRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZ1J7HReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"plate": paramsDto.PlateNo,
"plateType": paramsDto.CarPlateType,
"name": paramsDto.Name,
}
// 调用极光API
respBytes, err := deps.JiguangService.CallAPI(ctx, "labor-arbitration-information", "labor-arbitration-information", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -36,6 +36,11 @@ func ProcessIVYZ3P9MRequest(ctx context.Context, params []byte, deps *processors
if returnType == "" { if returnType == "" {
returnType = "1" returnType = "1"
} }
paramSign := map[string]interface{}{
"returnType": returnType,
"realName": encryptedName,
"certCode": encryptedCertCode,
}
reqData := map[string]interface{}{ reqData := map[string]interface{}{
"realName": encryptedName, "realName": encryptedName,
@@ -43,7 +48,8 @@ func ProcessIVYZ3P9MRequest(ctx context.Context, params []byte, deps *processors
"returnType": returnType, "returnType": returnType,
} }
respData, err := deps.MuziService.CallAPI(ctx, "PC0041", reqData)
respData, err := deps.MuziService.CallAPI(ctx, "PC0041", "/academic",reqData,paramSign)
if err != nil { if err != nil {
switch { switch {
case errors.Is(err, muzi.ErrDatasource): case errors.Is(err, muzi.ErrDatasource):

View File

@@ -0,0 +1,47 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessIVYZ9H2MRequest IVYZ9H2M API处理方法 - 极光个人婚姻查询V2版
func ProcessIVYZ9H2MRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZ9H2MReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"id_no": paramsDto.IDCard,
"name": paramsDto.Name,
}
// 调用极光API
// apiCode: marriage-single-v2 (用于请求头)
// apiPath: marriage/single-v2 (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "marriage-single-v2", "marriage/single-v2", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,51 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei"
)
// ProcessIVYZBPQ2Request IVYZBPQ2 人脸比对V2API处理方法
func ProcessIVYZBPQ2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZBPQ2Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求数据使用xingwei服务的正确字段名
reqData := map[string]interface{}{
"name": paramsDto.Name,
"idCardNum": paramsDto.IDCard,
"image": paramsDto.PhotoData,
}
// 调用行为数据API使用指定的project_id
projectID := "CDJ-1104321425593790464"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil {
if errors.Is(err, xingwei.ErrNotFound) {
// 查空情况,返回特定的查空错误
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
// 数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
// 系统错误
return nil, errors.Join(processors.ErrSystem, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,52 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei"
)
// ProcessIVYZSFELRequest IVYZSFEL 全国自然人人像三要素核验_V1API处理方法
func ProcessIVYZSFELRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZSFELReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求数据使用xingwei服务的正确字段名
reqData := map[string]interface{}{
"name": paramsDto.Name,
"idCardNum": paramsDto.IDCard,
"photo": paramsDto.PhotoData,
"authAuthorizeFileCode": paramsDto.AuthAuthorizeFileCode,
}
// 调用行为数据API使用指定的project_id
projectID := "CDJ-1068350101927161856"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil {
if errors.Is(err, xingwei.ErrNotFound) {
// 查空情况,返回特定的查空错误
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
// 数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
// 系统错误
return nil, errors.Join(processors.ErrSystem, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,51 @@
package ivyz
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei"
)
// ProcessIVYZZQT3Request IVYZZQT3 人脸比对V3API处理方法
func ProcessIVYZZQT3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.IVYZZQT3Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求数据使用xingwei服务的正确字段名
reqData := map[string]interface{}{
"name": paramsDto.Name,
"idCardNum": paramsDto.IDCard,
"image": paramsDto.PhotoData,
}
// 调用行为数据API使用指定的project_id
projectID := "CDJ-1104321430396268544"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil {
if errors.Is(err, xingwei.ErrNotFound) {
// 查空情况,返回特定的查空错误
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
// 数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
// 系统错误
return nil, errors.Join(processors.ErrSystem, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,47 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG1H7YRequest QCXG1H7Y API处理方法 - 车辆过户简版查询
func ProcessQCXG1H7YRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG1H7YReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"plateNumber": paramsDto.PlateNo,
}
// 调用极光API
// apiCode: car-vin (用于请求头)
// apiPath: car/car-vin (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-transfer", "vehicle/transfer", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,49 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG1U4URequest QCXG1U4U API处理方法 - 车辆里程记录(混合查询)
func ProcessQCXG1U4URequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG1U4UReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"licensePlate": paramsDto.PlateNo,
"callbackUrl": paramsDto.ReturnURL,
"imageUrl": paramsDto.ImageURL,
"regUrl": paramsDto.RegURL,
"engine": paramsDto.EngineNumber,
}
// 调用极光API
respBytes, err := deps.JiguangService.CallAPI(ctx, "car-mileage-b", "vehicle/car-mileage-b", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,47 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG2T6SRequest QCXG2T6S API处理方法 - 车辆里程记录(品牌查询)
func ProcessQCXG2T6SRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG2T6SReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"licensePlate": paramsDto.PlateNo,
"callbackUrl": paramsDto.ReturnURL,
"imageUrl": paramsDto.ImageURL,
}
// 调用极光API
respBytes, err := deps.JiguangService.CallAPI(ctx, "car-mileage-a", "vehicle/car-mileage-a", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,49 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG3Y6BRequest QCXG3Y6B API处理方法 - 车辆维保简版查询
func ProcessQCXG3Y6BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG1U4UReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"licensePlate": paramsDto.PlateNo,
"notifyUrl": paramsDto.ReturnURL,
"imageUrl": paramsDto.ImageURL,
"regUrl": paramsDto.RegURL,
"engine": paramsDto.EngineNumber,
}
// 调用极光API
respBytes, err := deps.JiguangService.CallAPI(ctx, "car-maintenance-info-v2", "vehicle/car-maintenance-info-v2", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,48 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG3Z3LRequest QCXG3Z3L API处理方法 - 车辆维保详细版查询
func ProcessQCXG3Z3LRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG3Z3LReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"notifyUrl": paramsDto.ReturnURL,
"imageUrl": paramsDto.ImageURL,
"plateNo": paramsDto.PlateNo,
"engine": paramsDto.EngineNumber,
}
// 调用极光API
respBytes, err := deps.JiguangService.CallAPI(ctx, "car-maintenance-info-v2", "vehicle/car-maintenance-info-v2", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,52 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"strings"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/muzi"
)
// ProcessQCXG4896MRequest QCXG4896 API处理方法 - 网约车风险查询
func ProcessQCXG4896Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG4896Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
paramSign := map[string]interface{}{
"paramName": "licenseNo",
"paramValue": paramsDto.PlateNo,
}
reqData := map[string]interface{}{
"paramName": "licenseNo",
"paramValue": paramsDto.PlateNo,
"startTime": strings.Split(paramsDto.AuthDate, "-")[0],
"endTime": strings.Split(paramsDto.AuthDate, "-")[1],
}
respData, err := deps.MuziService.CallAPI(ctx, "PC0031", "/hailingScoreBySearch", reqData,paramSign)
if err != nil {
switch {
case errors.Is(err, muzi.ErrDatasource):
return nil, errors.Join(processors.ErrDatasource, err)
case errors.Is(err, muzi.ErrSystem):
return nil, errors.Join(processors.ErrSystem, err)
default:
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respData, nil
}

View File

@@ -0,0 +1,49 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 极光名下车辆数量查询
func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG4D2EReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"idNum": paramsDto.IDCard,
"userType": paramsDto.UserType,
}
// 调用极光API
// apiCode: vehicle-inquiry-under-name (用于请求头)
// apiPath: vehicle/inquiry-under-name (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,46 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG4I1ZRequest QCXG4I1Z API处理方法 - 车辆过户详版查询
func ProcessQCXG4I1ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG4I1ZReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vim": paramsDto.VinCode,
}
// 调用极光API
// apiCode: car-vin (用于请求头)
// apiPath: car/car-vin (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "transfer-information", "vehicle/transfer-information", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,47 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 极光个人车辆查询
func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG5F3AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"id_card": paramsDto.IDCard,
}
// 调用极光API
// apiCode: vehicle-person-vehicles (用于请求头)
// apiPath: vehicle/person-vehicles (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-vehicles", "vehicle/person-vehicles", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -4,10 +4,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"strconv"
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha" "tyapi-server/internal/infrastructure/external/jiguang"
"github.com/tidwall/gjson"
) )
// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 // ProcessQCXG9P1CRequest QCXG9P1C API处理方法
@@ -21,44 +24,48 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) // 构建请求参数
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{ reqData := map[string]interface{}{
"idCard": encryptedIDCard, "id_card": paramsDto.IDCard,
"authorized": paramsDto.Authorized,
}
if paramsDto.Name != "" {
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData["name"] = encryptedName
}
// 如果传了 vehicleType则添加到请求数据中
if paramsDto.VehicleType != "" {
reqData["vehicleType"] = paramsDto.VehicleType
}
if paramsDto.UserType != "" {
reqData["userType"] = paramsDto.UserType
} }
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI051", reqData) // 调用极光API
// apiCode: vehicle-person-vehicles (用于请求头)
// apiPath: vehicle/person-vehicles (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-vehicles", "vehicle/person-vehicles", reqData)
if err != nil { if err != nil {
if errors.Is(err, zhicha.ErrDatasource) { // 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else { } else {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
} }
// 将响应数据转换为 JSON // 使用 gjson 检查并转换 vehicleCount
respBytes, err := json.Marshal(respData) vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount")
if err != nil { if vehicleCountResult.Exists() && vehicleCountResult.Type == gjson.String {
return nil, errors.Join(processors.ErrSystem, err) // 如果是字符串类型,转换为整数
vehicleCountInt, err := strconv.Atoi(vehicleCountResult.String())
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
// 解析 JSON 并修改 vehicleCount 字段
var respData map[string]interface{}
if err := json.Unmarshal(respBytes, &respData); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
respData["vehicleCount"] = vehicleCountInt
// 重新序列化为JSON并返回
resultBytes, err := json.Marshal(respData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return resultBytes, nil
} }
// 如果 vehicleCount 不存在或不是字符串,直接返回原始响应
return respBytes, nil return respBytes, nil
} }

View File

@@ -0,0 +1,80 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// CarPlateTypeMap 号牌类型代码到名称的映射
var CarPlateTypeMap = map[string]string{
"01": "大型汽车",
"02": "小型汽车",
"03": "使馆汽车",
"04": "领馆汽车",
"05": "境外汽车",
"06": "外籍汽车",
"07": "普通摩托车",
"08": "轻便摩托车",
"09": "使馆摩托车",
"10": "领馆摩托车",
"11": "境外摩托车",
"12": "外籍摩托车",
"13": "低速车",
"14": "拖拉机",
"15": "挂车",
"16": "教练汽车",
"17": "教练摩托车",
"20": "临时入境汽车",
"21": "临时入境摩托车",
"22": "临时行驶车",
"23": "警用汽车",
"24": "警用摩托",
"51": "新能源大型车",
"52": "新能源小型车",
}
// getCarPlateTypeName 根据号牌类型代码获取中文名称
func getCarPlateTypeName(code string) string {
if name, exists := CarPlateTypeMap[code]; exists {
return name
}
return code // 如果找不到,返回原值
}
// ProcessQCXGGB2QRequest QCXGGB2Q API处理方法 - 车辆二要素核验V1
func ProcessQCXGGB2QRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGGB2QReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数,将号牌类型代码转换为中文名称
reqData := map[string]interface{}{
"plateNumber": paramsDto.PlateNo,
"owner": paramsDto.Name,
"plateType": getCarPlateTypeName(paramsDto.CarPlateType),
}
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-factor-two-auth", "vehicle/factor-two-auth", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,46 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXGGJ3ARequest QCXGGJ3A API处理方法 - 车辆vin码查询号牌
func ProcessQCXGGJ3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGGJ3AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
}
// 调用极光API
// apiCode: car-vin (用于请求头)
// apiPath: car/car-vin (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "car-vin", "vehicle/car-vin", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,48 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXGJJ2ARequest QCXGJJ2A API处理方法 - vin码查车辆信息(一对多)
func ProcessQCXGJJ2ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGJJ2AReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"engineNumber": paramsDto.EngineNumber,
"noticeModel": paramsDto.NoticeModel,
}
// 调用极光API
// apiCode: carInfo-vin (用于请求头)
// apiPath: car/carInfo-vin (用于URL路径)
respBytes, err := deps.JiguangService.CallAPI(ctx, "carInfo-vin", "car/carInfo-vin", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,45 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXGP00WRequest QCXGP00W API处理方法 - 车辆出险详版查询
func ProcessQCXGP00WRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGP00WReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"licenseNo": paramsDto.PlateNo,
"notifyUrl": paramsDto.ReturnURL,
"image": paramsDto.VlPhotoData,
}
respBytes, err := deps.JiguangService.CallAPI(ctx, "car-accident-order-high", "car-accident-precision-order", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -0,0 +1,44 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/jiguang"
)
// ProcessQCXGYTS2Request QCXGYTS2 API处理方法 - 车辆二要素核验v2
func ProcessQCXGYTS2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGYTS2Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求参数
reqData := map[string]interface{}{
"vin": paramsDto.VinCode,
"plate": paramsDto.PlateNo,
"name": paramsDto.Name,
}
respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-and-vehicle-verification-v2", "vehicle/person-and-vehicle-verification-v2", reqData)
if err != nil {
// 根据错误类型返回相应的错误
if errors.Is(err, jiguang.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, jiguang.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
}
}
// 极光服务已经返回了 data 字段的 JSON直接返回即可
return respBytes, nil
}

View File

@@ -119,13 +119,3 @@ func ProcessQYGL23T7Request(ctx context.Context, params []byte, deps *processors
} }
} }
} }
// createStatusResponse 创建状态响应
func createStatusResponse(status int) []byte {
response := map[string]interface{}{
"status": status,
}
respBytes, _ := json.Marshal(response)
return respBytes
}

View File

@@ -0,0 +1,58 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"strconv"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
)
// ProcessQYGL2naoRequest QYGL2NAO API处理方法 - 股权变更
func ProcessQYGL2naoRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL2naoReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 设置默认值
pageSize := paramsDto.PageSize
if pageSize == 0 {
pageSize = int64(20)
}
pageNum := paramsDto.PageNum
if pageNum == 0 {
pageNum = int64(1)
}
// 构建API调用参数
apiParams := map[string]string{
"keyword": paramsDto.EntCode,
"pageSize": strconv.FormatInt(pageSize, 10),
"pageNum": strconv.FormatInt(pageNum, 10),
}
// 调用天眼查API - 企业基本信息
response, err := deps.TianYanChaService.CallAPI(ctx, "holderChange", apiParams)
if err != nil {
return nil, convertTianYanChaError(err)
}
// 检查天眼查API调用是否成功
if !response.Success {
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
}
// 返回天眼查响应数据
respBytes, err := json.Marshal(response.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -24,6 +24,26 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
// 第一步:企业信息验证 - 调用天眼查API
_, err := verifyEnterpriseInfo(ctx, paramsDto, deps)
if err != nil {
// 企业信息验证失败,只返回简单的状态码
return createStatusResponse(1), nil
}
// 企业信息验证通过,继续个人信息验证
_, err = verifyPersonalInfo(ctx, paramsDto, deps)
if err != nil {
// 个人信息验证失败,只返回简单的状态码
return createStatusResponse(1), nil
}
// 两个验证都通过,只返回成功状态码
return createStatusResponse(0), nil
}
// verifyEnterpriseInfo 验证企业信息
func verifyEnterpriseInfo(ctx context.Context, paramsDto dto.QYGL5CMPReq, deps *processors.ProcessorDependencies) (map[string]interface{}, error) {
// 构建API调用参数 // 构建API调用参数
apiParams := map[string]string{ apiParams := map[string]string{
"code": paramsDto.EntCode, "code": paramsDto.EntCode,
@@ -39,45 +59,41 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
// 检查天眼查API调用是否成功 // 检查天眼查API调用是否成功
if !response.Success { if !response.Success {
// 天眼查API调用失败返回企业信息校验不通过 return nil, fmt.Errorf("天眼查API调用失败")
return createStatusResponsess(1), nil
} }
// 解析天眼查响应数据 // 解析天眼查响应数据
if response.Data == nil { if response.Data == nil {
// 天眼查响应数据为空,返回企业信息校验不通过 return nil, fmt.Errorf("天眼查响应数据为空")
return createStatusResponsess(1), nil
} }
// 将response.Data转换为JSON字符串然后使用gjson解析 // 将response.Data转换为JSON字符串然后使用gjson解析
dataBytes, err := json.Marshal(response.Data) dataBytes, err := json.Marshal(response.Data)
if err != nil { if err != nil {
// 数据序列化失败,返回企业信息校验不通过 return nil, fmt.Errorf("数据序列化失败")
return createStatusResponsess(1), nil
} }
// 使用gjson解析嵌套的data.result.data字段 // 使用gjson解析嵌套的data.result.data字段
result := gjson.GetBytes(dataBytes, "result") result := gjson.GetBytes(dataBytes, "result")
if !result.Exists() { if !result.Exists() {
// 字段不存在,返回企业信息校验不通过 return nil, fmt.Errorf("result字段不存在")
return createStatusResponsess(1), nil
} }
// 检查data.result.data是否等于1 // 检查data.result.data是否等于1
if result.Int() != 1 { if result.Int() != 1 {
// 不等于1返回企业信息验不通过 return nil, fmt.Errorf("企业信息验不通过")
return createStatusResponsess(1), nil
} }
// 天眼查三要素验证通过,继续调用星维身份证三要素验证 // 构建天眼查API返回的数据结构
if err := json.Unmarshal(params, &paramsDto); err != nil { return map[string]interface{}{
return nil, errors.Join(processors.ErrSystem, err) "success": response.Success,
} "message": response.Message,
"data": response.Data,
if err := deps.Validator.ValidateStruct(paramsDto); err != nil { }, nil
return nil, errors.Join(processors.ErrInvalidParam, err) }
}
// verifyPersonalInfo 验证个人信息并返回API数据
func verifyPersonalInfo(ctx context.Context, paramsDto dto.QYGL5CMPReq, deps *processors.ProcessorDependencies) (map[string]interface{}, error) {
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名 // 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
reqData := map[string]interface{}{ reqData := map[string]interface{}{
"name": paramsDto.LegalPerson, "name": paramsDto.LegalPerson,
@@ -89,6 +105,7 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
projectID := "CDJ-1100244702166183936" projectID := "CDJ-1100244702166183936"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData) respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil { if err != nil {
// 个人信息验证失败,返回错误状态
if errors.Is(err, xingwei.ErrNotFound) { if errors.Is(err, xingwei.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err) return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) { } else if errors.Is(err, xingwei.ErrDatasource) {
@@ -106,42 +123,6 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("解析星维API响应失败: %w", err)) return nil, errors.Join(processors.ErrSystem, fmt.Errorf("解析星维API响应失败: %w", err))
} }
// 构建天眼查API返回的数据结构 // 返回星维API的全部数据
tianYanChaData := map[string]interface{}{ return xingweiData, nil
"success": response.Success,
"message": response.Message,
"data": response.Data,
}
// 解析status响应将JSON字节解析为对象
statusBytes := createStatusResponsess(0) // 验证通过status为0
var statusData map[string]interface{}
if err := json.Unmarshal(statusBytes, &statusData); err != nil {
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("解析status响应失败: %w", err))
}
// 合并两个API的返回数据
mergedData := map[string]interface{}{
"Personal Information": xingweiData, // 星维API返回的数据
"Enterprise Information": tianYanChaData, // 天眼查API返回的数据
"status": statusData, // 解析后的status对象
}
// 将合并后的数据序列化为JSON
mergedBytes, err := json.Marshal(mergedData)
if err != nil {
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("合并数据序列化失败: %w", err))
}
return mergedBytes, nil
}
// createStatusResponsess 创建状态响应
func createStatusResponsess(status int) []byte {
response := map[string]interface{}{
"status": status,
}
respBytes, _ := json.Marshal(response)
return respBytes
} }

View File

@@ -0,0 +1,53 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei"
)
// Processqygl66slRequest QYGL66SL API处理方法 - 全国企业司法模型服务查询_V1
func ProcessQYGL66SLRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGL66SLReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建请求数据,
reqData := map[string]interface{}{
"orgName": paramsDto.EntName,
"inquiredAuth": "authed:" + paramsDto.AuthDate,
"uscc": paramsDto.EntCode,
"authAuthorizeFileCode": paramsDto.AuthAuthorizeFileCode,
}
// 调用行为数据API使用指定的project_id
projectID := "CDJ-1068350101956521984"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil {
if errors.Is(err, xingwei.ErrNotFound) {
// 查空情况,返回特定的查空错误
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
// 数据源错误
return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
// 系统错误
return nil, errors.Join(processors.ErrSystem, err)
} else {
// 其他未知错误
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -0,0 +1,46 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
)
// ProcessQYGLNIO8Request QYGLNIO8 API处理方法 - 企业基本信息
func ProcessQYGLNIO8Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGLNIO8Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建API调用参数
apiParams := map[string]string{
"keyword": paramsDto.EntCode,
}
// 调用天眼查API - 企业基本信息
response, err := deps.TianYanChaService.CallAPI(ctx, "baseinfo", apiParams)
if err != nil {
return nil, convertTianYanChaError(err)
}
// 检查天眼查API调用是否成功
if !response.Success {
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
}
// 返回天眼查响应数据
respBytes, err := json.Marshal(response.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,68 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
)
// ProcessQYGLP0HTRequest QYGLP0HT API处理方法 - 股权穿透
func ProcessQYGLP0HTRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGLP0HTReq
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 设置默认值
flag := paramsDto.Flag
if flag == "" {
flag = "4"
}
dir := paramsDto.Dir
if dir == "" {
dir = "down"
}
minPercent := paramsDto.MinPercent
if minPercent == "" {
minPercent = "0"
}
maxPercent := paramsDto.MaxPercent
if maxPercent == "" {
maxPercent = "1"
}
// 构建API调用参数
apiParams := map[string]string{
"keyword": paramsDto.EntCode,
"flag": flag,
"dir": dir,
"minPercent": minPercent,
"maxPercent": maxPercent,
}
// 调用天眼查API - 企股权穿透
response, err := deps.TianYanChaService.CallAPI(ctx, "investtree", apiParams)
if err != nil {
return nil, convertTianYanChaError(err)
}
// 检查天眼查API调用是否成功
if !response.Success {
return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message))
}
// 返回天眼查响应数据
respBytes, err := json.Marshal(response.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,12 @@
package qygl
import "encoding/json"
// createStatusResponse 创建状态响应
func createStatusResponse(status int) []byte {
response := map[string]interface{}{
"status": status,
}
respBytes, _ := json.Marshal(response)
return respBytes
}

View File

@@ -4,12 +4,38 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"strings"
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/westdex" "tyapi-server/internal/infrastructure/external/xingwei"
) )
// XingweiResponseData 星维数据源返回的数据结构
type XingweiResponseData struct {
OrderNo string `json:"orderNo"`
HandleTime string `json:"handleTime"`
Type string `json:"type"`
Result string `json:"result"`
Gender string `json:"gender"`
Age string `json:"age"`
Remark string `json:"remark"`
}
// YYSY09CDResponse 原来的返回结构
type YYSY09CDResponse struct {
Code string `json:"code"`
Data YYSY09CDResponseData `json:"data"`
}
// YYSY09CDResponseData 原来的返回数据结构
type YYSY09CDResponseData struct {
Msg string `json:"msg"`
PhoneType string `json:"phoneType"`
Code int `json:"code"`
EncryptType string `json:"encryptType"`
}
// ProcessYYSY09CDRequest YYSY09CD API处理方法 // ProcessYYSY09CDRequest YYSY09CD API处理方法
func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.YYSY09CDReq var paramsDto dto.YYSY09CDReq
@@ -21,38 +47,98 @@ func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) // 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedIDCard, err := deps.WestDexService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.WestDexService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
reqData := map[string]interface{}{ reqData := map[string]interface{}{
"data": map[string]interface{}{ "name": paramsDto.Name,
"name": encryptedName, "idCardNum": paramsDto.IDCard,
"idNo": encryptedIDCard, "phoneNumber": paramsDto.MobileNo,
"phone": encryptedMobileNo,
"phoneType": paramsDto.MobileType,
},
} }
respBytes, err := deps.WestDexService.CallAPI(ctx, "G16BJ02", reqData) // 调用行为数据API使用指定的project_id
projectID := "CDJ-1100244697766359040"
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
if err != nil { if err != nil {
if errors.Is(err, westdex.ErrDatasource) { if errors.Is(err, xingwei.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
return nil, errors.Join(processors.ErrSystem, err)
} else { } else {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
} }
return respBytes, nil // 解析星维返回的数据
var xingweiData XingweiResponseData
if err := json.Unmarshal(respBytes, &xingweiData); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
// 转换为原来的格式
response := convertToOriginalFormat(xingweiData, paramsDto.MobileType)
// 序列化为JSON
resultBytes, err := json.Marshal(response)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return resultBytes, nil
}
// convertToOriginalFormat 将星维数据源返回的数据转换为原来的格式
func convertToOriginalFormat(xingweiData XingweiResponseData, mobileType string) YYSY09CDResponse {
// 转换 result 到 code
var code string
var codeInt int
switch xingweiData.Result {
case "01": // 一致
code = "1000"
codeInt = 1000
case "02": // 不一致
code = "1001"
codeInt = 1001
case "03", "04": // 不确定或失败/虚拟号 -> 查无
code = "1002"
codeInt = 1002
default:
// 默认查无
code = "1002"
codeInt = 1002
}
// 从 remark 提取 msg去掉"认证"前缀
msg := xingweiData.Remark
if strings.HasPrefix(msg, "认证") {
msg = strings.TrimPrefix(msg, "认证")
}
// 转换 type 到 phoneType
// 如果请求参数中有 mobileType优先使用否则从返回的 type 转换
phoneType := mobileType
if phoneType == "" {
switch xingweiData.Type {
case "1": // 移动
phoneType = "CMCC"
case "2": // 联通
phoneType = "CUCC"
case "3": // 电信
phoneType = "CTCC"
case "4": // 广电
phoneType = "CBN"
default:
phoneType = ""
}
}
return YYSY09CDResponse{
Code: code,
Data: YYSY09CDResponseData{
Msg: msg,
PhoneType: phoneType,
Code: codeInt,
EncryptType: "MD5",
},
}
} }

View File

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

View File

@@ -27,7 +27,7 @@ func ProcessYYSY6D9ARequest(ctx context.Context, params []byte, deps *processors
} }
reqData := map[string]interface{}{ reqData := map[string]interface{}{
"phone": encryptedMobileNo, "phone": encryptedMobileNo,
} }
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI030", reqData) respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI030", reqData)

View File

@@ -71,7 +71,7 @@ func (s *EnterpriseInfoSubmitRecordService) Save(ctx context.Context, enterprise
return s.repositories.Create(ctx, enterpriseInfoSubmitRecord) return s.repositories.Create(ctx, enterpriseInfoSubmitRecord)
} }
// ValidateWithWestdex 调用QYGL23T7处理器验证企业信息 // ValidateWithWestdex 调用QYGL5CMP处理器验证企业信息
func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Context, info *value_objects.EnterpriseInfo) error { func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Context, info *value_objects.EnterpriseInfo) error {
if info == nil { if info == nil {
return errors.New("企业信息不能为空") return errors.New("企业信息不能为空")
@@ -89,12 +89,13 @@ func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Cont
// return nil // return nil
// } // }
// 构建QYGL23T7请求参数 // 构建QYGL5CMP请求参数
reqDto := dto.QYGL23T7Req{ reqDto := dto.QYGL5CMPReq{
EntName: info.CompanyName, EntName: info.CompanyName,
LegalPerson: info.LegalPersonName, LegalPerson: info.LegalPersonName,
EntCode: info.UnifiedSocialCode, EntCode: info.UnifiedSocialCode,
IDCard: info.LegalPersonID, IDCard: info.LegalPersonID,
MobileNo: info.LegalPersonPhone,
} }
// 序列化请求参数 // 序列化请求参数

View File

@@ -0,0 +1,180 @@
package entities
import (
"fmt"
"math/rand"
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
// PurchaseOrderStatus 购买订单状态枚举(通用)
type PurchaseOrderStatus string
const (
PurchaseOrderStatusCreated PurchaseOrderStatus = "created" // 已创建
PurchaseOrderStatusPaid PurchaseOrderStatus = "paid" // 已支付
PurchaseOrderStatusFailed PurchaseOrderStatus = "failed" // 支付失败
PurchaseOrderStatusCancelled PurchaseOrderStatus = "cancelled" // 已取消
PurchaseOrderStatusRefunded PurchaseOrderStatus = "refunded" // 已退款
PurchaseOrderStatusClosed PurchaseOrderStatus = "closed" // 已关闭
)
// PurchaseOrder 购买订单实体(统一表 ty_purchase_orders兼容多支付渠道
type PurchaseOrder struct {
// 基础标识
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"购买订单唯一标识"`
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"购买用户ID"`
OrderNo string `gorm:"type:varchar(64);not null;uniqueIndex" json:"order_no" comment:"商户订单号"`
TradeNo *string `gorm:"type:varchar(64);uniqueIndex" json:"trade_no,omitempty" comment:"第三方支付交易号"`
// 产品信息
ProductID string `gorm:"type:varchar(36);not null;index" json:"product_id" comment:"产品ID"`
ProductCode string `gorm:"type:varchar(50);not null" json:"product_code" comment:"产品编号"`
ProductName string `gorm:"type:varchar(200);not null" json:"product_name" comment:"产品名称"`
Category string `gorm:"type:varchar(50)" json:"category,omitempty" comment:"产品分类"`
// 订单信息
Subject string `gorm:"type:varchar(200);not null" json:"subject" comment:"订单标题"`
Amount decimal.Decimal `gorm:"type:decimal(20,8);not null" json:"amount" comment:"订单金额"`
PayAmount *decimal.Decimal `gorm:"type:decimal(20,8)" json:"pay_amount,omitempty" comment:"实际支付金额"`
Status PurchaseOrderStatus `gorm:"type:varchar(20);not null;default:'created';index" json:"status" comment:"订单状态"`
Platform string `gorm:"type:varchar(20);not null" json:"platform" comment:"下单平台app/h5/pc/wx_h5/wx_mini等"`
PayChannel string `gorm:"type:varchar(20);default:'alipay';index" json:"pay_channel" comment:"支付渠道alipay/wechat"`
PaymentType string `gorm:"type:varchar(20);not null" json:"payment_type" comment:"支付类型alipay, wechat, free"`
// 支付渠道返回信息
BuyerID string `gorm:"type:varchar(64)" json:"buyer_id,omitempty" comment:"买家ID支付渠道方"`
SellerID string `gorm:"type:varchar(64)" json:"seller_id,omitempty" comment:"卖家ID支付渠道方"`
ReceiptAmount decimal.Decimal `gorm:"type:decimal(20,8)" json:"receipt_amount,omitempty" comment:"实收金额"`
// 回调信息
NotifyTime *time.Time `gorm:"index" json:"notify_time,omitempty" comment:"异步通知时间"`
ReturnTime *time.Time `gorm:"index" json:"return_time,omitempty" comment:"同步返回时间"`
PayTime *time.Time `gorm:"index" json:"pay_time,omitempty" comment:"支付完成时间"`
// 文件信息
FilePath *string `gorm:"type:varchar(500)" json:"file_path,omitempty" comment:"产品文件路径"`
FileSize *int64 `gorm:"type:bigint" json:"file_size,omitempty" comment:"文件大小(字节)"`
// 备注信息
Remark string `gorm:"type:varchar(500)" json:"remark,omitempty" comment:"备注信息"`
// 错误信息
ErrorCode string `gorm:"type:varchar(64)" json:"error_code,omitempty" comment:"错误码"`
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty" comment:"错误信息"`
// 时间戳字段
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
}
// TableName 指定数据库表名
func (PurchaseOrder) TableName() string {
return "ty_purchase_orders"
}
// BeforeCreate GORM钩子创建前自动生成UUID和订单号
func (p *PurchaseOrder) BeforeCreate(tx *gorm.DB) error {
if p.ID == "" {
p.ID = uuid.New().String()
}
if p.OrderNo == "" {
p.OrderNo = generatePurchaseOrderNo()
}
return nil
}
// generatePurchaseOrderNo 生成购买订单号
func generatePurchaseOrderNo() string {
// 使用时间戳+随机数生成唯一订单号例如PO202312200001
timestamp := time.Now().Format("20060102")
random := fmt.Sprintf("%04d", rand.Intn(9999))
return fmt.Sprintf("PO%s%s", timestamp, random)
}
// IsCreated 检查是否为已创建状态
func (p *PurchaseOrder) IsCreated() bool {
return p.Status == PurchaseOrderStatusCreated
}
// IsPaid 检查是否为已支付状态
func (p *PurchaseOrder) IsPaid() bool {
return p.Status == PurchaseOrderStatusPaid
}
// IsFailed 检查是否为支付失败状态
func (p *PurchaseOrder) IsFailed() bool {
return p.Status == PurchaseOrderStatusFailed
}
// IsCancelled 检查是否为已取消状态
func (p *PurchaseOrder) IsCancelled() bool {
return p.Status == PurchaseOrderStatusCancelled
}
// IsRefunded 检查是否为已退款状态
func (p *PurchaseOrder) IsRefunded() bool {
return p.Status == PurchaseOrderStatusRefunded
}
// IsClosed 检查是否为已关闭状态
func (p *PurchaseOrder) IsClosed() bool {
return p.Status == PurchaseOrderStatusClosed
}
// MarkPaid 标记为已支付
func (p *PurchaseOrder) MarkPaid(tradeNo, buyerID, sellerID string, payAmount, receiptAmount decimal.Decimal) {
p.Status = PurchaseOrderStatusPaid
p.TradeNo = &tradeNo
p.BuyerID = buyerID
p.SellerID = sellerID
p.PayAmount = &payAmount
p.ReceiptAmount = receiptAmount
now := time.Now()
p.PayTime = &now
p.NotifyTime = &now
}
// MarkFailed 标记为支付失败
func (p *PurchaseOrder) MarkFailed(errorCode, errorMessage string) {
p.Status = PurchaseOrderStatusFailed
p.ErrorCode = errorCode
p.ErrorMessage = errorMessage
}
// MarkCancelled 标记为已取消
func (p *PurchaseOrder) MarkCancelled() {
p.Status = PurchaseOrderStatusCancelled
}
// MarkRefunded 标记为已退款
func (p *PurchaseOrder) MarkRefunded() {
p.Status = PurchaseOrderStatusRefunded
}
// MarkClosed 标记为已关闭
func (p *PurchaseOrder) MarkClosed() {
p.Status = PurchaseOrderStatusClosed
}
// NewPurchaseOrder 通用工厂方法 - 创建购买订单(支持多支付渠道)
func NewPurchaseOrder(userID, productID, productCode, productName, subject string, amount decimal.Decimal, platform, payChannel, paymentType string) *PurchaseOrder {
return &PurchaseOrder{
ID: uuid.New().String(),
UserID: userID,
OrderNo: generatePurchaseOrderNo(),
ProductID: productID,
ProductCode: productCode,
ProductName: productName,
Subject: subject,
Amount: amount,
Status: PurchaseOrderStatusCreated,
Platform: platform,
PayChannel: payChannel,
PaymentType: paymentType,
}
}

View File

@@ -0,0 +1,63 @@
package repositories
import (
"context"
"time"
finance_entities "tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/shared/interfaces"
)
// PurchaseOrderRepository 购买订单仓储接口
type PurchaseOrderRepository interface {
// 创建订单
Create(ctx context.Context, order *finance_entities.PurchaseOrder) (*finance_entities.PurchaseOrder, error)
// 更新订单
Update(ctx context.Context, order *finance_entities.PurchaseOrder) error
// 根据ID获取订单
GetByID(ctx context.Context, id string) (*finance_entities.PurchaseOrder, error)
// 根据订单号获取订单
GetByOrderNo(ctx context.Context, orderNo string) (*finance_entities.PurchaseOrder, error)
// 根据用户ID获取订单列表
GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*finance_entities.PurchaseOrder, int64, error)
// 根据产品ID和用户ID获取订单
GetByUserIDAndProductID(ctx context.Context, userID, productID string) (*finance_entities.PurchaseOrder, error)
// 根据支付类型和第三方交易号获取订单
GetByPaymentTypeAndTransactionID(ctx context.Context, paymentType, transactionID string) (*finance_entities.PurchaseOrder, error)
// 根据交易号获取订单
GetByTradeNo(ctx context.Context, tradeNo string) (*finance_entities.PurchaseOrder, error)
// 更新支付状态
UpdatePaymentStatus(ctx context.Context, orderID string, status finance_entities.PurchaseOrderStatus, tradeNo *string, payAmount, receiptAmount *string, paymentTime *time.Time) error
// 获取用户已购买的产品编号列表
GetUserPurchasedProductCodes(ctx context.Context, userID string) ([]string, error)
// 检查用户是否已购买指定产品
HasUserPurchased(ctx context.Context, userID string, productCode string) (bool, error)
// 获取即将过期的订单(用于清理)
GetExpiringOrders(ctx context.Context, before time.Time, limit int) ([]*finance_entities.PurchaseOrder, error)
// 获取已过期订单(用于清理)
GetExpiredOrders(ctx context.Context, limit int) ([]*finance_entities.PurchaseOrder, error)
// 获取用户已支付的产品ID列表
GetUserPaidProductIDs(ctx context.Context, userID string) ([]string, error)
// 根据状态获取订单列表
GetByStatus(ctx context.Context, status finance_entities.PurchaseOrderStatus, limit, offset int) ([]*finance_entities.PurchaseOrder, int64, error)
// 根据筛选条件获取订单列表
GetByFilters(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*finance_entities.PurchaseOrder, error)
// 根据筛选条件统计订单数量
CountByFilters(ctx context.Context, filters map[string]interface{}) (int64, error)
}

View File

@@ -4,29 +4,30 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm" "gorm.io/gorm"
) )
// ComponentReportDownload 组件报告下载记录 // ComponentReportDownload 组件报告下载记录
type ComponentReportDownload struct { type ComponentReportDownload struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"下载记录ID"` ID string `gorm:"primaryKey;type:varchar(36)" comment:"下载记录ID"`
UserID string `gorm:"type:varchar(36);not null;index" comment:"用户ID"` UserID string `gorm:"type:varchar(36);not null;index" comment:"用户ID"`
ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"` ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"`
ProductCode string `gorm:"type:varchar(50);not null;index" comment:"产品编号"` ProductCode string `gorm:"type:varchar(50);not null;index" comment:"产品编号"`
SubProductIDs string `gorm:"type:text" comment:"产品ID列表JSON数组组合包使用"` ProductName string `gorm:"type:varchar(200);not null" comment:"产品名称"`
SubProductCodes string `gorm:"type:text" comment:"子产品编号列表JSON数组"` // 直接关联购买订单
DownloadPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"实际支付价格"` OrderID *string `gorm:"type:varchar(36);index" comment:"关联的购买订单ID"`
OriginalPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"原始总价"` OrderNumber *string `gorm:"type:varchar(64);index" comment:"关联的购买订单号"`
DiscountAmount decimal.Decimal `gorm:"type:decimal(10,2);default:0" comment:"减免金额"`
PaymentOrderID *string `gorm:"type:varchar(64);index" comment:"支付订单号(关联充值记录)"` // 组合包相关字段(从购买记录复制)
PaymentType *string `gorm:"type:varchar(20)" comment:"支付类型alipay, wechat"` SubProductIDs string `gorm:"type:text" comment:"子产品ID列表JSON数组组合包使用"`
PaymentStatus string `gorm:"type:varchar(20);default:'pending';index" comment:"支付状态pending, success, failed"` SubProductCodes string `gorm:"type:text" comment:"子产品编号列表JSON数组"`
FilePath *string `gorm:"type:varchar(500)" comment:"生成的ZIP文件路径用于二次下载"`
FileHash *string `gorm:"type:varchar(64)" comment:"文件哈希值(用于缓存验证)"` // 下载相关信息
DownloadCount int `gorm:"default:0" comment:"下载次数"` FilePath *string `gorm:"type:varchar(500)" comment:"生成的ZIP文件路径用于二次下载"`
LastDownloadAt *time.Time `comment:"最后下载时间"` FileHash *string `gorm:"type:varchar(64)" comment:"文件哈希值(用于缓存验证)"`
ExpiresAt *time.Time `gorm:"index" comment:"下载有效期支付成功后30天"` DownloadCount int `gorm:"default:0" comment:"下载次数"`
LastDownloadAt *time.Time `comment:"最后下载时间"`
ExpiresAt *time.Time `gorm:"index" comment:"下载有效期从创建日起30天"`
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"` CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
@@ -46,11 +47,6 @@ func (c *ComponentReportDownload) BeforeCreate(tx *gorm.DB) error {
return nil return nil
} }
// IsPaid 检查是否已支付
func (c *ComponentReportDownload) IsPaid() bool {
return c.PaymentStatus == "success"
}
// IsExpired 检查是否已过期 // IsExpired 检查是否已过期
func (c *ComponentReportDownload) IsExpired() bool { func (c *ComponentReportDownload) IsExpired() bool {
if c.ExpiresAt == nil { if c.ExpiresAt == nil {
@@ -61,5 +57,6 @@ func (c *ComponentReportDownload) IsExpired() bool {
// CanDownload 检查是否可以下载 // CanDownload 检查是否可以下载
func (c *ComponentReportDownload) CanDownload() bool { func (c *ComponentReportDownload) CanDownload() bool {
return c.IsPaid() && !c.IsExpired() // 下载记录存在即表示用户有下载权限,只需检查是否过期
return !c.IsExpired()
} }

View File

@@ -10,12 +10,13 @@ import (
// Subscription 订阅实体 // Subscription 订阅实体
type Subscription struct { type Subscription struct {
ID string `gorm:"primaryKey;type:varchar(36)" comment:"订阅ID"` ID string `gorm:"primaryKey;type:varchar(36)" comment:"订阅ID"`
UserID string `gorm:"type:varchar(36);not null;index" comment:"用户ID"` UserID string `gorm:"type:varchar(36);not null;index" comment:"用户ID"`
ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"` ProductID string `gorm:"type:varchar(36);not null;index" comment:"产品ID"`
Price decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"订阅价格"` Price decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"订阅价格"`
APIUsed int64 `gorm:"default:0" comment:"已使用API调用次数"` UIComponentPrice decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"UI组件价格组合包使用"`
Version int64 `gorm:"default:1" comment:"乐观锁版本号"` APIUsed int64 `gorm:"default:0" comment:"已使用API调用次数"`
Version int64 `gorm:"default:1" comment:"乐观锁版本号"`
// 关联关系 // 关联关系
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"` Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`

View File

@@ -9,22 +9,23 @@ import (
// UIComponent UI组件实体 // UIComponent UI组件实体
type UIComponent struct { type UIComponent struct {
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"组件ID"` ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"组件ID"`
ComponentCode string `gorm:"type:varchar(50);not null;uniqueIndex" json:"component_code" comment:"组件编码"` ComponentCode string `gorm:"type:varchar(50);not null;uniqueIndex" json:"component_code" comment:"组件编码"`
ComponentName string `gorm:"type:varchar(100);not null" json:"component_name" comment:"组件名称"` ComponentName string `gorm:"type:varchar(100);not null" json:"component_name" comment:"组件名称"`
Description string `gorm:"type:text" json:"description" comment:"组件描述"` Description string `gorm:"type:text" json:"description" comment:"组件描述"`
FilePath *string `gorm:"type:varchar(500)" json:"file_path" comment:"组件文件路径"` FilePath *string `gorm:"type:varchar(500)" json:"file_path" comment:"组件文件路径"`
FileHash *string `gorm:"type:varchar(64)" json:"file_hash" comment:"文件哈希值"` FileHash *string `gorm:"type:varchar(64)" json:"file_hash" comment:"文件哈希值"`
FileSize *int64 `gorm:"type:bigint" json:"file_size" comment:"文件大小"` FileSize *int64 `gorm:"type:bigint" json:"file_size" comment:"文件大小"`
FileType *string `gorm:"type:varchar(50)" json:"file_type" comment:"文件类型"` FileType *string `gorm:"type:varchar(50)" json:"file_type" comment:"文件类型"`
FolderPath *string `gorm:"type:varchar(500)" json:"folder_path" comment:"组件文件夹路径"` FolderPath *string `gorm:"type:varchar(500)" json:"folder_path" comment:"组件文件夹路径"`
IsExtracted bool `gorm:"default:false" json:"is_extracted" comment:"是否已解压"` IsExtracted bool `gorm:"default:false" json:"is_extracted" comment:"是否已解压"`
Version string `gorm:"type:varchar(20)" json:"version" comment:"组件版本"` FileUploadTime *time.Time `gorm:"type:timestamp" json:"file_upload_time" comment:"文件上传时间"`
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否启用"` Version string `gorm:"type:varchar(20)" json:"version" comment:"组件版本"`
SortOrder int `gorm:"default:0" json:"sort_order" comment:"排序"` IsActive bool `gorm:"default:true" json:"is_active" comment:"是否启用"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"` SortOrder int `gorm:"default:0" json:"sort_order" comment:"排序"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at" comment:"软删除时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at" comment:"软删除时间"`
} }
func (UIComponent) TableName() string { func (UIComponent) TableName() string {

View File

@@ -9,7 +9,7 @@ import (
// ComponentReportRepository 组件报告仓储接口 // ComponentReportRepository 组件报告仓储接口
type ComponentReportRepository interface { type ComponentReportRepository interface {
// 创建下载记录 // 创建下载记录
CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error) Create(ctx context.Context, download *entities.ComponentReportDownload) error
// 更新下载记录 // 更新下载记录
UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error
@@ -20,6 +20,15 @@ type ComponentReportRepository interface {
// 获取用户的下载记录列表 // 获取用户的下载记录列表
GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error) GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error)
// 获取用户有效的下载记录
GetActiveDownload(ctx context.Context, userID, productID string) (*entities.ComponentReportDownload, error)
// 更新下载记录文件路径
UpdateFilePath(ctx context.Context, downloadID, filePath string) error
// 增加下载次数
IncrementDownloadCount(ctx context.Context, downloadID string) error
// 检查用户是否已下载过指定产品编号的组件 // 检查用户是否已下载过指定产品编号的组件
HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error) HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error)

View File

@@ -104,9 +104,10 @@ func (s *ProductSubscriptionService) CreateSubscription(ctx context.Context, use
// 创建订阅 // 创建订阅
subscription := &entities.Subscription{ subscription := &entities.Subscription{
UserID: userID, UserID: userID,
ProductID: productID, ProductID: productID,
Price: product.Price, Price: product.Price,
UIComponentPrice: product.UIComponentPrice,
} }
createdSubscription, err := s.subscriptionRepo.Create(ctx, *subscription) createdSubscription, err := s.subscriptionRepo.Create(ctx, *subscription)
@@ -253,7 +254,7 @@ func (s *ProductSubscriptionService) IncrementSubscriptionAPIUsage(ctx context.C
// GetSubscriptionStats 获取订阅统计信息 // GetSubscriptionStats 获取订阅统计信息
func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (map[string]interface{}, error) { func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (map[string]interface{}, error) {
stats := make(map[string]interface{}) stats := make(map[string]interface{})
// 获取总订阅数 // 获取总订阅数
totalSubscriptions, err := s.subscriptionRepo.Count(ctx, interfaces.CountOptions{}) totalSubscriptions, err := s.subscriptionRepo.Count(ctx, interfaces.CountOptions{})
if err != nil { if err != nil {
@@ -261,7 +262,7 @@ func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (
return nil, fmt.Errorf("获取订阅总数失败: %w", err) return nil, fmt.Errorf("获取订阅总数失败: %w", err)
} }
stats["total_subscriptions"] = totalSubscriptions stats["total_subscriptions"] = totalSubscriptions
// 获取总收入 // 获取总收入
totalRevenue, err := s.subscriptionRepo.GetTotalRevenue(ctx) totalRevenue, err := s.subscriptionRepo.GetTotalRevenue(ctx)
if err != nil { if err != nil {
@@ -269,30 +270,30 @@ func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (
return nil, fmt.Errorf("获取总收入失败: %w", err) return nil, fmt.Errorf("获取总收入失败: %w", err)
} }
stats["total_revenue"] = totalRevenue stats["total_revenue"] = totalRevenue
return stats, nil return stats, nil
} }
// GetUserSubscriptionStats 获取用户订阅统计信息 // GetUserSubscriptionStats 获取用户订阅统计信息
func (s *ProductSubscriptionService) GetUserSubscriptionStats(ctx context.Context, userID string) (map[string]interface{}, error) { func (s *ProductSubscriptionService) GetUserSubscriptionStats(ctx context.Context, userID string) (map[string]interface{}, error) {
stats := make(map[string]interface{}) stats := make(map[string]interface{})
// 获取用户订阅数 // 获取用户订阅数
userSubscriptions, err := s.subscriptionRepo.FindByUserID(ctx, userID) userSubscriptions, err := s.subscriptionRepo.FindByUserID(ctx, userID)
if err != nil { if err != nil {
s.logger.Error("获取用户订阅失败", zap.Error(err)) s.logger.Error("获取用户订阅失败", zap.Error(err))
return nil, fmt.Errorf("获取用户订阅失败: %w", err) return nil, fmt.Errorf("获取用户订阅失败: %w", err)
} }
// 计算用户总收入 // 计算用户总收入
var totalRevenue float64 var totalRevenue float64
for _, subscription := range userSubscriptions { for _, subscription := range userSubscriptions {
totalRevenue += subscription.Price.InexactFloat64() totalRevenue += subscription.Price.InexactFloat64()
} }
stats["total_subscriptions"] = int64(len(userSubscriptions)) stats["total_subscriptions"] = int64(len(userSubscriptions))
stats["total_revenue"] = totalRevenue stats["total_revenue"] = totalRevenue
return stats, nil return stats, nil
} }
@@ -303,20 +304,47 @@ func (s *ProductSubscriptionService) UpdateSubscriptionPrice(ctx context.Context
if err != nil { if err != nil {
return fmt.Errorf("订阅不存在: %w", err) return fmt.Errorf("订阅不存在: %w", err)
} }
// 更新价格 // 更新价格
subscription.Price = decimal.NewFromFloat(newPrice) subscription.Price = decimal.NewFromFloat(newPrice)
subscription.Version++ // 增加版本号 subscription.Version++ // 增加版本号
// 保存更新 // 保存更新
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil { if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
s.logger.Error("更新订阅价格失败", zap.Error(err)) s.logger.Error("更新订阅价格失败", zap.Error(err))
return fmt.Errorf("更新订阅价格失败: %w", err) return fmt.Errorf("更新订阅价格失败: %w", err)
} }
s.logger.Info("订阅价格更新成功", s.logger.Info("订阅价格更新成功",
zap.String("subscription_id", subscriptionID), zap.String("subscription_id", subscriptionID),
zap.Float64("new_price", newPrice)) zap.Float64("new_price", newPrice))
return nil
}
// UpdateSubscriptionPriceWithUIComponent 更新订阅价格和UI组件价格
func (s *ProductSubscriptionService) UpdateSubscriptionPriceWithUIComponent(ctx context.Context, subscriptionID string, newPrice float64, newUIComponentPrice float64) error {
// 获取订阅
subscription, err := s.subscriptionRepo.GetByID(ctx, subscriptionID)
if err != nil {
return fmt.Errorf("订阅不存在: %w", err)
}
// 更新价格
subscription.Price = decimal.NewFromFloat(newPrice)
subscription.UIComponentPrice = decimal.NewFromFloat(newUIComponentPrice)
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),
zap.Float64("new_ui_component_price", newUIComponentPrice))
return nil return nil
} }

View File

@@ -115,7 +115,7 @@ func (s *UserAggregateServiceImpl) CreateUser(ctx context.Context, phone, passwo
func (s *UserAggregateServiceImpl) LoadUser(ctx context.Context, userID string) (*entities.User, error) { func (s *UserAggregateServiceImpl) LoadUser(ctx context.Context, userID string) (*entities.User, error) {
s.logger.Debug("加载用户聚合根", zap.String("user_id", userID)) s.logger.Debug("加载用户聚合根", zap.String("user_id", userID))
user, err := s.userRepo.GetByID(ctx, userID) user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, userID)
if err != nil { if err != nil {
return nil, fmt.Errorf("用户不存在: %w", err) return nil, fmt.Errorf("用户不存在: %w", err)
} }

View File

@@ -0,0 +1,352 @@
package repositories
import (
"context"
"errors"
"time"
"tyapi-server/internal/domains/finance/entities"
"tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/interfaces"
"go.uber.org/zap"
"gorm.io/gorm"
)
const (
PurchaseOrdersTable = "ty_purchase_orders"
)
type GormPurchaseOrderRepository struct {
*database.CachedBaseRepositoryImpl
}
var _ repositories.PurchaseOrderRepository = (*GormPurchaseOrderRepository)(nil)
func NewGormPurchaseOrderRepository(db *gorm.DB, logger *zap.Logger) repositories.PurchaseOrderRepository {
return &GormPurchaseOrderRepository{
CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, PurchaseOrdersTable),
}
}
func (r *GormPurchaseOrderRepository) Create(ctx context.Context, order *entities.PurchaseOrder) (*entities.PurchaseOrder, error) {
err := r.CreateEntity(ctx, order)
if err != nil {
return nil, err
}
return order, nil
}
func (r *GormPurchaseOrderRepository) Update(ctx context.Context, order *entities.PurchaseOrder) error {
return r.UpdateEntity(ctx, order)
}
func (r *GormPurchaseOrderRepository) GetByID(ctx context.Context, id string) (*entities.PurchaseOrder, error) {
var order entities.PurchaseOrder
err := r.SmartGetByID(ctx, id, &order)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &order, nil
}
func (r *GormPurchaseOrderRepository) GetByOrderNo(ctx context.Context, orderNo string) (*entities.PurchaseOrder, error) {
var order entities.PurchaseOrder
err := r.GetDB(ctx).Where("order_no = ?", orderNo).First(&order).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &order, nil
}
func (r *GormPurchaseOrderRepository) GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*entities.PurchaseOrder, int64, error) {
var orders []entities.PurchaseOrder
var count int64
db := r.GetDB(ctx).Where("user_id = ?", userID)
// 获取总数
err := db.Model(&entities.PurchaseOrder{}).Count(&count).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
err = db.Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&orders).Error
if err != nil {
return nil, 0, err
}
result := make([]*entities.PurchaseOrder, len(orders))
for i := range orders {
result[i] = &orders[i]
}
return result, count, nil
}
func (r *GormPurchaseOrderRepository) GetByUserIDAndProductID(ctx context.Context, userID, productID string) (*entities.PurchaseOrder, error) {
var order entities.PurchaseOrder
err := r.GetDB(ctx).
Where("user_id = ? AND product_id = ? AND status = ?", userID, productID, entities.PurchaseOrderStatusPaid).
First(&order).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &order, nil
}
func (r *GormPurchaseOrderRepository) GetByPaymentTypeAndTransactionID(ctx context.Context, paymentType, transactionID string) (*entities.PurchaseOrder, error) {
var order entities.PurchaseOrder
err := r.GetDB(ctx).
Where("payment_type = ? AND trade_no = ?", paymentType, transactionID).
First(&order).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &order, nil
}
func (r *GormPurchaseOrderRepository) GetByTradeNo(ctx context.Context, tradeNo string) (*entities.PurchaseOrder, error) {
var order entities.PurchaseOrder
err := r.GetDB(ctx).Where("trade_no = ?", tradeNo).First(&order).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound
}
return nil, err
}
return &order, nil
}
func (r *GormPurchaseOrderRepository) UpdatePaymentStatus(ctx context.Context, orderID string, status entities.PurchaseOrderStatus, tradeNo *string, payAmount, receiptAmount *string, paymentTime *time.Time) error {
updates := map[string]interface{}{
"status": status,
}
if tradeNo != nil {
updates["trade_no"] = *tradeNo
}
if payAmount != nil {
updates["pay_amount"] = *payAmount
}
if receiptAmount != nil {
updates["receipt_amount"] = *receiptAmount
}
if paymentTime != nil {
updates["pay_time"] = *paymentTime
updates["notify_time"] = *paymentTime
}
err := r.GetDB(ctx).
Model(&entities.PurchaseOrder{}).
Where("id = ?", orderID).
Updates(updates).Error
return err
}
func (r *GormPurchaseOrderRepository) GetUserPurchasedProductCodes(ctx context.Context, userID string) ([]string, error) {
var orders []entities.PurchaseOrder
err := r.GetDB(ctx).
Select("product_code").
Where("user_id = ? AND status = ?", userID, entities.PurchaseOrderStatusPaid).
Find(&orders).Error
if err != nil {
return nil, err
}
codesMap := make(map[string]bool)
for _, order := range orders {
// 添加主产品编号
if order.ProductCode != "" {
codesMap[order.ProductCode] = true
}
}
codes := make([]string, 0, len(codesMap))
for code := range codesMap {
codes = append(codes, code)
}
return codes, nil
}
func (r *GormPurchaseOrderRepository) GetUserPaidProductIDs(ctx context.Context, userID string) ([]string, error) {
var orders []entities.PurchaseOrder
err := r.GetDB(ctx).
Select("product_id").
Where("user_id = ? AND status = ?", userID, entities.PurchaseOrderStatusPaid).
Find(&orders).Error
if err != nil {
return nil, err
}
idsMap := make(map[string]bool)
for _, order := range orders {
// 添加主产品ID
if order.ProductID != "" {
idsMap[order.ProductID] = true
}
}
ids := make([]string, 0, len(idsMap))
for id := range idsMap {
ids = append(ids, id)
}
return ids, nil
}
func (r *GormPurchaseOrderRepository) HasUserPurchased(ctx context.Context, userID string, productCode string) (bool, error) {
var count int64
err := r.GetDB(ctx).Model(&entities.PurchaseOrder{}).
Where("user_id = ? AND product_code = ? AND status = ?", userID, productCode, entities.PurchaseOrderStatusPaid).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *GormPurchaseOrderRepository) GetExpiringOrders(ctx context.Context, before time.Time, limit int) ([]*entities.PurchaseOrder, error) {
// 购买订单实体没有过期时间字段,此方法返回空结果
return []*entities.PurchaseOrder{}, nil
}
func (r *GormPurchaseOrderRepository) GetExpiredOrders(ctx context.Context, limit int) ([]*entities.PurchaseOrder, error) {
// 购买订单实体没有过期时间字段,此方法返回空结果
return []*entities.PurchaseOrder{}, nil
}
func (r *GormPurchaseOrderRepository) GetByStatus(ctx context.Context, status entities.PurchaseOrderStatus, limit, offset int) ([]*entities.PurchaseOrder, int64, error) {
var orders []entities.PurchaseOrder
var count int64
db := r.GetDB(ctx).Where("status = ?", status)
// 获取总数
err := db.Model(&entities.PurchaseOrder{}).Count(&count).Error
if err != nil {
return nil, 0, err
}
// 获取分页数据
err = db.Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&orders).Error
if err != nil {
return nil, 0, err
}
result := make([]*entities.PurchaseOrder, len(orders))
for i := range orders {
result[i] = &orders[i]
}
return result, count, nil
}
func (r *GormPurchaseOrderRepository) GetByFilters(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) ([]*entities.PurchaseOrder, error) {
var orders []entities.PurchaseOrder
db := r.GetDB(ctx)
// 应用筛选条件
if filters != nil {
if userID, ok := filters["user_id"]; ok {
db = db.Where("user_id = ?", userID)
}
if status, ok := filters["status"]; ok && status != "" {
db = db.Where("status = ?", status)
}
if paymentType, ok := filters["payment_type"]; ok && paymentType != "" {
db = db.Where("payment_type = ?", paymentType)
}
if payChannel, ok := filters["pay_channel"]; ok && payChannel != "" {
db = db.Where("pay_channel = ?", payChannel)
}
if startTime, ok := filters["start_time"]; ok && startTime != "" {
db = db.Where("created_at >= ?", startTime)
}
if endTime, ok := filters["end_time"]; ok && endTime != "" {
db = db.Where("created_at <= ?", endTime)
}
}
// 应用排序和分页
// 默认按创建时间倒序
db = db.Order("created_at DESC")
// 应用分页
if options.PageSize > 0 {
db = db.Limit(options.PageSize)
}
if options.Page > 0 {
db = db.Offset((options.Page - 1) * options.PageSize)
}
// 执行查询
err := db.Find(&orders).Error
if err != nil {
return nil, err
}
// 转换为指针切片
result := make([]*entities.PurchaseOrder, len(orders))
for i := range orders {
result[i] = &orders[i]
}
return result, nil
}
func (r *GormPurchaseOrderRepository) CountByFilters(ctx context.Context, filters map[string]interface{}) (int64, error) {
var count int64
db := r.GetDB(ctx).Model(&entities.PurchaseOrder{})
// 应用筛选条件
if filters != nil {
if userID, ok := filters["user_id"]; ok {
db = db.Where("user_id = ?", userID)
}
if status, ok := filters["status"]; ok && status != "" {
db = db.Where("status = ?", status)
}
if paymentType, ok := filters["payment_type"]; ok && paymentType != "" {
db = db.Where("payment_type = ?", paymentType)
}
if payChannel, ok := filters["pay_channel"]; ok && payChannel != "" {
db = db.Where("pay_channel = ?", payChannel)
}
if startTime, ok := filters["start_time"]; ok && startTime != "" {
db = db.Where("created_at >= ?", startTime)
}
if endTime, ok := filters["end_time"]; ok && endTime != "" {
db = db.Where("created_at <= ?", endTime)
}
}
// 执行计数
err := db.Count(&count).Error
return count, err
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"time"
"tyapi-server/internal/domains/product/entities" "tyapi-server/internal/domains/product/entities"
"tyapi-server/internal/domains/product/repositories" "tyapi-server/internal/domains/product/repositories"
@@ -29,12 +30,8 @@ func NewGormComponentReportRepository(db *gorm.DB, logger *zap.Logger) repositor
} }
} }
func (r *GormComponentReportRepository) CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error) { func (r *GormComponentReportRepository) Create(ctx context.Context, download *entities.ComponentReportDownload) error {
err := r.CreateEntity(ctx, download) return r.CreateEntity(ctx, download)
if err != nil {
return nil, err
}
return download, nil
} }
func (r *GormComponentReportRepository) UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error { func (r *GormComponentReportRepository) UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error {
@@ -55,7 +52,7 @@ func (r *GormComponentReportRepository) GetDownloadByID(ctx context.Context, id
func (r *GormComponentReportRepository) GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error) { func (r *GormComponentReportRepository) GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error) {
var downloads []entities.ComponentReportDownload var downloads []entities.ComponentReportDownload
query := r.GetDB(ctx).Where("user_id = ? AND payment_status = ?", userID, "success") query := r.GetDB(ctx).Where("user_id = ?", userID)
if productID != nil && *productID != "" { if productID != nil && *productID != "" {
query = query.Where("product_id = ?", *productID) query = query.Where("product_id = ?", *productID)
@@ -76,7 +73,7 @@ func (r *GormComponentReportRepository) GetUserDownloads(ctx context.Context, us
func (r *GormComponentReportRepository) HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error) { func (r *GormComponentReportRepository) HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error) {
var count int64 var count int64
err := r.GetDB(ctx).Model(&entities.ComponentReportDownload{}). err := r.GetDB(ctx).Model(&entities.ComponentReportDownload{}).
Where("user_id = ? AND product_code = ? AND payment_status = ?", userID, productCode, "success"). Where("user_id = ? AND product_code = ?", userID, productCode).
Count(&count).Error Count(&count).Error
if err != nil { if err != nil {
return false, err return false, err
@@ -88,7 +85,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex
var downloads []entities.ComponentReportDownload var downloads []entities.ComponentReportDownload
err := r.GetDB(ctx). err := r.GetDB(ctx).
Select("DISTINCT sub_product_codes"). Select("DISTINCT sub_product_codes").
Where("user_id = ? AND payment_status = ?", userID, "success"). Where("user_id = ?", userID).
Find(&downloads).Error Find(&downloads).Error
if err != nil { if err != nil {
return nil, err return nil, err
@@ -119,7 +116,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex
func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) { func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) {
var download entities.ComponentReportDownload var download entities.ComponentReportDownload
err := r.GetDB(ctx).Where("payment_order_id = ?", orderID).First(&download).Error err := r.GetDB(ctx).Where("order_number = ?", orderID).First(&download).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, gorm.ErrRecordNotFound return nil, gorm.ErrRecordNotFound
@@ -128,3 +125,65 @@ func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.
} }
return &download, nil return &download, nil
} }
// GetActiveDownload 获取用户有效的下载记录
func (r *GormComponentReportRepository) GetActiveDownload(ctx context.Context, userID, productID string) (*entities.ComponentReportDownload, error) {
var download entities.ComponentReportDownload
// 先尝试查找有支付订单号的下载记录(已支付)
err := r.GetDB(ctx).
Where("user_id = ? AND product_id = ? AND order_number IS NOT NULL AND deleted_at IS NULL", userID, productID).
Order("created_at DESC").
First(&download).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 如果没有找到有支付订单号的记录,尝试查找任何有效的下载记录
err = r.GetDB(ctx).
Where("user_id = ? AND product_id = ? AND deleted_at IS NULL", userID, productID).
Order("created_at DESC").
First(&download).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
} else {
return nil, err
}
}
// 如果找到了下载记录,检查关联的购买订单状态
if download.OrderID != nil {
// 这里需要查询购买订单状态,但当前仓库没有依赖购买订单仓库
// 所以只检查是否有过期时间设置,如果有则认为已支付
if download.ExpiresAt == nil {
return nil, nil // 没有过期时间,表示未支付
}
}
// 检查是否已过期
if download.IsExpired() {
return nil, nil
}
return &download, nil
}
// UpdateFilePath 更新下载记录文件路径
func (r *GormComponentReportRepository) UpdateFilePath(ctx context.Context, downloadID, filePath string) error {
return r.GetDB(ctx).Model(&entities.ComponentReportDownload{}).Where("id = ?", downloadID).Update("file_path", filePath).Error
}
// IncrementDownloadCount 增加下载次数
func (r *GormComponentReportRepository) IncrementDownloadCount(ctx context.Context, downloadID string) error {
now := time.Now()
return r.GetDB(ctx).Model(&entities.ComponentReportDownload{}).
Where("id = ?", downloadID).
Updates(map[string]interface{}{
"download_count": gorm.Expr("download_count + 1"),
"last_download_at": &now,
}).Error
}

View File

@@ -108,7 +108,8 @@ func (r *GormUIComponentRepository) Update(ctx context.Context, component entiti
// Delete 删除UI组件 // Delete 删除UI组件
func (r *GormUIComponentRepository) Delete(ctx context.Context, id string) error { func (r *GormUIComponentRepository) Delete(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Delete(&entities.UIComponent{}, id).Error; err != nil { // 记录删除操作的详细信息
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&entities.UIComponent{}).Error; err != nil {
return fmt.Errorf("删除UI组件失败: %w", err) return fmt.Errorf("删除UI组件失败: %w", err)
} }
return nil return nil

View File

@@ -0,0 +1,48 @@
package jiguang
import (
"crypto/hmac"
"crypto/md5"
"encoding/hex"
"fmt"
"strings"
)
// SignMethod 签名方法类型
type SignMethod string
const (
SignMethodMD5 SignMethod = "md5"
SignMethodHMACMD5 SignMethod = "hmac"
)
// GenerateSign 生成签名
// 根据 signMethod 参数选择使用 MD5 或 HMAC-MD5 算法
// MD5: md5(timestamp + "&appSecret=" + appSecret),然后转大写十六进制
// HMAC-MD5: hmac_md5(timestamp, appSecret),然后转大写十六进制
func GenerateSign(timestamp string, appSecret string, signMethod SignMethod) (string, error) {
var hashBytes []byte
switch signMethod {
case SignMethodMD5:
// MD5算法在待签名字符串后面加上 &appSecret=xxx 再进行计算
signStr := timestamp + "&appSecret=" + appSecret
hash := md5.Sum([]byte(signStr))
hashBytes = hash[:]
case SignMethodHMACMD5:
// HMAC-MD5算法使用 appSecret 初始化摘要算法再进行计算
mac := hmac.New(md5.New, []byte(appSecret))
mac.Write([]byte(timestamp))
hashBytes = mac.Sum(nil)
default:
return "", fmt.Errorf("不支持的签名方法: %s", signMethod)
}
// 将二进制转化为大写的十六进制正确签名应该为32大写字符串
return strings.ToUpper(hex.EncodeToString(hashBytes)), nil
}
// GenerateSignWithDefault 使用默认的 HMAC-MD5 方法生成签名
func GenerateSignWithDefault(timestamp string, appSecret string) (string, error) {
return GenerateSign(timestamp, appSecret, SignMethodHMACMD5)
}

View File

@@ -0,0 +1,149 @@
package jiguang
import (
"fmt"
)
// JiguangError 极光服务错误
type JiguangError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// Error 实现error接口
func (e *JiguangError) Error() string {
return fmt.Sprintf("极光错误 [%d]: %s", e.Code, e.Message)
}
// IsSuccess 检查是否成功
func (e *JiguangError) IsSuccess() bool {
return e.Code == 0
}
// IsQueryFailed 检查是否查询失败
func (e *JiguangError) IsQueryFailed() bool {
return e.Code == 922
}
// IsNoRecord 检查是否查无记录
func (e *JiguangError) IsNoRecord() bool {
return e.Code == 921
}
// IsParamError 检查是否是参数相关错误
func (e *JiguangError) IsParamError() bool {
return e.Code == 400 || e.Code == 906 || e.Code == 914 || e.Code == 918
}
// IsAuthError 检查是否是认证相关错误
func (e *JiguangError) IsAuthError() bool {
return e.Code == 902 || e.Code == 903 || e.Code == 904 || e.Code == 905
}
// IsSystemError 检查是否是系统错误
func (e *JiguangError) IsSystemError() bool {
return e.Code == 405 || e.Code == 911 || e.Code == 912 || e.Code == 915 || e.Code == 916 || e.Code == 917 || e.Code == 919 || e.Code == 923
}
// 预定义错误常量
var (
// 成功状态
ErrSuccess = &JiguangError{Code: 0, Message: "请求成功"}
// 参数错误
ErrParamInvalid = &JiguangError{Code: 400, Message: "请求参数不正确"}
ErrMethodInvalid = &JiguangError{Code: 405, Message: "请求方法不正确"}
ErrParamFormInvalid = &JiguangError{Code: 906, Message: "请求参数形式不正确"}
ErrBodyIncomplete = &JiguangError{Code: 914, Message: "Body 请求参数不完整"}
ErrBodyNotSupported = &JiguangError{Code: 918, Message: "Body 请求参数不支持"}
// 认证错误
ErrAppIDInvalid = &JiguangError{Code: 902, Message: "错误的 appId/账户已删除"}
ErrTimestampInvalid = &JiguangError{Code: 903, Message: "错误的时间戳/时间误差大于 10 分钟"}
ErrSignMethodInvalid = &JiguangError{Code: 904, Message: "无法识别的签名方法"}
ErrSignInvalid = &JiguangError{Code: 905, Message: "签名不合法"}
// 系统错误
ErrAccountStatusError = &JiguangError{Code: 911, Message: "账户状态异常"}
ErrInterfaceDisabled = &JiguangError{Code: 912, Message: "接口状态不可用"}
ErrAPICallError = &JiguangError{Code: 915, Message: "API 接口调用有误"}
ErrInternalError = &JiguangError{Code: 916, Message: "内部接口调用错误,请联系相关人员"}
ErrTimeout = &JiguangError{Code: 917, Message: "请求超时"}
ErrBusinessDisabled = &JiguangError{Code: 919, Message: "业务状态不可用"}
ErrInterfaceException = &JiguangError{Code: 923, Message: "接口异常"}
// 业务错误
ErrNoRecord = &JiguangError{Code: 921, Message: "查无记录"}
ErrQueryFailed = &JiguangError{Code: 922, Message: "查询失败"}
)
// NewJiguangError 创建新的极光错误
func NewJiguangError(code int, message string) *JiguangError {
return &JiguangError{
Code: code,
Message: message,
}
}
// NewJiguangErrorFromCode 根据状态码创建错误
func NewJiguangErrorFromCode(code int) *JiguangError {
switch code {
case 0:
return ErrSuccess
case 400:
return ErrParamInvalid
case 405:
return ErrMethodInvalid
case 902:
return ErrAppIDInvalid
case 903:
return ErrTimestampInvalid
case 904:
return ErrSignMethodInvalid
case 905:
return ErrSignInvalid
case 906:
return ErrParamFormInvalid
case 911:
return ErrAccountStatusError
case 912:
return ErrInterfaceDisabled
case 914:
return ErrBodyIncomplete
case 915:
return ErrAPICallError
case 916:
return ErrInternalError
case 917:
return ErrTimeout
case 918:
return ErrBodyNotSupported
case 919:
return ErrBusinessDisabled
case 921:
return ErrNoRecord
case 922:
return ErrQueryFailed
case 923:
return ErrInterfaceException
default:
return &JiguangError{
Code: code,
Message: fmt.Sprintf("未知错误码: %d", code),
}
}
}
// IsJiguangError 检查是否是极光错误
func IsJiguangError(err error) bool {
_, ok := err.(*JiguangError)
return ok
}
// GetJiguangError 获取极光错误
func GetJiguangError(err error) *JiguangError {
if jiguangErr, ok := err.(*JiguangError); ok {
return jiguangErr
}
return nil
}

View File

@@ -0,0 +1,85 @@
package jiguang
import (
"time"
"tyapi-server/internal/config"
"tyapi-server/internal/shared/external_logger"
)
// NewJiguangServiceWithConfig 使用配置创建极光服务
func NewJiguangServiceWithConfig(cfg *config.Config) (*JiguangService, error) {
// 将配置类型转换为通用外部服务日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: cfg.Jiguang.Logging.Enabled,
LogDir: cfg.Jiguang.Logging.LogDir,
ServiceName: "jiguang",
UseDaily: cfg.Jiguang.Logging.UseDaily,
EnableLevelSeparation: cfg.Jiguang.Logging.EnableLevelSeparation,
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
}
// 转换级别配置
for key, value := range cfg.Jiguang.Logging.LevelConfigs {
loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{
MaxSize: value.MaxSize,
MaxBackups: value.MaxBackups,
MaxAge: value.MaxAge,
Compress: value.Compress,
}
}
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 解析签名方法
var signMethod SignMethod
if cfg.Jiguang.SignMethod == "md5" {
signMethod = SignMethodMD5
} else {
signMethod = SignMethodHMACMD5 // 默认使用 HMAC-MD5
}
// 解析超时时间
timeout := 60 * time.Second
if cfg.Jiguang.Timeout > 0 {
timeout = cfg.Jiguang.Timeout
}
// 创建极光服务
service := NewJiguangService(
cfg.Jiguang.URL,
cfg.Jiguang.AppID,
cfg.Jiguang.AppSecret,
signMethod,
timeout,
logger,
)
return service, nil
}
// NewJiguangServiceWithLogging 使用自定义日志配置创建极光服务
func NewJiguangServiceWithLogging(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*JiguangService, error) {
// 设置服务名称
loggingConfig.ServiceName = "jiguang"
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建极光服务
service := NewJiguangService(url, appID, appSecret, signMethod, timeout, logger)
return service, nil
}
// NewJiguangServiceSimple 创建简单的极光服务(无日志)
func NewJiguangServiceSimple(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration) *JiguangService {
return NewJiguangService(url, appID, appSecret, signMethod, timeout, nil)
}

View File

@@ -0,0 +1,270 @@
package jiguang
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"tyapi-server/internal/shared/external_logger"
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("查询为空")
)
// JiguangResponse 极光API响应结构
type JiguangResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
OrderID string `json:"order_id"`
Data interface{} `json:"data"`
}
// JiguangConfig 极光服务配置
type JiguangConfig struct {
URL string
AppID string
AppSecret string
SignMethod SignMethod // 签名方法md5 或 hmac
Timeout time.Duration
}
// JiguangService 极光服务
type JiguangService struct {
config JiguangConfig
logger *external_logger.ExternalServiceLogger
}
// NewJiguangService 创建一个新的极光服务实例
func NewJiguangService(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *JiguangService {
// 如果没有指定签名方法,默认使用 HMAC-MD5
if signMethod == "" {
signMethod = SignMethodHMACMD5
}
// 如果没有指定超时时间,默认使用 60 秒
if timeout == 0 {
timeout = 60 * time.Second
}
return &JiguangService{
config: JiguangConfig{
URL: url,
AppID: appID,
AppSecret: appSecret,
SignMethod: signMethod,
Timeout: timeout,
},
logger: logger,
}
}
// generateRequestID 生成请求ID
func (j *JiguangService) generateRequestID() string {
timestamp := time.Now().UnixNano()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, j.config.AppID)))
return fmt.Sprintf("jiguang_%x", hash[:8])
}
// CallAPI 调用极光API
// apiCode: API服务编码如 marriage-single-v2用于请求头
// apiPath: API路径如 marriage/single-v2用于URL路径
// params: 请求参数会作为JSON body发送
func (j *JiguangService) CallAPI(ctx context.Context, apiCode string, apiPath string, params map[string]interface{}) (resp []byte, err error) {
startTime := time.Now()
requestID := j.generateRequestID()
// 生成时间戳(毫秒)
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 生成签名
sign, signErr := GenerateSign(timestamp, j.config.AppSecret, j.config.SignMethod)
if signErr != nil {
err = errors.Join(ErrSystem, fmt.Errorf("生成签名失败: %w", signErr))
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
// 构建完整的请求URL使用apiPath作为路径
requestURL := strings.TrimSuffix(j.config.URL, "/") + "/" + strings.TrimPrefix(apiPath, "/")
// 记录请求日志
if j.logger != nil {
j.logger.LogRequest(requestID, transactionID, apiCode, requestURL, params)
}
// 将请求参数转换为JSON
jsonData, marshalErr := json.Marshal(params)
if marshalErr != nil {
err = errors.Join(ErrSystem, marshalErr)
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
// 创建HTTP POST请求
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", requestURL, bytes.NewBuffer(jsonData))
if newRequestErr != nil {
err = errors.Join(ErrSystem, newRequestErr)
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("appId", j.config.AppID)
req.Header.Set("apiCode", apiCode)
req.Header.Set("timestamp", timestamp)
req.Header.Set("signMethod", string(j.config.SignMethod))
req.Header.Set("sign", sign)
// 创建HTTP客户端
client := &http.Client{
Timeout: j.config.Timeout,
}
// 发送请求
httpResp, clientDoErr := client.Do(req)
if clientDoErr != nil {
// 检查是否是超时错误
isTimeout := false
if ctx.Err() == context.DeadlineExceeded {
isTimeout = true
} else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() {
isTimeout = true
} else if errStr := clientDoErr.Error(); errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
isTimeout = true
}
if isTimeout {
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", clientDoErr))
} else {
err = errors.Join(ErrSystem, clientDoErr)
}
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
defer func(Body io.ReadCloser) {
closeErr := Body.Close()
if closeErr != nil {
// 记录关闭错误
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, errors.Join(ErrSystem, fmt.Errorf("关闭响应体失败: %w", closeErr)), params)
}
}
}(httpResp.Body)
// 计算请求耗时
duration := time.Since(startTime)
// 读取响应体
bodyBytes, readErr := io.ReadAll(httpResp.Body)
if readErr != nil {
err = errors.Join(ErrSystem, readErr)
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
// 检查HTTP状态码
if httpResp.StatusCode != http.StatusOK {
err = errors.Join(ErrSystem, fmt.Errorf("极光请求失败,状态码: %d", httpResp.StatusCode))
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
// 解析响应结构
var jiguangResp JiguangResponse
if err := json.Unmarshal(bodyBytes, &jiguangResp); err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
if j.logger != nil {
j.logger.LogError(requestID, transactionID, apiCode, err, params)
}
return nil, err
}
// 记录响应日志
if j.logger != nil {
if jiguangResp.OrderID != "" {
j.logger.LogResponseWithID(requestID, transactionID, apiCode, httpResp.StatusCode, bodyBytes, duration, jiguangResp.OrderID)
} else {
j.logger.LogResponse(requestID, transactionID, apiCode, httpResp.StatusCode, bodyBytes, duration)
}
}
// 检查业务状态码
if jiguangResp.Code != 0 {
// 创建极光错误
jiguangErr := NewJiguangErrorFromCode(jiguangResp.Code)
if jiguangErr.Message == fmt.Sprintf("未知错误码: %d", jiguangResp.Code) && jiguangResp.Msg != "" {
jiguangErr.Message = jiguangResp.Msg
}
// 记录错误日志
if j.logger != nil {
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID)
}
// 根据错误类型返回不同的错误
if jiguangErr.IsNoRecord() {
return nil, errors.Join(ErrNotFound, jiguangErr)
} else if jiguangErr.IsQueryFailed() {
return nil, errors.Join(ErrDatasource, jiguangErr)
} else if jiguangErr.IsSystemError() {
return nil, errors.Join(ErrSystem, jiguangErr)
} else {
return nil, errors.Join(ErrDatasource, jiguangErr)
}
}
// 成功响应返回data字段
if jiguangResp.Data == nil {
return []byte("{}"), nil
}
// 将data转换为JSON字节
dataBytes, err := json.Marshal(jiguangResp.Data)
if err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("data字段序列化失败: %w", err))
if j.logger != nil {
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, err, params, jiguangResp.OrderID)
}
return nil, err
}
return dataBytes, nil
}
// GetConfig 获取配置信息
func (j *JiguangService) GetConfig() JiguangConfig {
return j.config
}

View File

@@ -15,6 +15,7 @@ import (
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"tyapi-server/internal/shared/external_logger" "tyapi-server/internal/shared/external_logger"
@@ -90,13 +91,14 @@ func (m *MuziService) generateRequestID() string {
} }
// CallAPI 调用木子数据接口 // CallAPI 调用木子数据接口
func (m *MuziService) CallAPI(ctx context.Context, prodCode string, params map[string]interface{}) (json.RawMessage, error) { func (m *MuziService) CallAPI(ctx context.Context, prodCode string, path string, params map[string]interface{},paramSign map[string]interface{}) (json.RawMessage, error) {
requestID := m.generateRequestID() requestID := m.generateRequestID()
now := time.Now() now := time.Now()
timestamp := strconv.FormatInt(now.UnixMilli(), 10) timestamp := strconv.FormatInt(now.UnixMilli(), 10)
flatParams := flattenParams(params) flatParams := flattenParams(params)
signParts := collectSignatureValues(params)
signParts := collectSignatureValues(paramSign)
signature := m.GenerateSignature(prodCode, timestamp, signParts...) signature := m.GenerateSignature(prodCode, timestamp, signParts...)
// 从上下文获取链路ID // 从上下文获取链路ID
@@ -128,7 +130,21 @@ func (m *MuziService) CallAPI(ctx context.Context, prodCode string, params map[s
return nil, err return nil, err
} }
req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, m.config.URL, bytes.NewBuffer(bodyBytes)) // 构建完整的URL拼接路径参数
fullURL := m.config.URL
if path != "" {
// 确保路径以/开头
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// 确保URL不以/结尾,避免双斜杠
if strings.HasSuffix(fullURL, "/") {
fullURL = fullURL[:len(fullURL)-1]
}
fullURL += path
}
req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, bytes.NewBuffer(bodyBytes))
if reqErr != nil { if reqErr != nil {
err := errors.Join(ErrSystem, reqErr) err := errors.Join(ErrSystem, reqErr)
if m.logger != nil { if m.logger != nil {

View File

@@ -26,8 +26,11 @@ var APIEndpoints = map[string]string{
"FinancingHistory": "/open/cd/findHistoryRongzi/2.0", // 融资历史 "FinancingHistory": "/open/cd/findHistoryRongzi/2.0", // 融资历史
"PunishmentInfo": "/open/mr/punishmentInfo/3.0", // 行政处罚 "PunishmentInfo": "/open/mr/punishmentInfo/3.0", // 行政处罚
"AbnormalInfo": "/open/mr/abnormal/2.0", // 经营异常 "AbnormalInfo": "/open/mr/abnormal/2.0", // 经营异常
"OwnTax": "/open/mr/ownTax", // 欠税公告 "OwnTax": "/open/mr/ownTax/2.0", // 欠税公告
"TaxContravention": "/open/mr/taxContravention", // 税收违法 "TaxContravention": "/open/mr/taxContravention/2.0", // 税收违法
"holderChange": "/open/ic/holderChange/2.0", // 股权变更
"baseinfo": "/open/ic/baseinfo/normal", // 企业基本信息
"investtree": "/v3/open/investtree", // 股权穿透
} }
// TianYanChaConfig 天眼查配置 // TianYanChaConfig 天眼查配置

View File

@@ -0,0 +1,428 @@
package handlers
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"tyapi-server/internal/application/product"
)
// ComponentReportOrderHandler 组件报告订单处理器
type ComponentReportOrderHandler struct {
service *product.ComponentReportOrderService
logger *zap.Logger
}
// NewComponentReportOrderHandler 创建组件报告订单处理器
func NewComponentReportOrderHandler(
service *product.ComponentReportOrderService,
logger *zap.Logger,
) *ComponentReportOrderHandler {
return &ComponentReportOrderHandler{
service: service,
logger: logger,
}
}
// CheckDownloadAvailability 检查下载可用性
// GET /api/v1/products/:id/component-report/check
func (h *ComponentReportOrderHandler) CheckDownloadAvailability(c *gin.Context) {
h.logger.Info("开始检查下载可用性")
productID := c.Param("id")
h.logger.Info("获取产品ID", zap.String("product_id", productID))
if productID == "" {
h.logger.Error("产品ID不能为空")
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
userID := c.GetString("user_id")
h.logger.Info("获取用户ID", zap.String("user_id", userID))
if userID == "" {
h.logger.Error("用户未登录")
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
// 调用服务获取订单信息,检查是否可以下载
orderInfo, err := h.service.GetOrderInfo(c.Request.Context(), userID, productID)
if err != nil {
h.logger.Error("获取订单信息失败", zap.Error(err), zap.String("product_id", productID), zap.String("user_id", userID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取订单信息失败",
"error": err.Error(),
})
return
}
h.logger.Info("获取订单信息成功", zap.Bool("can_download", orderInfo.CanDownload), zap.Bool("is_package", orderInfo.IsPackage))
// 返回检查结果
message := "需要购买"
if orderInfo.CanDownload {
message = "可以下载"
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{
"can_download": orderInfo.CanDownload,
"is_package": orderInfo.IsPackage,
"message": message,
},
})
}
// GetDownloadInfo 获取下载信息和价格计算
// GET /api/v1/products/:id/component-report/info
func (h *ComponentReportOrderHandler) GetDownloadInfo(c *gin.Context) {
h.logger.Info("开始获取下载信息和价格计算")
productID := c.Param("id")
h.logger.Info("获取产品ID", zap.String("product_id", productID))
if productID == "" {
h.logger.Error("产品ID不能为空")
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
userID := c.GetString("user_id")
h.logger.Info("获取用户ID", zap.String("user_id", userID))
if userID == "" {
h.logger.Error("用户未登录")
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
orderInfo, err := h.service.GetOrderInfo(c.Request.Context(), userID, productID)
if err != nil {
h.logger.Error("获取订单信息失败", zap.Error(err), zap.String("product_id", productID), zap.String("user_id", userID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取订单信息失败",
"error": err.Error(),
})
return
}
// 记录详细的订单信息
h.logger.Info("获取订单信息成功",
zap.String("product_id", orderInfo.ProductID),
zap.String("product_code", orderInfo.ProductCode),
zap.String("product_name", orderInfo.ProductName),
zap.Bool("is_package", orderInfo.IsPackage),
zap.Int("sub_products_count", len(orderInfo.SubProducts)),
zap.String("price", orderInfo.Price),
zap.Strings("purchased_product_codes", orderInfo.PurchasedProductCodes),
zap.Bool("can_download", orderInfo.CanDownload),
)
// 记录子产品详情
for i, subProduct := range orderInfo.SubProducts {
h.logger.Info("子产品信息",
zap.Int("index", i),
zap.String("sub_product_id", subProduct.ProductID),
zap.String("sub_product_code", subProduct.ProductCode),
zap.String("sub_product_name", subProduct.ProductName),
zap.String("price", subProduct.Price),
zap.Bool("is_purchased", subProduct.IsPurchased),
)
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": orderInfo,
})
}
// CreatePaymentOrder 创建支付订单
// POST /api/v1/products/:id/component-report/create-order
func (h *ComponentReportOrderHandler) CreatePaymentOrder(c *gin.Context) {
h.logger.Info("开始创建支付订单")
productID := c.Param("id")
h.logger.Info("获取产品ID", zap.String("product_id", productID))
if productID == "" {
h.logger.Error("产品ID不能为空")
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "产品ID不能为空",
})
return
}
userID := c.GetString("user_id")
h.logger.Info("获取用户ID", zap.String("user_id", userID))
if userID == "" {
h.logger.Error("用户未登录")
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
var req product.CreatePaymentOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("请求参数错误", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误",
"error": err.Error(),
})
return
}
// 记录请求参数
h.logger.Info("支付订单请求参数",
zap.String("user_id", userID),
zap.String("product_id", productID),
zap.String("payment_type", req.PaymentType),
zap.String("platform", req.Platform),
zap.Strings("sub_product_codes", req.SubProductCodes),
)
// 设置用户ID和产品ID
req.UserID = userID
req.ProductID = productID
// 如果未指定支付平台根据User-Agent判断
if req.Platform == "" {
userAgent := c.GetHeader("User-Agent")
req.Platform = h.detectPlatform(userAgent)
h.logger.Info("根据User-Agent检测平台", zap.String("user_agent", userAgent), zap.String("detected_platform", req.Platform))
}
response, err := h.service.CreatePaymentOrder(c.Request.Context(), &req)
if err != nil {
h.logger.Error("创建支付订单失败", zap.Error(err),
zap.String("product_id", productID),
zap.String("user_id", userID),
zap.String("payment_type", req.PaymentType))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建支付订单失败",
"error": err.Error(),
})
return
}
// 记录创建订单成功响应
h.logger.Info("创建支付订单成功",
zap.String("order_id", response.OrderID),
zap.String("order_no", response.OrderNo),
zap.String("payment_type", response.PaymentType),
zap.String("amount", response.Amount),
zap.String("code_url", response.CodeURL),
zap.String("pay_url", response.PayURL),
)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": response,
})
}
// CheckPaymentStatus 检查支付状态
// GET /api/v1/component-report/check-payment/:orderId
func (h *ComponentReportOrderHandler) CheckPaymentStatus(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
orderID := c.Param("orderId")
if orderID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "订单ID不能为空",
})
return
}
response, err := h.service.CheckPaymentStatus(c.Request.Context(), orderID)
if err != nil {
h.logger.Error("检查支付状态失败", zap.Error(err), zap.String("order_id", orderID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "检查支付状态失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": response,
"message": "查询支付状态成功",
})
}
// DownloadFile 下载文件
// GET /api/v1/component-report/download/:orderId
func (h *ComponentReportOrderHandler) DownloadFile(c *gin.Context) {
h.logger.Info("开始处理文件下载请求")
userID := c.GetString("user_id")
if userID == "" {
h.logger.Error("用户未登录")
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
h.logger.Info("获取用户ID", zap.String("user_id", userID))
orderID := c.Param("orderId")
if orderID == "" {
h.logger.Error("订单ID不能为空")
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "订单ID不能为空",
})
return
}
h.logger.Info("获取订单ID", zap.String("order_id", orderID))
filePath, err := h.service.DownloadFile(c.Request.Context(), orderID)
if err != nil {
h.logger.Error("下载文件失败", zap.Error(err), zap.String("order_id", orderID), zap.String("user_id", userID))
// 根据错误类型返回不同的状态码和消息
errorMessage := err.Error()
statusCode := http.StatusInternalServerError
// 根据错误消息判断具体错误类型
if strings.Contains(errorMessage, "购买订单不存在") {
statusCode = http.StatusNotFound
} else if strings.Contains(errorMessage, "订单未支付") || strings.Contains(errorMessage, "已过期") {
statusCode = http.StatusForbidden
} else if strings.Contains(errorMessage, "生成报告文件失败") {
statusCode = http.StatusInternalServerError
}
c.JSON(statusCode, gin.H{
"code": statusCode,
"message": "下载文件失败",
"error": errorMessage,
})
return
}
h.logger.Info("成功获取文件路径",
zap.String("order_id", orderID),
zap.String("user_id", userID),
zap.String("file_path", filePath))
// 设置响应头
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", "attachment; filename=component_report.zip")
// 发送文件
h.logger.Info("开始发送文件", zap.String("file_path", filePath))
c.File(filePath)
h.logger.Info("文件发送成功", zap.String("file_path", filePath))
}
// GetUserOrders 获取用户订单列表
// GET /api/v1/component-report/orders
func (h *ComponentReportOrderHandler) GetUserOrders(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
// 解析分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
offset := (page - 1) * pageSize
orders, total, err := h.service.GetUserOrders(c.Request.Context(), userID, pageSize, offset)
if err != nil {
h.logger.Error("获取用户订单列表失败", zap.Error(err), zap.String("user_id", userID))
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "获取用户订单列表失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": gin.H{
"list": orders,
"total": total,
"page": page,
"page_size": pageSize,
},
})
}
// detectPlatform 根据 User-Agent 检测支付平台类型
func (h *ComponentReportOrderHandler) detectPlatform(userAgent string) string {
if userAgent == "" {
return "h5" // 默认 H5
}
ua := strings.ToLower(userAgent)
// 检测移动设备
if strings.Contains(ua, "mobile") || strings.Contains(ua, "android") ||
strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") {
// 检测是否是支付宝或微信内置浏览器
if strings.Contains(ua, "alipay") {
return "app" // 支付宝 APP
}
if strings.Contains(ua, "micromessenger") {
return "h5" // 微信 H5
}
return "h5" // 移动端默认 H5
}
// PC 端
return "pc"
}

View File

@@ -1106,3 +1106,192 @@ func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息") h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
} }
// GetUserPurchaseRecords 获取用户购买记录
// @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 payment_type query string false "支付类型: alipay, wechat, free"
// @Param pay_channel query string false "支付渠道: alipay, wechat"
// @Param status query string false "订单状态: created, paid, failed, cancelled, refunded, closed"
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
// @Param min_amount query string false "最小金额"
// @Param max_amount query string false "最大金额"
// @Param product_code query string false "产品编号"
// @Success 200 {object} responses.PurchaseRecordListResponse "获取成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/finance/purchase-records [get]
func (h *FinanceHandler) GetUserPurchaseRecords(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 时间范围筛选
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 paymentType := c.Query("payment_type"); paymentType != "" {
filters["payment_type"] = paymentType
}
// 支付渠道筛选
if payChannel := c.Query("pay_channel"); payChannel != "" {
filters["pay_channel"] = payChannel
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 产品编号筛选
if productCode := c.Query("product_code"); productCode != "" {
filters["product_code"] = productCode
}
// 金额范围筛选
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.appService.GetUserPurchaseRecords(c.Request.Context(), userID, filters, options)
if err != nil {
h.logger.Error("获取用户购买记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取购买记录失败")
return
}
h.responseBuilder.Success(c, result, "获取用户购买记录成功")
}
// GetAdminPurchaseRecords 获取管理端购买记录
// @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 payment_type query string false "支付类型: alipay, wechat, free"
// @Param pay_channel query string false "支付渠道: alipay, wechat"
// @Param status query string false "订单状态: created, paid, failed, cancelled, refunded, closed"
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
// @Param min_amount query string false "最小金额"
// @Param max_amount query string false "最大金额"
// @Param product_code query string false "产品编号"
// @Success 200 {object} responses.PurchaseRecordListResponse "获取成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 403 {object} map[string]interface{} "权限不足"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/finance/purchase-records [get]
func (h *FinanceHandler) GetAdminPurchaseRecords(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 paymentType := c.Query("payment_type"); paymentType != "" {
filters["payment_type"] = paymentType
}
// 支付渠道筛选
if payChannel := c.Query("pay_channel"); payChannel != "" {
filters["pay_channel"] = payChannel
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 产品编号筛选
if productCode := c.Query("product_code"); productCode != "" {
filters["product_code"] = productCode
}
// 金额范围筛选
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.appService.GetAdminPurchaseRecords(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端购买记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取购买记录失败")
return
}
h.responseBuilder.Success(c, result, "获取管理端购买记录成功")
}

View File

@@ -1,8 +1,6 @@
package handlers package handlers
import ( import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -13,6 +11,9 @@ import (
"tyapi-server/internal/application/product/dto/queries" "tyapi-server/internal/application/product/dto/queries"
"tyapi-server/internal/application/product/dto/responses" "tyapi-server/internal/application/product/dto/responses"
"tyapi-server/internal/shared/interfaces" "tyapi-server/internal/shared/interfaces"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
) )
// ProductAdminHandler 产品管理员HTTP处理器 // ProductAdminHandler 产品管理员HTTP处理器
@@ -296,7 +297,6 @@ func (h *ProductAdminHandler) ListProducts(c *gin.Context) {
// 解析查询参数 // 解析查询参数
page := h.getIntQuery(c, "page", 1) page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10) pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件 // 构建筛选条件
filters := make(map[string]interface{}) filters := make(map[string]interface{})

View File

@@ -282,7 +282,8 @@ func (h *UIComponentHandler) DeleteUIComponent(c *gin.Context) {
h.responseBuilder.NotFound(c, "UI组件不存在") h.responseBuilder.NotFound(c, "UI组件不存在")
return return
} }
h.responseBuilder.InternalError(c, "删除UI组件失败") // 提供更详细的错误信息
h.responseBuilder.InternalError(c, fmt.Sprintf("删除UI组件失败: %v", err))
return return
} }

View File

@@ -0,0 +1,58 @@
package routes
import (
"tyapi-server/internal/infrastructure/http/handlers"
sharedhttp "tyapi-server/internal/shared/http"
"tyapi-server/internal/shared/middleware"
"go.uber.org/zap"
)
// ComponentReportOrderRoutes 组件报告订单路由
type ComponentReportOrderRoutes struct {
componentReportOrderHandler *handlers.ComponentReportOrderHandler
auth *middleware.JWTAuthMiddleware
logger *zap.Logger
}
// NewComponentReportOrderRoutes 创建组件报告订单路由
func NewComponentReportOrderRoutes(
componentReportOrderHandler *handlers.ComponentReportOrderHandler,
auth *middleware.JWTAuthMiddleware,
logger *zap.Logger,
) *ComponentReportOrderRoutes {
return &ComponentReportOrderRoutes{
componentReportOrderHandler: componentReportOrderHandler,
auth: auth,
logger: logger,
}
}
// Register 注册组件报告订单相关路由
func (r *ComponentReportOrderRoutes) Register(router *sharedhttp.GinRouter) {
engine := router.GetEngine()
// 产品组件报告相关接口 - 需要认证
componentReportGroup := engine.Group("/api/v1/products/:id/component-report", r.auth.Handle())
{
// 检查下载可用性
componentReportGroup.GET("/check", r.componentReportOrderHandler.CheckDownloadAvailability)
// 获取下载信息
componentReportGroup.GET("/info", r.componentReportOrderHandler.GetDownloadInfo)
// 创建支付订单
componentReportGroup.POST("/create-order", r.componentReportOrderHandler.CreatePaymentOrder)
}
// 组件报告订单相关接口 - 需要认证
componentReportOrder := engine.Group("/api/v1/component-report", r.auth.Handle())
{
// 检查支付状态
componentReportOrder.GET("/check-payment/:orderId", r.componentReportOrderHandler.CheckPaymentStatus)
// 下载文件
componentReportOrder.GET("/download/:orderId", r.componentReportOrderHandler.DownloadFile)
// 获取用户订单列表
componentReportOrder.GET("/orders", r.componentReportOrderHandler.GetUserOrders)
}
r.logger.Info("组件报告订单路由注册完成")
}

View File

@@ -69,6 +69,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页 walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态 walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态
walletGroup.GET("/wechat-order-status", r.financeHandler.GetWechatOrderStatus) // 获取微信订单状态 walletGroup.GET("/wechat-order-status", r.financeHandler.GetWechatOrderStatus) // 获取微信订单状态
financeGroup.GET("/purchase-records", r.financeHandler.GetUserPurchaseRecords) // 用户购买记录分页
} }
} }
@@ -91,6 +92,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值 adminFinanceGroup.POST("/transfer-recharge", r.financeHandler.TransferRecharge) // 对公转账充值
adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值 adminFinanceGroup.POST("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页 adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
adminFinanceGroup.GET("/purchase-records", r.financeHandler.GetAdminPurchaseRecords) // 管理员购买记录分页
} }
// 管理员发票相关路由组 // 管理员发票相关路由组

View File

@@ -81,8 +81,6 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
subscriptions.POST("/batch-update-prices", r.handler.BatchUpdateSubscriptionPrices) subscriptions.POST("/batch-update-prices", r.handler.BatchUpdateSubscriptionPrices)
} }
// 消费记录管理 // 消费记录管理
walletTransactions := adminGroup.Group("/wallet-transactions") walletTransactions := adminGroup.Group("/wallet-transactions")
{ {

View File

@@ -70,14 +70,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
componentReport.POST("/generate-and-download", r.componentReportHandler.GenerateAndDownloadZip) componentReport.POST("/generate-and-download", r.componentReportHandler.GenerateAndDownloadZip)
} }
// 产品组件报告相关接口 - 需要认证 // 产品组件报告相关接口 - 已迁移到 ComponentReportOrderRoutes
componentReportGroup := products.Group("/:id/component-report", r.auth.Handle())
{
componentReportGroup.GET("/check", r.componentReportHandler.CheckDownloadAvailability)
componentReportGroup.GET("/info", r.componentReportHandler.GetDownloadInfo)
componentReportGroup.POST("/create-order", r.componentReportHandler.CreatePaymentOrder)
componentReportGroup.GET("/check-payment/:orderId", r.componentReportHandler.CheckPaymentStatus)
}
// 分类 - 公开接口 // 分类 - 公开接口
categories := engine.Group("/api/v1/categories") categories := engine.Group("/api/v1/categories")

View File

@@ -1,34 +1,42 @@
package routes package routes
import ( import (
"github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
"tyapi-server/internal/infrastructure/http/handlers" "tyapi-server/internal/infrastructure/http/handlers"
"tyapi-server/internal/shared/interfaces" sharedhttp "tyapi-server/internal/shared/http"
"tyapi-server/internal/shared/middleware"
) )
// UIComponentRoutes UI组件路由 // UIComponentRoutes UI组件路由
type UIComponentRoutes struct { type UIComponentRoutes struct {
uiComponentHandler *handlers.UIComponentHandler uiComponentHandler *handlers.UIComponentHandler
logger *zap.Logger logger *zap.Logger
auth *middleware.JWTAuthMiddleware
admin *middleware.AdminAuthMiddleware
} }
// NewUIComponentRoutes 创建UI组件路由 // NewUIComponentRoutes 创建UI组件路由
func NewUIComponentRoutes( func NewUIComponentRoutes(
uiComponentHandler *handlers.UIComponentHandler, uiComponentHandler *handlers.UIComponentHandler,
logger *zap.Logger, logger *zap.Logger,
auth *middleware.JWTAuthMiddleware,
admin *middleware.AdminAuthMiddleware,
) *UIComponentRoutes { ) *UIComponentRoutes {
return &UIComponentRoutes{ return &UIComponentRoutes{
uiComponentHandler: uiComponentHandler, uiComponentHandler: uiComponentHandler,
logger: logger, logger: logger,
auth: auth,
admin: admin,
} }
} }
// RegisterRoutes 注册UI组件路由 // RegisterRoutes 注册UI组件路由
func (r *UIComponentRoutes) RegisterRoutes(router *gin.RouterGroup, authMiddleware interfaces.Middleware) { func (r *UIComponentRoutes) Register(router *sharedhttp.GinRouter) {
uiComponentGroup := router.Group("/ui-components") // 管理员路由组
uiComponentGroup.Use(authMiddleware.Handle()) engine := router.GetEngine()
uiComponentGroup := engine.Group("/api/v1/admin/ui-components")
uiComponentGroup.Use(r.admin.Handle()) // 管理员权限验证
{ {
// UI组件管理 // UI组件管理
uiComponentGroup.POST("", r.uiComponentHandler.CreateUIComponent) // 创建UI组件 uiComponentGroup.POST("", r.uiComponentHandler.CreateUIComponent) // 创建UI组件

View File

@@ -193,6 +193,13 @@ componentReportHandler := component_report.NewComponentReportHandler(
productRepo, productRepo,
docRepo, docRepo,
apiConfigRepo, apiConfigRepo,
componentReportRepo,
purchaseOrderRepo,
rechargeRecordRepo,
alipayOrderRepo,
wechatOrderRepo,
aliPayService,
wechatPayService,
logger, logger,
) )

View File

@@ -0,0 +1,137 @@
package component_report
import (
"fmt"
"os"
"path/filepath"
"time"
"go.uber.org/zap"
)
// CacheManager 缓存管理器
type CacheManager struct {
cacheDir string
ttl time.Duration
logger *zap.Logger
}
// NewCacheManager 创建缓存管理器
func NewCacheManager(cacheDir string, ttl time.Duration, logger *zap.Logger) *CacheManager {
return &CacheManager{
cacheDir: cacheDir,
ttl: ttl,
logger: logger,
}
}
// CleanExpiredCache 清理过期缓存
func (cm *CacheManager) CleanExpiredCache() error {
// 确保缓存目录存在
if _, err := os.Stat(cm.cacheDir); os.IsNotExist(err) {
return nil // 目录不存在,无需清理
}
// 遍历缓存目录
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 跳过目录
if info.IsDir() {
return nil
}
// 检查文件是否过期
if time.Since(info.ModTime()) > cm.ttl {
// cm.logger.Debug("删除过期缓存文件",
// zap.String("path", path),
// zap.Time("mod_time", info.ModTime()),
// zap.Duration("age", time.Since(info.ModTime())))
if err := os.Remove(path); err != nil {
cm.logger.Error("删除过期缓存文件失败",
zap.Error(err),
zap.String("path", path))
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("清理过期缓存失败: %w", err)
}
// cm.logger.Info("缓存清理完成", zap.String("cache_dir", cm.cacheDir))
return nil
}
// GetCacheSize 获取缓存总大小
func (cm *CacheManager) GetCacheSize() (int64, error) {
var totalSize int64
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalSize += info.Size()
}
return nil
})
if err != nil {
return 0, fmt.Errorf("计算缓存大小失败: %w", err)
}
return totalSize, nil
}
// GetCacheCount 获取缓存文件数量
func (cm *CacheManager) GetCacheCount() (int, error) {
var count int
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
count++
}
return nil
})
if err != nil {
return 0, fmt.Errorf("统计缓存文件数量失败: %w", err)
}
return count, nil
}
// ClearAllCache 清理所有缓存
func (cm *CacheManager) ClearAllCache() error {
// 确保缓存目录存在
if _, err := os.Stat(cm.cacheDir); os.IsNotExist(err) {
return nil // 目录不存在,无需清理
}
err := os.RemoveAll(cm.cacheDir)
if err != nil {
return fmt.Errorf("清理所有缓存失败: %w", err)
}
// 重新创建目录
if err := os.MkdirAll(cm.cacheDir, 0755); err != nil {
return fmt.Errorf("重新创建缓存目录失败: %w", err)
}
// cm.logger.Info("所有缓存已清理", zap.String("cache_dir", cm.cacheDir))
return nil
}

View File

@@ -0,0 +1,102 @@
package component_report
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
finance_entities "tyapi-server/internal/domains/finance/entities"
)
// CheckPaymentStatusFixed 修复版检查支付状态方法
func (h *ComponentReportHandler) CheckPaymentStatusFixed(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
orderID := c.Param("orderId")
if orderID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "订单ID不能为空",
})
return
}
// 根据订单ID查询下载记录
download, err := h.componentReportRepo.GetDownloadByID(c.Request.Context(), orderID)
if err != nil {
h.logger.Error("查询下载记录失败", zap.Error(err), zap.String("order_id", orderID))
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "订单不存在",
})
return
}
// 验证订单是否属于当前用户
if download.UserID != userID {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"message": "无权访问此订单",
})
return
}
// 使用购买订单状态来判断支付状态
var paymentStatus string
var canDownload bool
// 优先使用OrderID查询购买订单状态
if download.OrderID != nil {
// 查询购买订单状态
purchaseOrder, err := h.purchaseOrderRepo.GetByID(c.Request.Context(), *download.OrderID)
if err != nil {
h.logger.Error("查询购买订单失败", zap.Error(err), zap.String("order_id", *download.OrderID))
paymentStatus = "unknown"
} else {
// 根据购买订单状态设置支付状态
switch purchaseOrder.Status {
case finance_entities.PurchaseOrderStatusPaid:
paymentStatus = "success"
canDownload = true
case finance_entities.PurchaseOrderStatusCreated:
paymentStatus = "pending"
canDownload = false
case finance_entities.PurchaseOrderStatusCancelled:
paymentStatus = "cancelled"
canDownload = false
case finance_entities.PurchaseOrderStatusFailed:
paymentStatus = "failed"
canDownload = false
default:
paymentStatus = "unknown"
canDownload = false
}
}
} else if download.OrderNumber != nil {
// 兼容旧的支付订单逻辑
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
canDownload = true
} else {
paymentStatus = "pending"
canDownload = false
}
// 检查是否过期
if download.IsExpired() {
canDownload = false
}
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
OrderID: download.ID,
PaymentStatus: paymentStatus,
CanDownload: canDownload,
})
}

View File

@@ -2,12 +2,15 @@ package component_report
import ( import (
"context" "context"
"crypto/md5"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
"go.uber.org/zap" "go.uber.org/zap"
@@ -21,6 +24,10 @@ type ExampleJSONGenerator struct {
docRepo repositories.ProductDocumentationRepository docRepo repositories.ProductDocumentationRepository
apiConfigRepo repositories.ProductApiConfigRepository apiConfigRepo repositories.ProductApiConfigRepository
logger *zap.Logger logger *zap.Logger
// 缓存配置
CacheEnabled bool
CacheDir string
CacheTTL time.Duration
} }
// NewExampleJSONGenerator 创建示例JSON生成器 // NewExampleJSONGenerator 创建示例JSON生成器
@@ -35,6 +42,30 @@ func NewExampleJSONGenerator(
docRepo: docRepo, docRepo: docRepo,
apiConfigRepo: apiConfigRepo, apiConfigRepo: apiConfigRepo,
logger: logger, logger: logger,
CacheEnabled: true,
CacheDir: "storage/component-reports/cache",
CacheTTL: 24 * time.Hour, // 默认缓存24小时
}
}
// NewExampleJSONGeneratorWithCache 创建带有自定义缓存配置的示例JSON生成器
func NewExampleJSONGeneratorWithCache(
productRepo repositories.ProductRepository,
docRepo repositories.ProductDocumentationRepository,
apiConfigRepo repositories.ProductApiConfigRepository,
logger *zap.Logger,
cacheEnabled bool,
cacheDir string,
cacheTTL time.Duration,
) *ExampleJSONGenerator {
return &ExampleJSONGenerator{
productRepo: productRepo,
docRepo: docRepo,
apiConfigRepo: apiConfigRepo,
logger: logger,
CacheEnabled: cacheEnabled,
CacheDir: cacheDir,
CacheTTL: cacheTTL,
} }
} }
@@ -54,6 +85,20 @@ type ExampleJSONItem struct {
// productID: 产品ID可以是组合包或单品 // productID: 产品ID可以是组合包或单品
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品) // subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) { func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) {
// 生成缓存键
cacheKey := g.generateCacheKey(productID, subProductCodes)
// 检查缓存
if g.CacheEnabled {
cachedData, err := g.getCachedData(cacheKey)
if err == nil && cachedData != nil {
// g.logger.Debug("使用缓存的example.json数据",
// zap.String("product_id", productID),
// zap.String("cache_key", cacheKey))
return cachedData, nil
}
}
// 1. 获取产品信息 // 1. 获取产品信息
product, err := g.productRepo.GetByID(ctx, productID) product, err := g.productRepo.GetByID(ctx, productID)
if err != nil { if err != nil {
@@ -157,12 +202,21 @@ func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productI
return nil, fmt.Errorf("序列化example.json失败: %w", err) return nil, fmt.Errorf("序列化example.json失败: %w", err)
} }
// 缓存数据
if g.CacheEnabled {
if err := g.cacheData(cacheKey, jsonData); err != nil {
g.logger.Warn("缓存example.json数据失败", zap.Error(err))
} else {
g.logger.Debug("example.json数据已缓存", zap.String("cache_key", cacheKey))
}
}
return jsonData, nil return jsonData, nil
} }
// MatchProductCodeToPath 根据产品编码匹配 UI 组件路径返回路径和类型folder/file // MatchSubProductCodeToPath 根据产品编码匹配 UI 组件路径返回路径和类型folder/file
func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) { func (g *ExampleJSONGenerator) MatchSubProductCodeToPath(ctx context.Context, subProductCode string) (string, string, error) {
basePath := filepath.Join("resources", "Pure Component", "src", "ui") basePath := filepath.Join("resources", "Pure_Component", "src", "ui")
entries, err := os.ReadDir(basePath) entries, err := os.ReadDir(basePath)
if err != nil { if err != nil {
@@ -172,18 +226,8 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
for _, entry := range entries { for _, entry := range entries {
name := entry.Name() name := entry.Name()
// 精确匹配 // 使用改进的相似性匹配算法
if name == productCode { if isSimilarCode(subProductCode, name) {
path := filepath.Join(basePath, name)
fileType := "folder"
if !entry.IsDir() {
fileType = "file"
}
return path, fileType, nil
}
// 模糊匹配:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
if strings.Contains(name, productCode) || strings.Contains(productCode, extractCoreCode(name)) {
path := filepath.Join(basePath, name) path := filepath.Join(basePath, name)
fileType := "folder" fileType := "folder"
if !entry.IsDir() { if !entry.IsDir() {
@@ -193,7 +237,7 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
} }
} }
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", productCode) return "", "", fmt.Errorf("未找到匹配的组件文件: %s", subProductCode)
} }
// extractCoreCode 提取文件名中的核心编码部分 // extractCoreCode 提取文件名中的核心编码部分
@@ -206,6 +250,44 @@ func extractCoreCode(name string) string {
return name return name
} }
// extractMainCode 从子产品编码或文件夹名称中提取主要编码部分
// 处理可能的格式差异,如前缀、后缀等
func extractMainCode(code string) string {
// 移除常见的前缀,如 C
if len(code) > 0 && code[0] == 'C' {
return code[1:]
}
return code
}
// isSimilarCode 判断两个编码是否相似,考虑多种可能的格式差异
func isSimilarCode(code1, code2 string) bool {
// 直接相等
if code1 == code2 {
return true
}
// 移除常见前缀后比较
mainCode1 := extractMainCode(code1)
mainCode2 := extractMainCode(code2)
if mainCode1 == mainCode2 || mainCode1 == code2 || code1 == mainCode2 {
return true
}
// 包含关系
if strings.Contains(code1, code2) || strings.Contains(code2, code1) {
return true
}
// 移除前缀后的包含关系
if strings.Contains(mainCode1, code2) || strings.Contains(code2, mainCode1) ||
strings.Contains(code1, mainCode2) || strings.Contains(mainCode2, code1) {
return true
}
return false
}
// extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值) // extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值)
func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, product *entities.Product) interface{} { func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, product *entities.Product) interface{} {
var responseData interface{} var responseData interface{}
@@ -216,20 +298,20 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
// 尝试直接解析为JSON // 尝试直接解析为JSON
err := json.Unmarshal([]byte(doc.ResponseExample), &responseData) err := json.Unmarshal([]byte(doc.ResponseExample), &responseData)
if err == nil { if err == nil {
g.logger.Debug("从产品文档中提取响应示例成功", // g.logger.Debug("从产品文档中提取响应示例成功",
zap.String("product_id", product.ID), // zap.String("product_id", product.ID),
zap.String("product_code", product.Code), // zap.String("product_code", product.Code),
) // )
return responseData return responseData
} }
// 如果解析失败尝试从Markdown代码块中提取JSON // 如果解析失败尝试从Markdown代码块中提取JSON
extractedData := extractJSONFromMarkdown(doc.ResponseExample) extractedData := extractJSONFromMarkdown(doc.ResponseExample)
if extractedData != nil { if extractedData != nil {
g.logger.Debug("从Markdown代码块中提取响应示例成功", // g.logger.Debug("从Markdown代码块中提取响应示例成功",
zap.String("product_id", product.ID), // zap.String("product_id", product.ID),
zap.String("product_code", product.Code), // zap.String("product_code", product.Code),
) // )
return extractedData return extractedData
} }
} }
@@ -240,10 +322,10 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
// API配置的响应示例通常是 JSON 字符串 // API配置的响应示例通常是 JSON 字符串
err := json.Unmarshal([]byte(apiConfig.ResponseExample), &responseData) err := json.Unmarshal([]byte(apiConfig.ResponseExample), &responseData)
if err == nil { if err == nil {
g.logger.Debug("从产品API配置中提取响应示例成功", // g.logger.Debug("从产品API配置中提取响应示例成功",
zap.String("product_id", product.ID), // zap.String("product_id", product.ID),
zap.String("product_code", product.Code), // zap.String("product_code", product.Code),
) // )
return responseData return responseData
} }
} }
@@ -284,3 +366,57 @@ func extractJSONFromMarkdown(markdown string) interface{} {
// 如果提取失败,返回 nil由调用者决定默认值 // 如果提取失败,返回 nil由调用者决定默认值
return nil return nil
} }
// generateCacheKey 生成缓存键
func (g *ExampleJSONGenerator) generateCacheKey(productID string, subProductCodes []string) string {
// 使用产品ID和子产品编码列表生成MD5哈希
data := productID
for _, code := range subProductCodes {
data += "|" + code
}
hash := md5.Sum([]byte(data))
return hex.EncodeToString(hash[:]) + ".json"
}
// getCachedData 获取缓存数据
func (g *ExampleJSONGenerator) getCachedData(cacheKey string) ([]byte, error) {
// 确保缓存目录存在
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
return nil, fmt.Errorf("创建缓存目录失败: %w", err)
}
cacheFilePath := filepath.Join(g.CacheDir, cacheKey)
// 检查文件是否存在
fileInfo, err := os.Stat(cacheFilePath)
if os.IsNotExist(err) {
return nil, nil // 文件不存在,但不是错误
}
if err != nil {
return nil, err
}
// 检查文件是否过期
if time.Since(fileInfo.ModTime()) > g.CacheTTL {
// 文件过期,删除
os.Remove(cacheFilePath)
return nil, nil
}
// 读取文件内容
return os.ReadFile(cacheFilePath)
}
// cacheData 缓存数据
func (g *ExampleJSONGenerator) cacheData(cacheKey string, data []byte) error {
// 确保缓存目录存在
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
return fmt.Errorf("创建缓存目录失败: %w", err)
}
cacheFilePath := filepath.Join(g.CacheDir, cacheKey)
// 写入文件
return os.WriteFile(cacheFilePath, data, 0644)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
package component_report
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
finance_entities "tyapi-server/internal/domains/finance/entities"
financeRepositories "tyapi-server/internal/domains/finance/repositories"
"tyapi-server/internal/domains/product/repositories"
"tyapi-server/internal/shared/payment"
)
// ComponentReportHandler 组件报告处理器
type ComponentReportHandlerFixed struct {
exampleJSONGenerator *ExampleJSONGenerator
zipGenerator *ZipGenerator
productRepo repositories.ProductRepository
componentReportRepo repositories.ComponentReportRepository
purchaseOrderRepo financeRepositories.PurchaseOrderRepository
rechargeRecordRepo interface {
Create(ctx context.Context, record finance_entities.RechargeRecord) (finance_entities.RechargeRecord, error)
}
alipayOrderRepo interface {
Create(ctx context.Context, order finance_entities.AlipayOrder) (finance_entities.AlipayOrder, error)
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.AlipayOrder, error)
Update(ctx context.Context, order finance_entities.AlipayOrder) error
}
wechatOrderRepo interface {
Create(ctx context.Context, order finance_entities.WechatOrder) (finance_entities.WechatOrder, error)
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.WechatOrder, error)
Update(ctx context.Context, order finance_entities.WechatOrder) error
}
aliPayService *payment.AliPayService
wechatPayService *payment.WechatPayService
logger *zap.Logger
}
// NewComponentReportHandlerFixed 创建组件报告处理器(修复版)
func NewComponentReportHandlerFixed(
productRepo repositories.ProductRepository,
docRepo repositories.ProductDocumentationRepository,
apiConfigRepo repositories.ProductApiConfigRepository,
componentReportRepo repositories.ComponentReportRepository,
purchaseOrderRepo financeRepositories.PurchaseOrderRepository,
rechargeRecordRepo interface {
Create(ctx context.Context, record finance_entities.RechargeRecord) (finance_entities.RechargeRecord, error)
},
alipayOrderRepo interface {
Create(ctx context.Context, order finance_entities.AlipayOrder) (finance_entities.AlipayOrder, error)
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.AlipayOrder, error)
Update(ctx context.Context, order finance_entities.AlipayOrder) error
},
wechatOrderRepo interface {
Create(ctx context.Context, order finance_entities.WechatOrder) (finance_entities.WechatOrder, error)
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.WechatOrder, error)
Update(ctx context.Context, order finance_entities.WechatOrder) error
},
aliPayService *payment.AliPayService,
wechatPayService *payment.WechatPayService,
logger *zap.Logger,
) *ComponentReportHandlerFixed {
exampleJSONGenerator := NewExampleJSONGenerator(productRepo, docRepo, apiConfigRepo, logger)
zipGenerator := NewZipGenerator(logger)
return &ComponentReportHandlerFixed{
exampleJSONGenerator: exampleJSONGenerator,
zipGenerator: zipGenerator,
productRepo: productRepo,
componentReportRepo: componentReportRepo,
purchaseOrderRepo: purchaseOrderRepo,
rechargeRecordRepo: rechargeRecordRepo,
alipayOrderRepo: alipayOrderRepo,
wechatOrderRepo: wechatOrderRepo,
aliPayService: aliPayService,
wechatPayService: wechatPayService,
logger: logger,
}
}
// CheckPaymentStatusFixed 检查支付状态(修复版)
func (h *ComponentReportHandlerFixed) CheckPaymentStatusFixed(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "用户未登录",
})
return
}
orderID := c.Param("orderId")
if orderID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "订单ID不能为空",
})
return
}
// 根据订单ID查询下载记录
download, err := h.componentReportRepo.GetDownloadByID(c.Request.Context(), orderID)
if err != nil {
h.logger.Error("查询下载记录失败", zap.Error(err), zap.String("order_id", orderID))
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "订单不存在",
})
return
}
// 验证订单是否属于当前用户
if download.UserID != userID {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"message": "无权访问此订单",
})
return
}
// 使用购买订单状态来判断支付状态
var paymentStatus string
var canDownload bool
if download.OrderID != nil {
// 查询购买订单状态
purchaseOrder, err := h.purchaseOrderRepo.GetByID(c.Request.Context(), *download.OrderID)
if err != nil {
h.logger.Error("查询购买订单失败", zap.Error(err), zap.String("OrderID", *download.OrderID))
paymentStatus = "unknown"
} else {
// 根据购买订单状态设置支付状态
switch purchaseOrder.Status {
case finance_entities.PurchaseOrderStatusPaid:
paymentStatus = "success"
canDownload = true
case finance_entities.PurchaseOrderStatusCreated:
paymentStatus = "pending"
canDownload = false
case finance_entities.PurchaseOrderStatusCancelled:
paymentStatus = "cancelled"
canDownload = false
case finance_entities.PurchaseOrderStatusFailed:
paymentStatus = "failed"
canDownload = false
default:
paymentStatus = "unknown"
canDownload = false
}
}
} else if download.OrderNumber != nil {
// 兼容旧的支付订单逻辑
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
canDownload = true
} else {
paymentStatus = "pending"
canDownload = false
}
// 检查是否过期
if download.IsExpired() {
canDownload = false
}
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
OrderID: download.ID,
PaymentStatus: paymentStatus,
CanDownload: canDownload,
})
}

View File

@@ -3,11 +3,14 @@ package component_report
import ( import (
"archive/zip" "archive/zip"
"context" "context"
"crypto/md5"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -15,18 +18,35 @@ import (
// ZipGenerator ZIP文件生成器 // ZipGenerator ZIP文件生成器
type ZipGenerator struct { type ZipGenerator struct {
logger *zap.Logger logger *zap.Logger
// 缓存配置
CacheEnabled bool
CacheDir string
CacheTTL time.Duration
} }
// NewZipGenerator 创建ZIP文件生成器 // NewZipGenerator 创建ZIP文件生成器
func NewZipGenerator(logger *zap.Logger) *ZipGenerator { func NewZipGenerator(logger *zap.Logger) *ZipGenerator {
return &ZipGenerator{ return &ZipGenerator{
logger: logger, logger: logger,
CacheEnabled: true,
CacheDir: "storage/component-reports/cache",
CacheTTL: 24 * time.Hour, // 默认缓存24小时
} }
} }
// GenerateZipFile 生成ZIP文件包含 example.json 和匹配的组件文件 // NewZipGeneratorWithCache 创建带有自定义缓存配置的ZIP文件生成器
func NewZipGeneratorWithCache(logger *zap.Logger, cacheEnabled bool, cacheDir string, cacheTTL time.Duration) *ZipGenerator {
return &ZipGenerator{
logger: logger,
CacheEnabled: cacheEnabled,
CacheDir: cacheDir,
CacheTTL: cacheTTL,
}
}
// GenerateZipFile 生成ZIP文件包含 example.json 和根据子产品编码匹配的UI组件文件
// productID: 产品ID // productID: 产品ID
// subProductCodes: 子产品编列表(如果为空,则处理所有子产品 // subProductCodes: 子产品编列表(用于过滤和下载匹配的UI组件
// exampleJSONGenerator: 示例JSON生成器 // exampleJSONGenerator: 示例JSON生成器
// outputPath: 输出ZIP文件路径如果为空则使用默认路径 // outputPath: 输出ZIP文件路径如果为空则使用默认路径
func (g *ZipGenerator) GenerateZipFile( func (g *ZipGenerator) GenerateZipFile(
@@ -36,6 +56,29 @@ func (g *ZipGenerator) GenerateZipFile(
exampleJSONGenerator *ExampleJSONGenerator, exampleJSONGenerator *ExampleJSONGenerator,
outputPath string, outputPath string,
) (string, error) { ) (string, error) {
// 生成缓存键
cacheKey := g.generateCacheKey(productID, subProductCodes)
// 检查缓存
if g.CacheEnabled {
cachedPath, err := g.getCachedFile(cacheKey)
if err == nil && cachedPath != "" {
// g.logger.Debug("使用缓存的ZIP文件",
// zap.String("product_id", productID),
// zap.String("cache_path", cachedPath))
// 如果指定了输出路径,复制缓存文件到目标位置
if outputPath != "" && outputPath != cachedPath {
if err := g.copyFile(cachedPath, outputPath); err != nil {
g.logger.Error("复制缓存文件失败", zap.Error(err))
} else {
return outputPath, nil
}
}
return cachedPath, nil
}
}
// 1. 生成 example.json 内容 // 1. 生成 example.json 内容
exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes) exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes)
if err != nil { if err != nil {
@@ -62,8 +105,8 @@ func (g *ZipGenerator) GenerateZipFile(
zipWriter := zip.NewWriter(zipFile) zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close() defer zipWriter.Close()
// 4. 添加 example.json 到 public 目录 // 4. 将生成的内容添加到 Pure_Component/public 目录下的 example.json
exampleWriter, err := zipWriter.Create("public/example.json") exampleWriter, err := zipWriter.Create("Pure_Component/public/example.json")
if err != nil { if err != nil {
return "", fmt.Errorf("创建example.json文件失败: %w", err) return "", fmt.Errorf("创建example.json文件失败: %w", err)
} }
@@ -73,14 +116,14 @@ func (g *ZipGenerator) GenerateZipFile(
return "", fmt.Errorf("写入example.json失败: %w", err) return "", fmt.Errorf("写入example.json失败: %w", err)
} }
// 5. 添加整个 src 目录,但过滤 ui 目录下的文件 // 5. 添加整个 Pure_Component 目录但只包含子产品编码匹配的UI组件文件
srcBasePath := filepath.Join("resources", "Pure Component", "src") srcBasePath := filepath.Join("resources", "Pure_Component")
uiBasePath := filepath.Join(srcBasePath, "ui") uiBasePath := filepath.Join(srcBasePath, "src", "ui")
// 收集所有匹配的组件名称(文件夹名或文件名) // 根据子产品编码收集所有匹配的组件名称(文件夹名或文件名)
matchedNames := make(map[string]bool) matchedNames := make(map[string]bool)
for _, productCode := range subProductCodes { for _, subProductCode := range subProductCodes {
path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode) path, _, err := exampleJSONGenerator.MatchSubProductCodeToPath(ctx, subProductCode)
if err == nil && path != "" { if err == nil && path != "" {
// 获取组件名称(文件夹名或文件名) // 获取组件名称(文件夹名或文件名)
componentName := filepath.Base(path) componentName := filepath.Base(path)
@@ -88,20 +131,20 @@ func (g *ZipGenerator) GenerateZipFile(
} }
} }
// 遍历整个 src 目录 // 遍历整个 Pure_Component 目录
err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error { err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
// 计算相对于 src 的路径 // 计算相对于 Pure_Component 的路径
relPath, err := filepath.Rel(srcBasePath, path) relPath, err := filepath.Rel(srcBasePath, path)
if err != nil { if err != nil {
return err return err
} }
// 转换为ZIP路径格式 // 转换为ZIP路径格式保持在Pure_Component目录下
zipPath := filepath.ToSlash(filepath.Join("src", relPath)) zipPath := filepath.ToSlash(filepath.Join("Pure_Component", relPath))
// 检查是否在 ui 目录下 // 检查是否在 ui 目录下
uiRelPath, err := filepath.Rel(uiBasePath, path) uiRelPath, err := filepath.Rel(uiBasePath, path)
@@ -120,26 +163,19 @@ func (g *ZipGenerator) GenerateZipFile(
// 获取文件/文件夹名称 // 获取文件/文件夹名称
fileName := info.Name() fileName := info.Name()
// 检查是否应该保留: // 检查是否应该保留:匹配到的组件文件夹/文件
// 1. CBehaviorRiskScan.vue 文件(无论在哪里)
// 2. 匹配到的组件文件夹/文件
shouldInclude := false shouldInclude := false
// 检查是否是 CBehaviorRiskScan.vue // 检查是否是匹配的组件(检查组件名称)
if fileName == "CBehaviorRiskScan.vue" { if matchedNames[fileName] {
shouldInclude = true shouldInclude = true
} else { } else {
// 检查是否匹配的组件(检查组件名称) // 检查是否匹配的组件文件夹内
if matchedNames[fileName] { // 获取相对于 ui 的路径的第一部分(组件文件夹名)
shouldInclude = true parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
} else { if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
// 检查是否在匹配的组件文件夹内 if matchedNames[parts[0]] {
// 获取相对于 ui 的路径的第一部分(组件文件夹名) shouldInclude = true
parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
if matchedNames[parts[0]] {
shouldInclude = true
}
} }
} }
} }
@@ -164,7 +200,7 @@ func (g *ZipGenerator) GenerateZipFile(
}) })
if err != nil { if err != nil {
g.logger.Warn("添加src目录失败", zap.Error(err)) g.logger.Warn("添加Pure_Component目录失败", zap.Error(err))
} }
g.logger.Info("成功生成ZIP文件", g.logger.Info("成功生成ZIP文件",
@@ -174,6 +210,15 @@ func (g *ZipGenerator) GenerateZipFile(
zap.Int("sub_product_count", len(subProductCodes)), zap.Int("sub_product_count", len(subProductCodes)),
) )
// 缓存文件
if g.CacheEnabled {
if err := g.cacheFile(outputPath, cacheKey); err != nil {
g.logger.Warn("缓存ZIP文件失败", zap.Error(err))
} else {
g.logger.Debug("ZIP文件已缓存", zap.String("cache_key", cacheKey))
}
}
return outputPath, nil return outputPath, nil
} }
@@ -263,3 +308,197 @@ func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPat
return g.AddFileToZip(zipWriter, path, zipPath) return g.AddFileToZip(zipWriter, path, zipPath)
}) })
} }
// GenerateFilteredComponentZip 生成筛选后的组件ZIP文件
// productID: 产品ID
// subProductCodes: 子产品编号列表(用于筛选组件)
// outputPath: 输出ZIP文件路径如果为空则使用默认路径
func (g *ZipGenerator) GenerateFilteredComponentZip(
ctx context.Context,
productID string,
subProductCodes []string,
outputPath string,
) (string, error) {
// 1. 确定基础路径
basePath := filepath.Join("resources", "Pure_Component")
uiBasePath := filepath.Join(basePath, "src", "ui")
// 2. 确定输出路径
if outputPath == "" {
// 使用默认路径storage/component-reports/{productID}_filtered.zip
outputDir := "storage/component-reports"
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("创建输出目录失败: %w", err)
}
outputPath = filepath.Join(outputDir, fmt.Sprintf("%s_filtered.zip", productID))
}
// 3. 创建ZIP文件
zipFile, err := os.Create(outputPath)
if err != nil {
return "", fmt.Errorf("创建ZIP文件失败: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// 4. 收集所有匹配的组件名称(文件夹名或文件名)
matchedNames := make(map[string]bool)
for _, productCode := range subProductCodes {
// 简化匹配逻辑,直接使用产品代码作为组件名
matchedNames[productCode] = true
}
// 5. 递归添加整个 Pure_Component 目录,但筛选 ui 目录下的内容
err = filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对于基础路径的相对路径
relPath, err := filepath.Rel(basePath, path)
if err != nil {
return err
}
// 转换为ZIP路径格式保持在Pure_Component目录下
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", relPath))
// 检查是否在 ui 目录下
uiRelPath, err := filepath.Rel(uiBasePath, path)
isInUIDir := err == nil && !strings.HasPrefix(uiRelPath, "..")
if isInUIDir {
// 如果是 ui 目录本身,直接添加
if uiRelPath == "." || uiRelPath == "" {
if info.IsDir() {
_, err = zipWriter.Create(zipPath + "/")
return err
}
return nil
}
// 获取文件/文件夹名称
fileName := info.Name()
// 检查是否应该保留:匹配到的组件文件夹/文件
shouldInclude := false
// 检查是否是匹配的组件(检查组件名称)
if matchedNames[fileName] {
shouldInclude = true
} else {
// 检查是否在匹配的组件文件夹内
// 获取相对于 ui 的路径的第一部分(组件文件夹名)
parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
if matchedNames[parts[0]] {
shouldInclude = true
}
}
}
if !shouldInclude {
// 跳过不匹配的文件/文件夹
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
// 如果是目录,创建目录项
if info.IsDir() {
_, err = zipWriter.Create(zipPath + "/")
return err
}
// 添加文件
return g.AddFileToZip(zipWriter, path, zipPath)
})
if err != nil {
g.logger.Warn("添加Pure_Component目录失败", zap.Error(err))
return "", fmt.Errorf("添加Pure_Component目录失败: %w", err)
}
g.logger.Info("成功生成筛选后的组件ZIP文件",
zap.String("product_id", productID),
zap.String("output_path", outputPath),
zap.Int("matched_components_count", len(matchedNames)),
)
return outputPath, nil
}
// generateCacheKey 生成缓存键
func (g *ZipGenerator) generateCacheKey(productID string, subProductCodes []string) string {
// 使用产品ID和子产品编码列表生成MD5哈希
data := productID
for _, code := range subProductCodes {
data += "|" + code
}
hash := md5.Sum([]byte(data))
return hex.EncodeToString(hash[:])
}
// getCachedFile 获取缓存文件
func (g *ZipGenerator) getCachedFile(cacheKey string) (string, error) {
// 确保缓存目录存在
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
return "", fmt.Errorf("创建缓存目录失败: %w", err)
}
cacheFilePath := filepath.Join(g.CacheDir, cacheKey+".zip")
// 检查文件是否存在
fileInfo, err := os.Stat(cacheFilePath)
if os.IsNotExist(err) {
return "", nil // 文件不存在,但不是错误
}
if err != nil {
return "", err
}
// 检查文件是否过期
if time.Since(fileInfo.ModTime()) > g.CacheTTL {
// 文件过期,删除
os.Remove(cacheFilePath)
return "", nil
}
return cacheFilePath, nil
}
// cacheFile 缓存文件
func (g *ZipGenerator) cacheFile(filePath, cacheKey string) error {
// 确保缓存目录存在
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
return fmt.Errorf("创建缓存目录失败: %w", err)
}
cacheFilePath := filepath.Join(g.CacheDir, cacheKey+".zip")
// 复制文件到缓存目录
return g.copyFile(filePath, cacheFilePath)
}
// copyFile 复制文件
func (g *ZipGenerator) copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}

View File

@@ -35,9 +35,17 @@ type CacheInfo struct {
// NewPDFCacheManager 创建PDF缓存管理器 // NewPDFCacheManager 创建PDF缓存管理器
func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) { func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) {
// 如果缓存目录为空,使用默认目录 // 如果缓存目录为空,使用项目根目录的storage/component-reports/cache目录
if cacheDir == "" { if cacheDir == "" {
cacheDir = filepath.Join(os.TempDir(), "tyapi_pdf_cache") // 获取当前工作目录并构建项目根目录路径
wd, err := os.Getwd()
if err != nil {
// 如果获取工作目录失败,回退到临时目录
cacheDir = filepath.Join(os.TempDir(), "tyapi_pdf_cache")
} else {
// 构建项目根目录下的storage/component-reports/cache路径
cacheDir = filepath.Join(wd, "storage", "component-reports", "cache")
}
} }
// 确保缓存目录存在 // 确保缓存目录存在
@@ -60,7 +68,6 @@ func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration,
zap.Duration("ttl", ttl), zap.Duration("ttl", ttl),
zap.Int64("max_size", maxSize), zap.Int64("max_size", maxSize),
) )
return manager, nil return manager, nil
} }

View File

@@ -186,6 +186,7 @@ func validateIDCardChecksum(idCard string) bool {
return byte(lastChar) == checksum return byte(lastChar) == checksum
} }
// validatePrice 价格验证 // validatePrice 价格验证
func validatePrice(fl validator.FieldLevel) bool { func validatePrice(fl validator.FieldLevel) bool {
price := fl.Field().Float() price := fl.Field().Float()
@@ -559,18 +560,18 @@ func validateReturnURL(fl validator.FieldLevel) bool {
// validateEnterpriseName 企业名称验证器 // validateEnterpriseName 企业名称验证器
func validateEnterpriseName(fl validator.FieldLevel) bool { func validateEnterpriseName(fl validator.FieldLevel) bool {
enterpriseName := fl.Field().String() enterpriseName := fl.Field().String()
// 去除首尾空格 // 去除首尾空格
trimmedName := strings.TrimSpace(enterpriseName) trimmedName := strings.TrimSpace(enterpriseName)
if trimmedName == "" { if trimmedName == "" {
return false return false
} }
// 长度验证2-100个字符 // 长度验证2-100个字符
if len(trimmedName) < 2 || len(trimmedName) > 100 { if len(trimmedName) < 2 || len(trimmedName) > 100 {
return false return false
} }
// 检查是否包含非法字符(允许中英文括号) // 检查是否包含非法字符(允许中英文括号)
invalidChars := []string{ invalidChars := []string{
"`", "~", "!", "@", "#", "$", "%", "^", "&", "*", "`", "~", "!", "@", "#", "$", "%", "^", "&", "*",
@@ -581,13 +582,13 @@ func validateEnterpriseName(fl validator.FieldLevel) bool {
return false return false
} }
} }
// 必须包含至少一个中文字符或英文字母 // 必须包含至少一个中文字符或英文字母
hasValidChar := regexp.MustCompile(`[\p{Han}a-zA-Z]`).MatchString(trimmedName) hasValidChar := regexp.MustCompile(`[\p{Han}a-zA-Z]`).MatchString(trimmedName)
if !hasValidChar { if !hasValidChar {
return false return false
} }
// 验证企业名称的基本格式(支持各种类型的企业) // 验证企业名称的基本格式(支持各种类型的企业)
// 支持:有限公司、股份有限公司、工作室、个体工商户、合伙企业等 // 支持:有限公司、股份有限公司、工作室、个体工商户、合伙企业等
validSuffixes := []string{ validSuffixes := []string{
@@ -598,7 +599,7 @@ func validateEnterpriseName(fl validator.FieldLevel) bool {
"Co.,Ltd", "Co., Ltd", "Ltd", "LLC", "Inc", "Corp", "Co.,Ltd", "Co., Ltd", "Ltd", "LLC", "Inc", "Corp",
"Company", "Studio", "Workshop", "Enterprise", "Company", "Studio", "Workshop", "Enterprise",
} }
// 检查是否以合法的企业类型结尾(不强制要求,因为有些企业名称可能没有标准后缀) // 检查是否以合法的企业类型结尾(不强制要求,因为有些企业名称可能没有标准后缀)
// 但如果有后缀,必须是合法的 // 但如果有后缀,必须是合法的
hasValidSuffix := false hasValidSuffix := false
@@ -613,7 +614,7 @@ func validateEnterpriseName(fl validator.FieldLevel) bool {
break break
} }
} }
// 如果名称中包含常见的企业类型关键词,则必须是合法的后缀 // 如果名称中包含常见的企业类型关键词,则必须是合法的后缀
enterpriseKeywords := []string{"公司", "工作室", "企业", "集团", "Co", "Ltd", "LLC", "Inc", "Corp", "Company", "Studio", "Workshop", "Enterprise"} enterpriseKeywords := []string{"公司", "工作室", "企业", "集团", "Co", "Ltd", "LLC", "Inc", "Corp", "Company", "Studio", "Workshop", "Enterprise"}
containsKeyword := false containsKeyword := false
@@ -623,12 +624,12 @@ func validateEnterpriseName(fl validator.FieldLevel) bool {
break break
} }
} }
// 如果包含企业关键词但没有合法后缀,则验证失败 // 如果包含企业关键词但没有合法后缀,则验证失败
if containsKeyword && !hasValidSuffix { if containsKeyword && !hasValidSuffix {
return false return false
} }
return true return true
} }
@@ -657,7 +658,7 @@ func ValidatePassword(password string) error {
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password) hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password) hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasDigit := regexp.MustCompile(`\d`).MatchString(password) hasDigit := regexp.MustCompile(`\d`).MatchString(password)
if !hasUpper { if !hasUpper {
return fmt.Errorf("密码必须包含大写字母") return fmt.Errorf("密码必须包含大写字母")
} }
@@ -790,7 +791,7 @@ func ValidateRequired(value interface{}, fieldName string) error {
if value == nil { if value == nil {
return fmt.Errorf("%s不能为空", fieldName) return fmt.Errorf("%s不能为空", fieldName)
} }
switch v := value.(type) { switch v := value.(type) {
case string: case string:
if strings.TrimSpace(v) == "" { if strings.TrimSpace(v) == "" {
@@ -801,7 +802,7 @@ func ValidateRequired(value interface{}, fieldName string) error {
return fmt.Errorf("%s不能为空", fieldName) return fmt.Errorf("%s不能为空", fieldName)
} }
} }
return nil return nil
} }
@@ -821,7 +822,7 @@ func ValidateSliceNotEmpty(slice interface{}, fieldName string) error {
if slice == nil { if slice == nil {
return fmt.Errorf("%s不能为空", fieldName) return fmt.Errorf("%s不能为空", fieldName)
} }
switch v := slice.(type) { switch v := slice.(type) {
case []string: case []string:
if len(v) == 0 { if len(v) == 0 {
@@ -832,7 +833,7 @@ func ValidateSliceNotEmpty(slice interface{}, fieldName string) error {
return fmt.Errorf("%s不能为空", fieldName) return fmt.Errorf("%s不能为空", fieldName)
} }
} }
return nil return nil
} }
@@ -841,13 +842,13 @@ func ValidateEnterpriseName(enterpriseName string) error {
if enterpriseName == "" { if enterpriseName == "" {
return fmt.Errorf("企业名称不能为空") return fmt.Errorf("企业名称不能为空")
} }
// 去除首尾空格 // 去除首尾空格
trimmedName := strings.TrimSpace(enterpriseName) trimmedName := strings.TrimSpace(enterpriseName)
if trimmedName == "" { if trimmedName == "" {
return fmt.Errorf("企业名称不能为空") return fmt.Errorf("企业名称不能为空")
} }
// 长度验证2-100个字符 // 长度验证2-100个字符
if len(trimmedName) < 2 { if len(trimmedName) < 2 {
return fmt.Errorf("企业名称长度不能少于2个字符") return fmt.Errorf("企业名称长度不能少于2个字符")
@@ -855,7 +856,7 @@ func ValidateEnterpriseName(enterpriseName string) error {
if len(trimmedName) > 100 { if len(trimmedName) > 100 {
return fmt.Errorf("企业名称长度不能超过100个字符") return fmt.Errorf("企业名称长度不能超过100个字符")
} }
// 检查是否包含非法字符(允许中英文括号) // 检查是否包含非法字符(允许中英文括号)
invalidChars := []string{ invalidChars := []string{
"`", "~", "!", "@", "#", "$", "%", "^", "&", "*", "`", "~", "!", "@", "#", "$", "%", "^", "&", "*",
@@ -866,13 +867,13 @@ func ValidateEnterpriseName(enterpriseName string) error {
return fmt.Errorf("企业名称不能包含特殊字符: %s", char) return fmt.Errorf("企业名称不能包含特殊字符: %s", char)
} }
} }
// 必须包含至少一个中文字符或英文字母 // 必须包含至少一个中文字符或英文字母
hasValidChar := regexp.MustCompile(`[\p{Han}a-zA-Z]`).MatchString(trimmedName) hasValidChar := regexp.MustCompile(`[\p{Han}a-zA-Z]`).MatchString(trimmedName)
if !hasValidChar { if !hasValidChar {
return fmt.Errorf("企业名称必须包含中文字符或英文字母") return fmt.Errorf("企业名称必须包含中文字符或英文字母")
} }
// 验证企业名称的基本格式(支持各种类型的企业) // 验证企业名称的基本格式(支持各种类型的企业)
// 支持:有限公司、股份有限公司、工作室、个体工商户、合伙企业等 // 支持:有限公司、股份有限公司、工作室、个体工商户、合伙企业等
validSuffixes := []string{ validSuffixes := []string{
@@ -883,7 +884,7 @@ func ValidateEnterpriseName(enterpriseName string) error {
"Co.,Ltd", "Co., Ltd", "Ltd", "LLC", "Inc", "Corp", "Co.,Ltd", "Co., Ltd", "Ltd", "LLC", "Inc", "Corp",
"Company", "Studio", "Workshop", "Enterprise", "Company", "Studio", "Workshop", "Enterprise",
} }
// 检查是否以合法的企业类型结尾 // 检查是否以合法的企业类型结尾
hasValidSuffix := false hasValidSuffix := false
for _, suffix := range validSuffixes { for _, suffix := range validSuffixes {
@@ -897,7 +898,7 @@ func ValidateEnterpriseName(enterpriseName string) error {
break break
} }
} }
// 如果名称中包含常见的企业类型关键词,则必须是合法的后缀 // 如果名称中包含常见的企业类型关键词,则必须是合法的后缀
enterpriseKeywords := []string{"公司", "工作室", "企业", "集团", "Co", "Ltd", "LLC", "Inc", "Corp", "Company", "Studio", "Workshop", "Enterprise"} enterpriseKeywords := []string{"公司", "工作室", "企业", "集团", "Co", "Ltd", "LLC", "Inc", "Corp", "Company", "Studio", "Workshop", "Enterprise"}
containsKeyword := false containsKeyword := false
@@ -907,12 +908,12 @@ func ValidateEnterpriseName(enterpriseName string) error {
break break
} }
} }
// 如果包含企业关键词但没有合法后缀,则验证失败 // 如果包含企业关键词但没有合法后缀,则验证失败
if containsKeyword && !hasValidSuffix { if containsKeyword && !hasValidSuffix {
return fmt.Errorf("企业名称格式不正确,请使用标准的企业类型后缀(如:有限公司、工作室等)") return fmt.Errorf("企业名称格式不正确,请使用标准的企业类型后缀(如:有限公司、工作室等)")
} }
return nil return nil
} }
@@ -922,9 +923,9 @@ func ValidateEnterpriseName(enterpriseName string) error {
func NewBusinessValidator() *BusinessValidator { func NewBusinessValidator() *BusinessValidator {
// 确保全局校验器已初始化 // 确保全局校验器已初始化
InitGlobalValidator() InitGlobalValidator()
return &BusinessValidator{ return &BusinessValidator{
validator: GetGlobalValidator(), // 使用全局校验器 validator: GetGlobalValidator(), // 使用全局校验器
} }
} }
@@ -946,29 +947,29 @@ func (bv *BusinessValidator) ValidateField(field interface{}, tag string) error
// validateBase64Image Base64图片格式验证器JPG、BMP、PNG // validateBase64Image Base64图片格式验证器JPG、BMP、PNG
func validateBase64Image(fl validator.FieldLevel) bool { func validateBase64Image(fl validator.FieldLevel) bool {
base64Str := fl.Field().String() base64Str := fl.Field().String()
// 如果为空,由 omitempty 处理 // 如果为空,由 omitempty 处理
if base64Str == "" { if base64Str == "" {
return true return true
} }
// 去除首尾空格 // 去除首尾空格
base64Str = strings.TrimSpace(base64Str) base64Str = strings.TrimSpace(base64Str)
if base64Str == "" { if base64Str == "" {
return false return false
} }
// 解码 base64 字符串 // 解码 base64 字符串
decoded, err := base64.StdEncoding.DecodeString(base64Str) decoded, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil { if err != nil {
return false return false
} }
// 检查数据长度(至少要有文件头) // 检查数据长度(至少要有文件头)
if len(decoded) < 4 { if len(decoded) < 4 {
return false return false
} }
// 检查文件头,判断图片格式 // 检查文件头,判断图片格式
// JPG: FF D8 FF // JPG: FF D8 FF
// PNG: 89 50 4E 47 // PNG: 89 50 4E 47
@@ -977,16 +978,16 @@ func validateBase64Image(fl validator.FieldLevel) bool {
// JPG格式 // JPG格式
return true return true
} }
if len(decoded) >= 4 && decoded[0] == 0x89 && decoded[1] == 0x50 && decoded[2] == 0x4E && decoded[3] == 0x47 { if len(decoded) >= 4 && decoded[0] == 0x89 && decoded[1] == 0x50 && decoded[2] == 0x4E && decoded[3] == 0x47 {
// PNG格式 // PNG格式
return true return true
} }
if len(decoded) >= 2 && decoded[0] == 0x42 && decoded[1] == 0x4D { if len(decoded) >= 2 && decoded[0] == 0x42 && decoded[1] == 0x4D {
// BMP格式 // BMP格式
return true return true
} }
return false return false
} }

Some files were not shown because too many files have changed in this diff Show More