Compare commits
20 Commits
report-rou
...
fe44b452e3
| Author | SHA1 | Date | |
|---|---|---|---|
| fe44b452e3 | |||
| f1ec9bfe7f | |||
| a70e736cdd | |||
| 53d2c75a9c | |||
| 0bfa7b4f50 | |||
| e2e729eec8 | |||
| 5f7fb43804 | |||
| 89c5c0f9ad | |||
| 6c949a4a1c | |||
| 8556e7331d | |||
| 311d7a9b01 | |||
| ce45ce3ed0 | |||
| 34e2c1bc41 | |||
| 2618105140 | |||
| 6b41f3833a | |||
| 446a5c7661 | |||
| 7f8554fa12 | |||
| 65a61d0336 | |||
| 8dd6f71baf | |||
| e96653751d |
5
.gitignore
vendored
@@ -40,8 +40,13 @@ internal/shared/pdf/fonts/*.ttf
|
||||
internal/shared/pdf/fonts/*.ttc
|
||||
internal/shared/pdf/fonts/*.otf
|
||||
|
||||
# Pure Component 目录(用于持久化存储,不进行版本控制)
|
||||
resources/Pure_Component/
|
||||
|
||||
# 其他
|
||||
*.exe
|
||||
*.exe*
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
cmd/api/__debug_bin*
|
||||
@@ -54,7 +54,8 @@ COPY config.yaml .
|
||||
COPY configs/ ./configs/
|
||||
|
||||
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||
COPY resources ./resources
|
||||
COPY resources/etc ./resources/etc
|
||||
COPY resources/pdf ./resources/pdf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
40
config.yaml
@@ -124,7 +124,7 @@ sms:
|
||||
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
||||
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
|
||||
endpoint_url: "dysmsapi.aliyuncs.com"
|
||||
sign_name: "天远查"
|
||||
sign_name: "海南海宇大数据"
|
||||
template_code: "SMS_302641455"
|
||||
code_length: 6
|
||||
expire_time: 5m
|
||||
@@ -437,7 +437,7 @@ zhicha:
|
||||
# 🌐 木子数据配置
|
||||
# ===========================================
|
||||
muzi:
|
||||
url: "https://carv.m0101.com/magic/carv/pubin/service/academic"
|
||||
url: "https://carv.m0101.com/magic/carv/pubin/service"
|
||||
app_id: "713014138179585"
|
||||
app_secret: "bd4090ac652c404c80e90ebbdcd6ba1d"
|
||||
timeout: 60s
|
||||
@@ -494,3 +494,39 @@ xingwei:
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
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
|
||||
|
||||
@@ -122,38 +122,38 @@ wallet:
|
||||
# 🚦 频率限制配置 - 生产环境
|
||||
# ===========================================
|
||||
daily_ratelimit:
|
||||
max_requests_per_day: 50000 # 生产环境每日最大请求次数
|
||||
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
|
||||
max_concurrent: 200 # 生产环境最大并发请求数
|
||||
|
||||
max_requests_per_day: 50000 # 生产环境每日最大请求次数
|
||||
max_requests_per_ip: 5000 # 生产环境每个IP每日最大请求次数
|
||||
max_concurrent: 200 # 生产环境最大并发请求数
|
||||
|
||||
# 排除频率限制的路径
|
||||
exclude_paths:
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
- "/health" # 健康检查接口
|
||||
- "/metrics" # 监控指标接口
|
||||
|
||||
# 排除频率限制的域名
|
||||
exclude_domains:
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
- "api.*" # API二级域名不受频率限制
|
||||
- "*.api.*" # 支持多级API域名
|
||||
|
||||
# 生产环境安全配置(严格限制)
|
||||
enable_ip_whitelist: false # 生产环境不启用IP白名单
|
||||
enable_ip_blacklist: true # 启用IP黑名单
|
||||
ip_blacklist: # 生产环境IP黑名单
|
||||
- "192.168.1.100" # 示例:被禁止的IP
|
||||
- "10.0.0.50" # 示例:被禁止的IP
|
||||
|
||||
enable_user_agent: true # 启用User-Agent检查
|
||||
blocked_user_agents: # 被阻止的User-Agent
|
||||
- "curl" # 阻止curl请求
|
||||
- "wget" # 阻止wget请求
|
||||
- "python-requests" # 阻止Python requests
|
||||
|
||||
enable_referer: true # 启用Referer检查
|
||||
allowed_referers: # 允许的Referer
|
||||
enable_ip_whitelist: false # 生产环境不启用IP白名单
|
||||
enable_ip_blacklist: true # 启用IP黑名单
|
||||
ip_blacklist: # 生产环境IP黑名单
|
||||
- "192.168.1.100" # 示例:被禁止的IP
|
||||
- "10.0.0.50" # 示例:被禁止的IP
|
||||
|
||||
enable_user_agent: true # 启用User-Agent检查
|
||||
blocked_user_agents: # 被阻止的User-Agent
|
||||
- "curl" # 阻止curl请求
|
||||
- "wget" # 阻止wget请求
|
||||
- "python-requests" # 阻止Python requests
|
||||
|
||||
enable_referer: true # 启用Referer检查
|
||||
allowed_referers: # 允许的Referer
|
||||
- "https://console.tianyuanapi.com"
|
||||
- "https://consoletest.tianyuanapi.com"
|
||||
|
||||
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
|
||||
enable_proxy_check: true # 启用代理检查
|
||||
|
||||
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
|
||||
enable_proxy_check: true # 启用代理检查
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ services:
|
||||
networks:
|
||||
- tyapi-network
|
||||
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
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
@@ -88,6 +89,7 @@ services:
|
||||
- "25000:8080"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./resources/Pure_Component:/app/resources/Pure_Component
|
||||
# user: "1001:1001" # 注释掉,使用root权限运行
|
||||
networks:
|
||||
- tyapi-network
|
||||
@@ -169,7 +171,15 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
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
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
@@ -189,6 +199,8 @@ volumes:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
pure_component:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
tyapi-network:
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure Component/src/ui` 目录下,通过产品编号(product_code)匹配对应的文件夹或文件。
|
||||
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure_Component/src/ui` 目录下,通过产品编号(product_code)匹配对应的文件夹或文件。
|
||||
|
||||
## 二、核心需求
|
||||
|
||||
### 2.1 基本功能
|
||||
|
||||
1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure Component/src/ui` 下的文件夹或文件
|
||||
1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure_Component/src/ui` 下的文件夹或文件
|
||||
- 支持前缀匹配(如产品编号为 `DWBG6A2C`,文件夹可能是 `DWBG6A2C` 或 `多cDWBG6A2C`)
|
||||
- 匹配规则:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
|
||||
|
||||
@@ -537,7 +537,7 @@ func (s *ComponentReportServiceImpl) MatchProductCodeToPath(ctx context.Context,
|
||||
}
|
||||
|
||||
// 2. 扫描目录
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
basePath := "resources/Pure_Component/src/ui"
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -807,7 +807,7 @@ func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, produc
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 3. 遍历子产品,添加UI组件文件到ZIP
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
basePath := "resources/Pure_Component/src/ui"
|
||||
for _, productCode := range subProductCodes {
|
||||
path, fileType, err := s.MatchProductCodeToPath(ctx, productCode)
|
||||
if err != nil {
|
||||
@@ -847,7 +847,7 @@ func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, produc
|
||||
|
||||
// 5. 添加其他必要的文件(如果需要)
|
||||
// 例如:复制 public 目录下的其他文件(如果有)
|
||||
publicBasePath := "resources/Pure Component/public"
|
||||
publicBasePath := "resources/Pure_Component/public"
|
||||
publicFiles, err := os.ReadDir(publicBasePath)
|
||||
if err == nil {
|
||||
for _, file := range publicFiles {
|
||||
|
||||
@@ -231,6 +231,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
&financeEntities.AlipayOrder{},
|
||||
&financeEntities.InvoiceApplication{},
|
||||
&financeEntities.UserInvoiceInfo{},
|
||||
&financeEntities.PurchaseOrder{}, //购买组件订单表
|
||||
|
||||
// 产品域
|
||||
&productEntities.Product{},
|
||||
|
||||
@@ -218,6 +218,12 @@ func (s *ApiApplicationServiceImpl) validateApiCall(ctx context.Context, cmd *co
|
||||
return nil, ErrFrozenAccount
|
||||
}
|
||||
|
||||
// 验证产品是否启用
|
||||
if !product.IsEnabled {
|
||||
s.logger.Error("产品未启用", zap.String("product_code", product.Code))
|
||||
return nil, ErrProductDisabled
|
||||
}
|
||||
|
||||
// 4. 验证IP白名单(非开发环境)
|
||||
if !s.config.App.IsDevelopment() && !cmd.Options.IsDebug {
|
||||
if !apiUser.IsWhiteListed(cmd.ClientIP) {
|
||||
@@ -583,12 +589,50 @@ func (s *ApiApplicationServiceImpl) GetUserApiCalls(ctx context.Context, userID
|
||||
// 转换为响应DTO
|
||||
var items []dto.ApiCallRecordResponse
|
||||
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{
|
||||
ID: call.ID,
|
||||
AccessId: call.AccessId,
|
||||
UserId: *call.UserId,
|
||||
TransactionId: call.TransactionId,
|
||||
ClientIp: call.ClientIp,
|
||||
RequestParams: requestParamsStr,
|
||||
Status: call.Status,
|
||||
StartAt: call.StartAt.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
|
||||
}
|
||||
|
||||
// 解密请求参数
|
||||
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{
|
||||
ID: call.ID,
|
||||
AccessId: call.AccessId,
|
||||
TransactionId: call.TransactionId,
|
||||
ClientIp: call.ClientIp,
|
||||
RequestParams: requestParamsStr,
|
||||
Status: call.Status,
|
||||
}
|
||||
|
||||
@@ -1292,7 +1374,7 @@ func (s *ApiApplicationServiceImpl) UpdateUserBalanceAlertSettings(ctx context.C
|
||||
// TestBalanceAlertSms 测试余额预警短信
|
||||
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 {
|
||||
s.logger.Error("获取用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
|
||||
@@ -49,6 +49,7 @@ type ApiCallRecordResponse struct {
|
||||
ProductName *string `json:"product_name,omitempty"`
|
||||
TransactionId string `json:"transaction_id"`
|
||||
ClientIp string `json:"client_ip"`
|
||||
RequestParams string `json:"request_params"`
|
||||
Status string `json:"status"`
|
||||
StartAt string `json:"start_at"`
|
||||
EndAt *string `json:"end_at,omitempty"`
|
||||
|
||||
@@ -109,13 +109,16 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
|
||||
)
|
||||
|
||||
// 验证验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
|
||||
record.MarkAsFailed(err.Error())
|
||||
saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record)
|
||||
if saveErr != nil {
|
||||
return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
|
||||
// 特殊验证码"768005"直接跳过验证环节
|
||||
if cmd.VerificationCode != "768005" {
|
||||
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
|
||||
record.MarkAsFailed(err.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("开始处理企业信息提交",
|
||||
zap.String("user_id", cmd.UserID))
|
||||
|
||||
@@ -125,3 +125,45 @@ type UserSimpleResponse struct {
|
||||
CompanyName string `json:"company_name"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ type FinanceApplicationService interface {
|
||||
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)
|
||||
|
||||
// 购买记录
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"tyapi-server/internal/application/finance/dto/commands"
|
||||
"tyapi-server/internal/application/finance/dto/queries"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_repositories "tyapi-server/internal/domains/product/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/component_report"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/export"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
@@ -36,6 +36,7 @@ type FinanceApplicationServiceImpl struct {
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository
|
||||
purchaseOrderRepo finance_repositories.PurchaseOrderRepository
|
||||
componentReportRepo product_repositories.ComponentReportRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
txManager *database.TransactionManager
|
||||
@@ -54,6 +55,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo finance_repositories.AlipayOrderRepository,
|
||||
wechatOrderRepo finance_repositories.WechatOrderRepository,
|
||||
rechargeRecordRepo finance_repositories.RechargeRecordRepository,
|
||||
purchaseOrderRepo finance_repositories.PurchaseOrderRepository,
|
||||
componentReportRepo product_repositories.ComponentReportRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
txManager *database.TransactionManager,
|
||||
@@ -70,6 +72,7 @@ func NewFinanceApplicationService(
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
componentReportRepo: componentReportRepo,
|
||||
userRepo: userRepo,
|
||||
txManager: txManager,
|
||||
@@ -854,13 +857,7 @@ func (s *FinanceApplicationServiceImpl) HandleAlipayCallback(ctx context.Context
|
||||
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)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝支付成功失败",
|
||||
@@ -886,20 +883,52 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
|
||||
return err
|
||||
}
|
||||
|
||||
// 直接调用充值记录服务处理支付成功逻辑
|
||||
// 该服务内部会处理所有必要的检查、事务和更新操作
|
||||
// 如果是组件报告下载订单,服务会自动跳过钱包余额增加
|
||||
err = s.rechargeRecordService.HandleAlipayPaymentSuccess(ctx, outTradeNo, amount, tradeNo)
|
||||
// 查找支付宝订单
|
||||
alipayOrder, err := s.alipayOrderRepo.GetByOutTradeNo(ctx, outTradeNo)
|
||||
if err != nil {
|
||||
s.logger.Error("处理支付宝支付成功失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
s.logger.Error("查找支付宝订单失败", zap.String("out_trade_no", outTradeNo), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查并更新组件报告下载记录状态(如果存在)
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
if alipayOrder == nil {
|
||||
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("支付宝支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
@@ -1477,30 +1506,7 @@ func (s *FinanceApplicationServiceImpl) HandleWechatPayCallback(ctx context.Cont
|
||||
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)
|
||||
if err != nil {
|
||||
s.logger.Error("处理微信支付成功失败",
|
||||
@@ -1535,26 +1541,34 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return fmt.Errorf("微信订单不存在")
|
||||
}
|
||||
|
||||
// 查找对应的充值记录
|
||||
// 判断是否为充值订单还是购买订单
|
||||
rechargeRecord, err := s.rechargeRecordService.GetByID(ctx, wechatOrder.RechargeID)
|
||||
if err != nil {
|
||||
s.logger.Error("查找充值记录失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("recharge_id", wechatOrder.RechargeID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("查找充值记录失败: %w", err)
|
||||
if err == nil {
|
||||
// 这是充值订单,继续原有的处理逻辑
|
||||
} else {
|
||||
// 尝试查找购买订单
|
||||
_, err = s.purchaseOrderRepo.GetByID(ctx, wechatOrder.RechargeID)
|
||||
if err == nil {
|
||||
// 这是购买订单(可能是示例报告购买订单),调用处理购买订单支付成功逻辑
|
||||
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 {
|
||||
s.logger.Info("微信支付订单已处理成功,跳过重复处理",
|
||||
@@ -1562,12 +1576,7 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("order_id", wechatOrder.ID),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
zap.Bool("is_component_report", isComponentReportOrder),
|
||||
)
|
||||
// 如果是组件报告下载订单,确保更新下载记录状态
|
||||
if isComponentReportOrder {
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1638,33 +1647,17 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否是组件报告下载订单(通过备注判断)
|
||||
isComponentReportOrder := strings.Contains(rechargeRecord.Notes, "购买") && strings.Contains(rechargeRecord.Notes, "报告示例")
|
||||
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("步骤5: 检测到组件报告下载订单,不增加钱包余额",
|
||||
// 充值到钱包(包含赠送金额)
|
||||
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("recharge_id", rechargeRecord.ID),
|
||||
zap.String("notes", rechargeRecord.Notes),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("total_amount", totalRechargeAmount.String()),
|
||||
zap.Error(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 err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1680,105 +1673,129 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是组件报告下载订单,更新下载记录状态
|
||||
if isComponentReportOrder {
|
||||
s.logger.Info("步骤6: 更新组件报告下载记录状态",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
s.updateComponentReportDownloadStatus(ctx, outTradeNo)
|
||||
}
|
||||
|
||||
s.logger.Info("微信支付成功处理完成",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("transaction_id", transactionID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.Bool("is_component_report", isComponentReportOrder),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateComponentReportDownloadStatus 更新组件报告下载记录状态
|
||||
func (s *FinanceApplicationServiceImpl) updateComponentReportDownloadStatus(ctx context.Context, outTradeNo string) {
|
||||
s.logger.Info("========== 开始更新组件报告下载记录状态 ==========",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
|
||||
if s.componentReportRepo == nil {
|
||||
s.logger.Warn("组件报告下载Repository未初始化,跳过更新")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据支付订单号查找组件报告下载记录
|
||||
download, err := s.componentReportRepo.GetDownloadByPaymentOrderID(ctx, outTradeNo)
|
||||
// processPurchaseOrderPaymentSuccess 处理购买订单支付成功的逻辑
|
||||
func (s *FinanceApplicationServiceImpl) processPurchaseOrderPaymentSuccess(ctx context.Context, purchaseOrderID, tradeNo string, amount decimal.Decimal, buyerID, sellerID string) error {
|
||||
// 查找购买订单
|
||||
purchaseOrder, err := s.purchaseOrderRepo.GetByID(ctx, purchaseOrderID)
|
||||
if err != nil {
|
||||
s.logger.Info("未找到组件报告下载记录,可能不是组件报告下载订单",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
s.logger.Error("查找购买订单失败",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
return fmt.Errorf("查找购买订单失败: %w", err)
|
||||
}
|
||||
|
||||
if download == nil {
|
||||
s.logger.Info("组件报告下载记录为空,跳过更新",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
if purchaseOrder == nil {
|
||||
s.logger.Error("购买订单不存在",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
)
|
||||
return
|
||||
return fmt.Errorf("购买订单不存在")
|
||||
}
|
||||
|
||||
s.logger.Info("步骤1: 找到组件报告下载记录",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("product_id", download.ProductID),
|
||||
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),
|
||||
// 检查订单状态,如果已支付则跳过
|
||||
if purchaseOrder.Status == finance_entities.PurchaseOrderStatusPaid {
|
||||
s.logger.Info("购买订单已支付,跳过处理",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("步骤2: 更新支付状态为成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
)
|
||||
|
||||
// 更新支付状态为成功
|
||||
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)
|
||||
// 更新购买订单状态
|
||||
purchaseOrder.MarkPaid(tradeNo, buyerID, sellerID, amount, amount)
|
||||
err = s.purchaseOrderRepo.Update(ctx, purchaseOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("更新组件报告下载记录状态失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
s.logger.Error("更新购买订单状态失败",
|
||||
zap.String("purchase_order_id", purchaseOrderID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
return fmt.Errorf("更新购买订单状态失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("========== 组件报告下载记录状态更新成功 ==========",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("download_id", download.ID),
|
||||
zap.String("product_id", download.ProductID),
|
||||
zap.String("payment_status", download.PaymentStatus),
|
||||
// 更新对应的支付订单状态(微信或支付宝)
|
||||
if purchaseOrder.PayChannel == "alipay" {
|
||||
alipayOrder, err := s.alipayOrderRepo.GetByRechargeID(ctx, purchaseOrderID)
|
||||
if err == nil && alipayOrder != nil {
|
||||
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 处理微信退款回调
|
||||
@@ -1842,3 +1859,163 @@ func (s *FinanceApplicationServiceImpl) HandleWechatRefundCallback(ctx context.C
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
1358
internal/application/product/component_report_order_service.go
Normal file
@@ -14,6 +14,10 @@ type CreateProductCommand struct {
|
||||
IsVisible bool `json:"is_visible" 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信息
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" 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:"是否展示"`
|
||||
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信息
|
||||
SEOTitle string `json:"seo_title" binding:"omitempty,max=100" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" binding:"omitempty,max=200" comment:"SEO描述"`
|
||||
|
||||
@@ -8,15 +8,16 @@ type CreateSubscriptionCommand struct {
|
||||
|
||||
// UpdateSubscriptionPriceCommand 更新订阅价格命令
|
||||
type UpdateSubscriptionPriceCommand struct {
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
||||
ID string `json:"-" uri:"id" binding:"required,uuid" comment:"订阅ID"`
|
||||
Price float64 `json:"price" binding:"price,min=0" comment:"订阅价格"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" binding:"omitempty,min=0" comment:"UI组件价格(组合包使用)"`
|
||||
}
|
||||
|
||||
// BatchUpdateSubscriptionPricesCommand 批量更新订阅价格命令
|
||||
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:按成本价倍数)"`
|
||||
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:"成本价倍数"`
|
||||
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
|
||||
}
|
||||
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:"成本价倍数"`
|
||||
Scope string `json:"scope" binding:"required,oneof=undiscounted all" comment:"改价范围(undiscounted:仅未打折,all:所有)"`
|
||||
}
|
||||
|
||||
@@ -15,17 +15,20 @@ type PackageItemResponse struct {
|
||||
|
||||
// ProductInfoResponse 产品详情响应
|
||||
type ProductInfoResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Content string `json:"content" comment:"产品内容"`
|
||||
CategoryID string `json:"category_id" comment:"产品分类ID"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsEnabled bool `json:"is_enabled" comment:"是否启用"`
|
||||
IsPackage bool `json:"is_package" 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信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
@@ -60,21 +63,22 @@ type ProductSearchResponse struct {
|
||||
|
||||
// ProductSimpleResponse 产品简单信息响应
|
||||
type ProductSimpleResponse struct {
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
ID string `json:"id" comment:"产品ID"`
|
||||
OldID *string `json:"old_id,omitempty" comment:"旧产品ID"`
|
||||
Name string `json:"name" comment:"产品名称"`
|
||||
Code string `json:"code" comment:"产品编号"`
|
||||
Description string `json:"description" comment:"产品简介"`
|
||||
Category *CategorySimpleResponse `json:"category,omitempty" comment:"分类信息"`
|
||||
Price float64 `json:"price" comment:"产品价格"`
|
||||
IsPackage bool `json:"is_package" comment:"是否组合包"`
|
||||
IsSubscribed *bool `json:"is_subscribed,omitempty" comment:"当前用户是否已订阅"`
|
||||
}
|
||||
|
||||
// ProductSimpleAdminResponse 管理员产品简单信息响应(包含成本价)
|
||||
type ProductSimpleAdminResponse struct {
|
||||
ProductSimpleResponse
|
||||
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||
CostPrice float64 `json:"cost_price" comment:"成本价"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格(组合包使用)"`
|
||||
}
|
||||
|
||||
// ProductStatsResponse 产品统计响应
|
||||
@@ -101,6 +105,10 @@ type ProductAdminInfoResponse struct {
|
||||
IsVisible bool `json:"is_visible" 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信息
|
||||
SEOTitle string `json:"seo_title" comment:"SEO标题"`
|
||||
SEODescription string `json:"seo_description" comment:"SEO描述"`
|
||||
|
||||
@@ -13,47 +13,48 @@ type UserSimpleResponse struct {
|
||||
|
||||
// SubscriptionInfoResponse 订阅详情响应
|
||||
type SubscriptionInfoResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
UserID string `json:"user_id" comment:"用户ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
UserID string `json:"user_id" comment:"用户ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
UIComponentPrice float64 `json:"ui_component_price" comment:"UI组件价格(组合包使用)"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
|
||||
// 关联信息
|
||||
User *UserSimpleResponse `json:"user,omitempty" comment:"用户信息"`
|
||||
Product *ProductSimpleResponse `json:"product,omitempty" comment:"产品信息"`
|
||||
// 管理员端使用,包含成本价的产品信息
|
||||
ProductAdmin *ProductSimpleAdminResponse `json:"product_admin,omitempty" comment:"产品信息(管理员端,包含成本价)"`
|
||||
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// SubscriptionListResponse 订阅列表响应
|
||||
type SubscriptionListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
Page int `json:"page" comment:"页码"`
|
||||
Size int `json:"size" comment:"每页数量"`
|
||||
Items []SubscriptionInfoResponse `json:"items" comment:"订阅列表"`
|
||||
}
|
||||
|
||||
// SubscriptionSimpleResponse 订阅简单信息响应
|
||||
type SubscriptionSimpleResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
Price float64 `json:"price" comment:"订阅价格"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
}
|
||||
|
||||
// SubscriptionUsageResponse 订阅使用情况响应
|
||||
type SubscriptionUsageResponse struct {
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
ID string `json:"id" comment:"订阅ID"`
|
||||
ProductID string `json:"product_id" comment:"产品ID"`
|
||||
APIUsed int64 `json:"api_used" comment:"已使用API调用次数"`
|
||||
}
|
||||
|
||||
// SubscriptionStatsResponse 订阅统计响应
|
||||
type SubscriptionStatsResponse struct {
|
||||
TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"`
|
||||
TotalRevenue float64 `json:"total_revenue" comment:"总收入"`
|
||||
}
|
||||
TotalSubscriptions int64 `json:"total_subscriptions" comment:"订阅总数"`
|
||||
TotalRevenue float64 `json:"total_revenue" comment:"总收入"`
|
||||
}
|
||||
|
||||
@@ -54,20 +54,22 @@ func NewProductApplicationService(
|
||||
func (s *ProductApplicationServiceImpl) CreateProduct(ctx context.Context, cmd *commands.CreateProductCommand) (*responses.ProductAdminInfoResponse, error) {
|
||||
// 1. 构建产品实体
|
||||
product := &entities.Product{
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Content: cmd.Content,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Price: decimal.NewFromFloat(cmd.Price),
|
||||
CostPrice: decimal.NewFromFloat(cmd.CostPrice),
|
||||
Remark: cmd.Remark,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
IsPackage: cmd.IsPackage,
|
||||
SEOTitle: cmd.SEOTitle,
|
||||
SEODescription: cmd.SEODescription,
|
||||
SEOKeywords: cmd.SEOKeywords,
|
||||
Name: cmd.Name,
|
||||
Code: cmd.Code,
|
||||
Description: cmd.Description,
|
||||
Content: cmd.Content,
|
||||
CategoryID: cmd.CategoryID,
|
||||
Price: decimal.NewFromFloat(cmd.Price),
|
||||
CostPrice: decimal.NewFromFloat(cmd.CostPrice),
|
||||
Remark: cmd.Remark,
|
||||
IsEnabled: cmd.IsEnabled,
|
||||
IsVisible: cmd.IsVisible,
|
||||
IsPackage: cmd.IsPackage,
|
||||
SellUIComponent: cmd.SellUIComponent,
|
||||
UIComponentPrice: decimal.NewFromFloat(cmd.UIComponentPrice),
|
||||
SEOTitle: cmd.SEOTitle,
|
||||
SEODescription: cmd.SEODescription,
|
||||
SEOKeywords: cmd.SEOKeywords,
|
||||
}
|
||||
|
||||
// 2. 创建产品
|
||||
@@ -101,6 +103,8 @@ func (s *ProductApplicationServiceImpl) UpdateProduct(ctx context.Context, cmd *
|
||||
existingProduct.IsEnabled = cmd.IsEnabled
|
||||
existingProduct.IsVisible = cmd.IsVisible
|
||||
existingProduct.IsPackage = cmd.IsPackage
|
||||
existingProduct.SellUIComponent = cmd.SellUIComponent
|
||||
existingProduct.UIComponentPrice = decimal.NewFromFloat(cmd.UIComponentPrice)
|
||||
existingProduct.SEOTitle = cmd.SEOTitle
|
||||
existingProduct.SEODescription = cmd.SEODescription
|
||||
existingProduct.SEOKeywords = cmd.SEOKeywords
|
||||
@@ -486,21 +490,23 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
|
||||
// convertToProductInfoResponse 转换为产品信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse {
|
||||
response := &responses.ProductInfoResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsPackage: product.IsPackage,
|
||||
SellUIComponent: product.SellUIComponent,
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加分类信息
|
||||
@@ -530,24 +536,26 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en
|
||||
// convertToProductAdminInfoResponse 转换为管理员产品信息响应
|
||||
func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse {
|
||||
response := &responses.ProductAdminInfoResponse{
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
Remark: product.Remark,
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible, // 管理员可以看到可见状态
|
||||
IsPackage: product.IsPackage,
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
ID: product.ID,
|
||||
OldID: product.OldID,
|
||||
Name: product.Name,
|
||||
Code: product.Code,
|
||||
Description: product.Description,
|
||||
Content: product.Content,
|
||||
CategoryID: product.CategoryID,
|
||||
Price: product.Price.InexactFloat64(),
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
Remark: product.Remark,
|
||||
IsEnabled: product.IsEnabled,
|
||||
IsVisible: product.IsVisible, // 管理员可以看到可见状态
|
||||
IsPackage: product.IsPackage,
|
||||
SellUIComponent: product.SellUIComponent,
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
SEOTitle: product.SEOTitle,
|
||||
SEODescription: product.SEODescription,
|
||||
SEOKeywords: product.SEOKeywords,
|
||||
CreatedAt: product.CreatedAt,
|
||||
UpdatedAt: product.UpdatedAt,
|
||||
}
|
||||
|
||||
// 添加分类信息
|
||||
|
||||
@@ -44,7 +44,7 @@ func NewSubscriptionApplicationService(
|
||||
// UpdateSubscriptionPrice 更新订阅价格
|
||||
// 业务流程:1. 获取订阅 2. 更新价格 3. 保存订阅
|
||||
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 一键改价
|
||||
@@ -377,16 +377,23 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponse(s
|
||||
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{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
User: userInfo,
|
||||
Product: productResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
UIComponentPrice: uiComponentPrice,
|
||||
User: userInfo,
|
||||
Product: productResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,16 +440,23 @@ func (s *SubscriptionApplicationServiceImpl) convertToSubscriptionInfoResponseFo
|
||||
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{
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
User: userInfo,
|
||||
ProductAdmin: productAdminResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
ID: subscription.ID,
|
||||
UserID: subscription.UserID,
|
||||
ProductID: subscription.ProductID,
|
||||
Price: subscription.Price.InexactFloat64(),
|
||||
UIComponentPrice: uiComponentPrice,
|
||||
User: userInfo,
|
||||
ProductAdmin: productAdminResponse,
|
||||
APIUsed: subscription.APIUsed,
|
||||
CreatedAt: subscription.CreatedAt,
|
||||
UpdatedAt: subscription.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +478,8 @@ func (s *SubscriptionApplicationServiceImpl) convertToProductSimpleAdminResponse
|
||||
Category: categoryResponse,
|
||||
IsPackage: product.IsPackage,
|
||||
},
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
CostPrice: product.CostPrice.InexactFloat64(),
|
||||
UIComponentPrice: product.UIComponentPrice.InexactFloat64(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UIComponentApplicationService UI组件应用服务接口
|
||||
@@ -93,6 +95,7 @@ type UIComponentApplicationServiceImpl struct {
|
||||
productUIComponentRepo repositories.ProductUIComponentRepository
|
||||
fileStorageService FileStorageService
|
||||
fileService UIComponentFileService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// FileStorageService 文件存储服务接口
|
||||
@@ -108,12 +111,14 @@ func NewUIComponentApplicationService(
|
||||
productUIComponentRepo repositories.ProductUIComponentRepository,
|
||||
fileStorageService FileStorageService,
|
||||
fileService UIComponentFileService,
|
||||
logger *zap.Logger,
|
||||
) UIComponentApplicationService {
|
||||
return &UIComponentApplicationServiceImpl{
|
||||
uiComponentRepo: uiComponentRepo,
|
||||
productUIComponentRepo: productUIComponentRepo,
|
||||
fileStorageService: fileStorageService,
|
||||
fileService: fileService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,10 +187,14 @@ func (s *UIComponentApplicationServiceImpl) CreateUIComponentWithFile(ctx contex
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure Component/src/ui"
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
createdComponent.FolderPath = &folderPath
|
||||
createdComponent.FileType = &fileType
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 仅对ZIP文件设置已解压标记
|
||||
if fileType == ".zip" {
|
||||
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
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
hasZipFile := false
|
||||
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
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
createdComponent.FileUploadTime = &now
|
||||
|
||||
// 检查是否有ZIP文件
|
||||
hasZipFile := false
|
||||
for _, fileHeader := range files {
|
||||
@@ -442,20 +459,56 @@ func (s *UIComponentApplicationServiceImpl) UpdateUIComponent(ctx context.Contex
|
||||
|
||||
// DeleteUIComponent 删除UI组件
|
||||
func (s *UIComponentApplicationServiceImpl) DeleteUIComponent(ctx context.Context, id string) error {
|
||||
// 获取组件信息
|
||||
component, err := s.uiComponentRepo.GetByID(ctx, id)
|
||||
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 {
|
||||
s.logger.Warn("UI组件不存在", zap.String("id", id))
|
||||
return ErrComponentNotFound
|
||||
}
|
||||
|
||||
// 删除关联的文件
|
||||
if component.FilePath != nil {
|
||||
_ = s.fileStorageService.DeleteFile(ctx, *component.FilePath)
|
||||
// 记录组件信息
|
||||
s.logger.Info("开始删除UI组件",
|
||||
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组件列表
|
||||
@@ -634,10 +687,14 @@ func (s *UIComponentApplicationServiceImpl) UploadAndExtractUIComponentFile(ctx
|
||||
fileType := strings.ToLower(filepath.Ext(file.Filename))
|
||||
|
||||
// 更新组件信息
|
||||
folderPath := "resources/Pure Component/src/ui"
|
||||
folderPath := "resources/Pure_Component/src/ui"
|
||||
component.FolderPath = &folderPath
|
||||
component.FileType = &fileType
|
||||
|
||||
// 记录文件上传时间
|
||||
now := time.Now()
|
||||
component.FileUploadTime = &now
|
||||
|
||||
// 仅对ZIP文件设置已解压标记
|
||||
if fileType == ".zip" {
|
||||
component.IsExtracted = true
|
||||
|
||||
@@ -32,6 +32,9 @@ type UIComponentFileService interface {
|
||||
|
||||
// 获取文件夹内容
|
||||
GetFolderContent(folderPath string) ([]FileInfo, error)
|
||||
|
||||
// 根据组件编码和上传时间智能删除组件相关文件
|
||||
DeleteFilesByComponentCode(componentCode string, uploadTime *time.Time) error
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
@@ -222,11 +225,34 @@ func (s *UIComponentFileServiceImpl) CreateFolderByCode(componentCode string) (s
|
||||
|
||||
// DeleteFolder 删除组件文件夹
|
||||
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) {
|
||||
s.logger.Info("文件夹不存在", zap.String("folderPath", folderPath))
|
||||
return nil // 文件夹不存在,不视为错误
|
||||
}
|
||||
|
||||
// 尝试删除文件夹
|
||||
s.logger.Info("开始删除文件夹", zap.String("folderPath", folderPath))
|
||||
if err := os.RemoveAll(folderPath); err != nil {
|
||||
s.logger.Error("删除文件夹失败",
|
||||
zap.Error(err),
|
||||
zap.String("folderPath", folderPath))
|
||||
return fmt.Errorf("删除文件夹失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -339,3 +365,95 @@ func (s *UIComponentFileServiceImpl) extractZipFile(zipPath, destPath string) er
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2772,18 +2772,19 @@ func (s *StatisticsApplicationServiceImpl) AdminGetTodayCertifiedEnterprises(ctx
|
||||
var enterprises []map[string]interface{}
|
||||
for _, cert := range completedCertifications {
|
||||
// 获取企业信息
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, cert.UserID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取企业信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取用户基本信息(仅需要用户名)
|
||||
user, err := s.userRepo.GetByID(ctx, cert.UserID)
|
||||
// 使用预加载方法一次性获取用户和企业信息
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, cert.UserID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取用户信息失败", zap.String("user_id", cert.UserID), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取企业信息
|
||||
enterpriseInfo := user.EnterpriseInfo
|
||||
if enterpriseInfo == nil {
|
||||
s.logger.Warn("用户没有企业信息", zap.String("user_id", cert.UserID))
|
||||
continue
|
||||
}
|
||||
|
||||
enterprise := map[string]interface{}{
|
||||
"id": cert.ID,
|
||||
|
||||
@@ -38,6 +38,7 @@ type Config struct {
|
||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||
Alicloud AlicloudConfig `mapstructure:"alicloud"`
|
||||
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
||||
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
||||
}
|
||||
|
||||
// ServerConfig HTTP服务器配置
|
||||
@@ -520,6 +521,35 @@ type XingweiLevelFileConfig struct {
|
||||
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 域名配置
|
||||
type DomainConfig struct {
|
||||
API string `mapstructure:"api"` // API域名
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/ocr"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
@@ -366,6 +367,10 @@ func NewContainer() *Container {
|
||||
func(cfg *config.Config) (*xingwei.XingweiService, error) {
|
||||
return xingwei.NewXingweiServiceWithConfig(cfg)
|
||||
},
|
||||
// JiguangService - 极光服务
|
||||
func(cfg *config.Config) (*jiguang.JiguangService, error) {
|
||||
return jiguang.NewJiguangServiceWithConfig(cfg)
|
||||
},
|
||||
func(cfg *config.Config) *yushan.YushanService {
|
||||
return yushan.NewYushanService(
|
||||
cfg.Yushan.URL,
|
||||
@@ -571,6 +576,11 @@ func NewContainer() *Container {
|
||||
product_repo.NewGormComponentReportRepository,
|
||||
fx.As(new(domain_product_repo.ComponentReportRepository)),
|
||||
),
|
||||
// 购买订单仓储
|
||||
fx.Annotate(
|
||||
finance_repo.NewGormPurchaseOrderRepository,
|
||||
fx.As(new(domain_finance_repo.PurchaseOrderRepository)),
|
||||
),
|
||||
// UI组件仓储 - 同时注册具体类型和接口类型
|
||||
fx.Annotate(
|
||||
product_repo.NewGormUIComponentRepository,
|
||||
@@ -893,6 +903,7 @@ func NewContainer() *Container {
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
|
||||
userRepo domain_user_repo.UserRepository,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
@@ -909,6 +920,7 @@ func NewContainer() *Container {
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
purchaseOrderRepo,
|
||||
componentReportRepo,
|
||||
userRepo,
|
||||
txManager,
|
||||
@@ -950,6 +962,36 @@ func NewContainer() *Container {
|
||||
},
|
||||
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配置应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
product.NewProductApiConfigApplicationService,
|
||||
@@ -1055,7 +1097,7 @@ func NewContainer() *Container {
|
||||
logger *zap.Logger,
|
||||
) product.UIComponentApplicationService {
|
||||
// 创建UI组件文件服务
|
||||
basePath := "resources/Pure Component/src/ui"
|
||||
basePath := "resources/Pure_Component/src/ui"
|
||||
fileService := product.NewUIComponentFileService(basePath, logger)
|
||||
|
||||
return product.NewUIComponentApplicationService(
|
||||
@@ -1063,6 +1105,7 @@ func NewContainer() *Container {
|
||||
productUIComponentRepo,
|
||||
fileStorageService,
|
||||
fileService,
|
||||
logger,
|
||||
)
|
||||
},
|
||||
fx.As(new(product.UIComponentApplicationService)),
|
||||
@@ -1183,6 +1226,7 @@ func NewContainer() *Container {
|
||||
docRepo domain_product_repo.ProductDocumentationRepository,
|
||||
apiConfigRepo domain_product_repo.ProductApiConfigRepository,
|
||||
componentReportRepo domain_product_repo.ComponentReportRepository,
|
||||
purchaseOrderRepo domain_finance_repo.PurchaseOrderRepository,
|
||||
rechargeRecordRepo domain_finance_repo.RechargeRecordRepository,
|
||||
alipayOrderRepo domain_finance_repo.AlipayOrderRepository,
|
||||
wechatOrderRepo domain_finance_repo.WechatOrderRepository,
|
||||
@@ -1190,7 +1234,14 @@ func NewContainer() *Container {
|
||||
wechatPayService *payment.WechatPayService,
|
||||
logger *zap.Logger,
|
||||
) *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处理器
|
||||
func(
|
||||
@@ -1215,6 +1266,8 @@ func NewContainer() *Container {
|
||||
routes.NewProductRoutes,
|
||||
// 产品管理员路由
|
||||
routes.NewProductAdminRoutes,
|
||||
// 组件报告订单路由
|
||||
routes.NewComponentReportOrderRoutes,
|
||||
// UI组件路由
|
||||
routes.NewUIComponentRoutes,
|
||||
// 文章路由
|
||||
@@ -1331,6 +1384,7 @@ func RegisterRoutes(
|
||||
financeRoutes *routes.FinanceRoutes,
|
||||
productRoutes *routes.ProductRoutes,
|
||||
productAdminRoutes *routes.ProductAdminRoutes,
|
||||
componentReportOrderRoutes *routes.ComponentReportOrderRoutes,
|
||||
uiComponentRoutes *routes.UIComponentRoutes,
|
||||
articleRoutes *routes.ArticleRoutes,
|
||||
announcementRoutes *routes.AnnouncementRoutes,
|
||||
@@ -1352,12 +1406,8 @@ func RegisterRoutes(
|
||||
financeRoutes.Register(router)
|
||||
productRoutes.Register(router)
|
||||
productAdminRoutes.Register(router)
|
||||
|
||||
// UI组件路由需要特殊处理,因为它需要管理员中间件
|
||||
engine := router.GetEngine()
|
||||
adminGroup := engine.Group("/api/v1/admin")
|
||||
adminGroup.Use(adminAuth.Handle())
|
||||
uiComponentRoutes.RegisterRoutes(adminGroup, adminAuth)
|
||||
componentReportOrderRoutes.Register(router)
|
||||
uiComponentRoutes.Register(router)
|
||||
|
||||
articleRoutes.Register(router)
|
||||
announcementRoutes.Register(router)
|
||||
|
||||
@@ -170,6 +170,24 @@ type YYSYD50FReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
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 {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
StartDate string `json:"start_date" validate:"required,validDate" encrypt:"false"`
|
||||
@@ -250,6 +268,10 @@ type QCXG7A2BReq struct {
|
||||
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 COMENT01Req struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
@@ -701,6 +723,11 @@ type IVYZ6M8PReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type IVYZ9H2MReq struct {
|
||||
IDNo string `json:"id_no" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type YYSY9E4AReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"tyapi-server/internal/domains/api/services/processors/yysy"
|
||||
"tyapi-server/internal/domains/product/services"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
@@ -54,6 +55,7 @@ func NewApiRequestService(
|
||||
alicloudService *alicloud.AlicloudService,
|
||||
zhichaService *zhicha.ZhichaService,
|
||||
xingweiService *xingwei.XingweiService,
|
||||
jiguangService *jiguang.JiguangService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
) *ApiRequestService {
|
||||
@@ -61,7 +63,7 @@ func NewApiRequestService(
|
||||
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)
|
||||
@@ -206,6 +208,10 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZ2B2T": ivyz.ProcessIVYZ2B2TRequest, //能力资质核验(学历)
|
||||
"IVYZ5A9O": ivyz.ProcessIVYZ5A9ORequest, //全国⾃然⼈⻛险评估评分模型
|
||||
"IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书
|
||||
"IVYZ9H2M": ivyz.ProcessIVYZ9H2MRequest, //极光个人婚姻查询(V2版)
|
||||
"IVYZZQT3": ivyz.ProcessIVYZZQT3Request, //人脸比对V3
|
||||
"IVYZBPQ2": ivyz.ProcessIVYZBPQ2Request, //人脸比对V2
|
||||
"IVYZSFEL": ivyz.ProcessIVYZSFELRequest, //全国自然人人像三要素核验_V1
|
||||
|
||||
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||
@@ -217,6 +223,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QCXG9P1C": qcxg.ProcessQCXG9P1CRequest,
|
||||
"QCXG8A3D": qcxg.ProcessQCXG8A3DRequest,
|
||||
"QCXG6B4E": qcxg.ProcessQCXG6B4ERequest,
|
||||
"QCXG4896": qcxg.ProcessQCXG4896Request,
|
||||
|
||||
// DWBG系列处理器 - 多维报告
|
||||
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||
|
||||
@@ -198,7 +198,12 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"IVYZ2B2T": &dto.IVYZ2B2TReq{}, //能力资质核验(学历)
|
||||
"IVYZ5A9O": &dto.IVYZ5A9OReq{}, //全国⾃然⼈⻛险评估评分模型
|
||||
"IVYZ6M8P": &dto.IVYZ6M8PReq{}, //职业资格证书
|
||||
"IVYZ9H2M": &dto.IVYZ9H2MReq{}, //极光个人婚姻查询(V2版)
|
||||
"QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证
|
||||
"QCXG4896": &dto.QCXG4896Req{}, //网约车风险查询
|
||||
"IVYZZQT3": &dto.IVYZZQT3Req{}, //人脸比对V3
|
||||
"IVYZBPQ2": &dto.IVYZBPQ2Req{}, //人脸比对V2
|
||||
"IVYZSFEL": &dto.IVYZSFELReq{}, //全国自然人人像三要素核验_V1
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
@@ -32,6 +33,7 @@ type ProcessorDependencies struct {
|
||||
AlicloudService *alicloud.AlicloudService
|
||||
ZhichaService *zhicha.ZhichaService
|
||||
XingweiService *xingwei.XingweiService
|
||||
JiguangService *jiguang.JiguangService
|
||||
Validator interfaces.RequestValidator
|
||||
CombService CombServiceInterface // Changed to interface to break import cycle
|
||||
Options *commands.ApiCallOptions // 添加Options支持
|
||||
@@ -47,6 +49,7 @@ func NewProcessorDependencies(
|
||||
alicloudService *alicloud.AlicloudService,
|
||||
zhichaService *zhicha.ZhichaService,
|
||||
xingweiService *xingwei.XingweiService,
|
||||
jiguangService *jiguang.JiguangService,
|
||||
validator interfaces.RequestValidator,
|
||||
combService CombServiceInterface, // Changed to interface
|
||||
) *ProcessorDependencies {
|
||||
@@ -58,6 +61,7 @@ func NewProcessorDependencies(
|
||||
AlicloudService: alicloudService,
|
||||
ZhichaService: zhichaService,
|
||||
XingweiService: xingweiService,
|
||||
JiguangService: jiguangService,
|
||||
Validator: validator,
|
||||
CombService: combService,
|
||||
Options: nil, // 初始化为nil,在调用时设置
|
||||
|
||||
@@ -24,7 +24,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
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("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
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("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
|
||||
if err != nil {
|
||||
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("查询为空"))
|
||||
}
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
|
||||
@@ -36,6 +36,11 @@ func ProcessIVYZ3P9MRequest(ctx context.Context, params []byte, deps *processors
|
||||
if returnType == "" {
|
||||
returnType = "1"
|
||||
}
|
||||
paramSign := map[string]interface{}{
|
||||
"returnType": returnType,
|
||||
"realName": encryptedName,
|
||||
"certCode": encryptedCertCode,
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"realName": encryptedName,
|
||||
@@ -43,7 +48,8 @@ func ProcessIVYZ3P9MRequest(ctx context.Context, params []byte, deps *processors
|
||||
"returnType": returnType,
|
||||
}
|
||||
|
||||
respData, err := deps.MuziService.CallAPI(ctx, "PC0041", reqData)
|
||||
|
||||
respData, err := deps.MuziService.CallAPI(ctx, "PC0041", "/academic",reqData,paramSign)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, muzi.ErrDatasource):
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
// 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, ¶msDto); 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{}{
|
||||
"idNo": paramsDto.IDNo,
|
||||
"name": paramsDto.Name,
|
||||
}
|
||||
|
||||
// 调用极光API,apiCode为 marriage-single-v2
|
||||
respBytes, err := deps.JiguangService.CallAPI(ctx, "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
|
||||
}
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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, ¶msDto); 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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -24,6 +24,26 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
|
||||
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调用参数
|
||||
apiParams := map[string]string{
|
||||
"code": paramsDto.EntCode,
|
||||
@@ -39,45 +59,41 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 检查天眼查API调用是否成功
|
||||
if !response.Success {
|
||||
// 天眼查API调用失败,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
return nil, fmt.Errorf("天眼查API调用失败")
|
||||
}
|
||||
|
||||
// 解析天眼查响应数据
|
||||
if response.Data == nil {
|
||||
// 天眼查响应数据为空,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
return nil, fmt.Errorf("天眼查响应数据为空")
|
||||
}
|
||||
|
||||
// 将response.Data转换为JSON字符串,然后使用gjson解析
|
||||
dataBytes, err := json.Marshal(response.Data)
|
||||
if err != nil {
|
||||
// 数据序列化失败,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
return nil, fmt.Errorf("数据序列化失败")
|
||||
}
|
||||
|
||||
// 使用gjson解析嵌套的data.result.data字段
|
||||
result := gjson.GetBytes(dataBytes, "result")
|
||||
if !result.Exists() {
|
||||
// 字段不存在,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
return nil, fmt.Errorf("result字段不存在")
|
||||
}
|
||||
|
||||
// 检查data.result.data是否等于1
|
||||
if result.Int() != 1 {
|
||||
// 不等于1,返回企业信息校验不通过
|
||||
return createStatusResponsess(1), nil
|
||||
return nil, fmt.Errorf("企业信息验证不通过")
|
||||
}
|
||||
|
||||
// 天眼查三要素验证通过,继续调用星维身份证三要素验证
|
||||
if err := json.Unmarshal(params, ¶msDto); 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返回的数据结构
|
||||
return map[string]interface{}{
|
||||
"success": response.Success,
|
||||
"message": response.Message,
|
||||
"data": response.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// verifyPersonalInfo 验证个人信息并返回API数据
|
||||
func verifyPersonalInfo(ctx context.Context, paramsDto dto.QYGL5CMPReq, deps *processors.ProcessorDependencies) (map[string]interface{}, error) {
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.LegalPerson,
|
||||
@@ -89,6 +105,7 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
|
||||
projectID := "CDJ-1100244702166183936"
|
||||
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) {
|
||||
@@ -106,42 +123,6 @@ func ProcessQYGL5CMPRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("解析星维API响应失败: %w", err))
|
||||
}
|
||||
|
||||
// 构建天眼查API返回的数据结构
|
||||
tianYanChaData := map[string]interface{}{
|
||||
"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
|
||||
// 返回星维API的全部数据
|
||||
return xingweiData, nil
|
||||
}
|
||||
|
||||
12
internal/domains/api/services/processors/qygl/utils.go
Normal 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
|
||||
}
|
||||
@@ -4,12 +4,38 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"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处理方法
|
||||
func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSY09CDReq
|
||||
@@ -21,38 +47,98 @@ func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"phoneType": paramsDto.MobileType,
|
||||
},
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
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 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)
|
||||
} 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
|
||||
// 解析星维返回的数据
|
||||
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",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (s *EnterpriseInfoSubmitRecordService) Save(ctx context.Context, enterprise
|
||||
return s.repositories.Create(ctx, enterpriseInfoSubmitRecord)
|
||||
}
|
||||
|
||||
// ValidateWithWestdex 调用QYGL23T7处理器验证企业信息
|
||||
// ValidateWithWestdex 调用QYGL5CMP处理器验证企业信息
|
||||
func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Context, info *value_objects.EnterpriseInfo) error {
|
||||
if info == nil {
|
||||
return errors.New("企业信息不能为空")
|
||||
@@ -89,12 +89,13 @@ func (s *EnterpriseInfoSubmitRecordService) ValidateWithWestdex(ctx context.Cont
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// 构建QYGL23T7请求参数
|
||||
reqDto := dto.QYGL23T7Req{
|
||||
// 构建QYGL5CMP请求参数
|
||||
reqDto := dto.QYGL5CMPReq{
|
||||
EntName: info.CompanyName,
|
||||
LegalPerson: info.LegalPersonName,
|
||||
EntCode: info.UnifiedSocialCode,
|
||||
IDCard: info.LegalPersonID,
|
||||
MobileNo: info.LegalPersonPhone,
|
||||
}
|
||||
|
||||
// 序列化请求参数
|
||||
|
||||
180
internal/domains/finance/entities/purchase_order.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -4,29 +4,30 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ComponentReportDownload 组件报告下载记录
|
||||
type ComponentReportDownload struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"下载记录ID"`
|
||||
UserID 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:"产品编号"`
|
||||
SubProductIDs string `gorm:"type:text" comment:"子产品ID列表(JSON数组,组合包使用)"`
|
||||
SubProductCodes string `gorm:"type:text" comment:"子产品编号列表(JSON数组)"`
|
||||
DownloadPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" comment:"实际支付价格"`
|
||||
OriginalPrice decimal.Decimal `gorm:"type:decimal(10,2);not null" 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"`
|
||||
PaymentStatus string `gorm:"type:varchar(20);default:'pending';index" comment:"支付状态:pending, success, failed"`
|
||||
FilePath *string `gorm:"type:varchar(500)" comment:"生成的ZIP文件路径(用于二次下载)"`
|
||||
FileHash *string `gorm:"type:varchar(64)" comment:"文件哈希值(用于缓存验证)"`
|
||||
DownloadCount int `gorm:"default:0" comment:"下载次数"`
|
||||
LastDownloadAt *time.Time `comment:"最后下载时间"`
|
||||
ExpiresAt *time.Time `gorm:"index" comment:"下载有效期(支付成功后30天)"`
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"下载记录ID"`
|
||||
UserID 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:"产品编号"`
|
||||
ProductName string `gorm:"type:varchar(200);not null" comment:"产品名称"`
|
||||
// 直接关联购买订单
|
||||
OrderID *string `gorm:"type:varchar(36);index" comment:"关联的购买订单ID"`
|
||||
OrderNumber *string `gorm:"type:varchar(64);index" comment:"关联的购买订单号"`
|
||||
|
||||
// 组合包相关字段(从购买记录复制)
|
||||
SubProductIDs string `gorm:"type:text" comment:"子产品ID列表(JSON数组,组合包使用)"`
|
||||
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:"下载次数"`
|
||||
LastDownloadAt *time.Time `comment:"最后下载时间"`
|
||||
ExpiresAt *time.Time `gorm:"index" comment:"下载有效期(从创建日起30天)"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
@@ -46,11 +47,6 @@ func (c *ComponentReportDownload) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPaid 检查是否已支付
|
||||
func (c *ComponentReportDownload) IsPaid() bool {
|
||||
return c.PaymentStatus == "success"
|
||||
}
|
||||
|
||||
// IsExpired 检查是否已过期
|
||||
func (c *ComponentReportDownload) IsExpired() bool {
|
||||
if c.ExpiresAt == nil {
|
||||
@@ -61,5 +57,6 @@ func (c *ComponentReportDownload) IsExpired() bool {
|
||||
|
||||
// CanDownload 检查是否可以下载
|
||||
func (c *ComponentReportDownload) CanDownload() bool {
|
||||
return c.IsPaid() && !c.IsExpired()
|
||||
// 下载记录存在即表示用户有下载权限,只需检查是否过期
|
||||
return !c.IsExpired()
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
|
||||
// Subscription 订阅实体
|
||||
type Subscription struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"订阅ID"`
|
||||
UserID 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:"订阅价格"`
|
||||
APIUsed int64 `gorm:"default:0" comment:"已使用API调用次数"`
|
||||
Version int64 `gorm:"default:1" comment:"乐观锁版本号"`
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"订阅ID"`
|
||||
UserID 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:"订阅价格"`
|
||||
UIComponentPrice decimal.Decimal `gorm:"type:decimal(10,2);not null;default:0" comment:"UI组件价格(组合包使用)"`
|
||||
APIUsed int64 `gorm:"default:0" comment:"已使用API调用次数"`
|
||||
Version int64 `gorm:"default:1" comment:"乐观锁版本号"`
|
||||
|
||||
// 关联关系
|
||||
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
|
||||
|
||||
@@ -9,22 +9,23 @@ import (
|
||||
|
||||
// UIComponent UI组件实体
|
||||
type UIComponent struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"组件ID"`
|
||||
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:"组件名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"组件描述"`
|
||||
FilePath *string `gorm:"type:varchar(500)" json:"file_path" comment:"组件文件路径"`
|
||||
FileHash *string `gorm:"type:varchar(64)" json:"file_hash" comment:"文件哈希值"`
|
||||
FileSize *int64 `gorm:"type:bigint" json:"file_size" comment:"文件大小"`
|
||||
FileType *string `gorm:"type:varchar(50)" json:"file_type" comment:"文件类型"`
|
||||
FolderPath *string `gorm:"type:varchar(500)" json:"folder_path" comment:"组件文件夹路径"`
|
||||
IsExtracted bool `gorm:"default:false" json:"is_extracted" comment:"是否已解压"`
|
||||
Version string `gorm:"type:varchar(20)" json:"version" comment:"组件版本"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否启用"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order" 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:"deleted_at" comment:"软删除时间"`
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"组件ID"`
|
||||
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:"组件名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"组件描述"`
|
||||
FilePath *string `gorm:"type:varchar(500)" json:"file_path" comment:"组件文件路径"`
|
||||
FileHash *string `gorm:"type:varchar(64)" json:"file_hash" comment:"文件哈希值"`
|
||||
FileSize *int64 `gorm:"type:bigint" json:"file_size" comment:"文件大小"`
|
||||
FileType *string `gorm:"type:varchar(50)" json:"file_type" comment:"文件类型"`
|
||||
FolderPath *string `gorm:"type:varchar(500)" json:"folder_path" comment:"组件文件夹路径"`
|
||||
IsExtracted bool `gorm:"default:false" json:"is_extracted" comment:"是否已解压"`
|
||||
FileUploadTime *time.Time `gorm:"type:timestamp" json:"file_upload_time" comment:"文件上传时间"`
|
||||
Version string `gorm:"type:varchar(20)" json:"version" comment:"组件版本"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active" comment:"是否启用"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order" 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:"deleted_at" comment:"软删除时间"`
|
||||
}
|
||||
|
||||
func (UIComponent) TableName() string {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// ComponentReportRepository 组件报告仓储接口
|
||||
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
|
||||
@@ -20,6 +20,15 @@ type ComponentReportRepository interface {
|
||||
// 获取用户的下载记录列表
|
||||
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)
|
||||
|
||||
|
||||
@@ -104,9 +104,10 @@ func (s *ProductSubscriptionService) CreateSubscription(ctx context.Context, use
|
||||
|
||||
// 创建订阅
|
||||
subscription := &entities.Subscription{
|
||||
UserID: userID,
|
||||
ProductID: productID,
|
||||
Price: product.Price,
|
||||
UserID: userID,
|
||||
ProductID: productID,
|
||||
Price: product.Price,
|
||||
UIComponentPrice: product.UIComponentPrice,
|
||||
}
|
||||
|
||||
createdSubscription, err := s.subscriptionRepo.Create(ctx, *subscription)
|
||||
@@ -253,7 +254,7 @@ func (s *ProductSubscriptionService) IncrementSubscriptionAPIUsage(ctx context.C
|
||||
// GetSubscriptionStats 获取订阅统计信息
|
||||
func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
|
||||
// 获取总订阅数
|
||||
totalSubscriptions, err := s.subscriptionRepo.Count(ctx, interfaces.CountOptions{})
|
||||
if err != nil {
|
||||
@@ -261,7 +262,7 @@ func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (
|
||||
return nil, fmt.Errorf("获取订阅总数失败: %w", err)
|
||||
}
|
||||
stats["total_subscriptions"] = totalSubscriptions
|
||||
|
||||
|
||||
// 获取总收入
|
||||
totalRevenue, err := s.subscriptionRepo.GetTotalRevenue(ctx)
|
||||
if err != nil {
|
||||
@@ -269,30 +270,30 @@ func (s *ProductSubscriptionService) GetSubscriptionStats(ctx context.Context) (
|
||||
return nil, fmt.Errorf("获取总收入失败: %w", err)
|
||||
}
|
||||
stats["total_revenue"] = totalRevenue
|
||||
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetUserSubscriptionStats 获取用户订阅统计信息
|
||||
func (s *ProductSubscriptionService) GetUserSubscriptionStats(ctx context.Context, userID string) (map[string]interface{}, error) {
|
||||
stats := make(map[string]interface{})
|
||||
|
||||
|
||||
// 获取用户订阅数
|
||||
userSubscriptions, err := s.subscriptionRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户订阅失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("获取用户订阅失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 计算用户总收入
|
||||
var totalRevenue float64
|
||||
for _, subscription := range userSubscriptions {
|
||||
totalRevenue += subscription.Price.InexactFloat64()
|
||||
}
|
||||
|
||||
|
||||
stats["total_subscriptions"] = int64(len(userSubscriptions))
|
||||
stats["total_revenue"] = totalRevenue
|
||||
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -303,20 +304,47 @@ func (s *ProductSubscriptionService) UpdateSubscriptionPrice(ctx context.Context
|
||||
if err != nil {
|
||||
return fmt.Errorf("订阅不存在: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 更新价格
|
||||
subscription.Price = decimal.NewFromFloat(newPrice)
|
||||
subscription.Version++ // 增加版本号
|
||||
|
||||
|
||||
// 保存更新
|
||||
if err := s.subscriptionRepo.Update(ctx, subscription); err != nil {
|
||||
s.logger.Error("更新订阅价格失败", zap.Error(err))
|
||||
return fmt.Errorf("更新订阅价格失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
s.logger.Info("订阅价格更新成功",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.Float64("new_price", newPrice))
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"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) {
|
||||
err := r.CreateEntity(ctx, download)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return download, nil
|
||||
func (r *GormComponentReportRepository) Create(ctx context.Context, download *entities.ComponentReportDownload) error {
|
||||
return r.CreateEntity(ctx, download)
|
||||
}
|
||||
|
||||
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) {
|
||||
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 != "" {
|
||||
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) {
|
||||
var count int64
|
||||
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
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -88,7 +85,7 @@ func (r *GormComponentReportRepository) GetUserDownloadedProductCodes(ctx contex
|
||||
var downloads []entities.ComponentReportDownload
|
||||
err := r.GetDB(ctx).
|
||||
Select("DISTINCT sub_product_codes").
|
||||
Where("user_id = ? AND payment_status = ?", userID, "success").
|
||||
Where("user_id = ?", userID).
|
||||
Find(&downloads).Error
|
||||
if err != nil {
|
||||
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) {
|
||||
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 errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
@@ -128,3 +125,65 @@ func (r *GormComponentReportRepository) GetDownloadByPaymentOrderID(ctx context.
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -108,7 +108,8 @@ func (r *GormUIComponentRepository) Update(ctx context.Context, component entiti
|
||||
|
||||
// Delete 删除UI组件
|
||||
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 nil
|
||||
|
||||
47
internal/infrastructure/external/jiguang/crypto.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 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 hex.EncodeToString(hashBytes), nil
|
||||
}
|
||||
|
||||
// GenerateSignWithDefault 使用默认的 HMAC-MD5 方法生成签名
|
||||
func GenerateSignWithDefault(timestamp string, appSecret string) (string, error) {
|
||||
return GenerateSign(timestamp, appSecret, SignMethodHMACMD5)
|
||||
}
|
||||
149
internal/infrastructure/external/jiguang/jiguang_errors.go
vendored
Normal 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
|
||||
}
|
||||
85
internal/infrastructure/external/jiguang/jiguang_factory.go
vendored
Normal 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)
|
||||
}
|
||||
265
internal/infrastructure/external/jiguang/jiguang_service.go
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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 map[string]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)
|
||||
// params: 请求参数(会作为JSON body发送)
|
||||
func (j *JiguangService) CallAPI(ctx context.Context, apiCode 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
|
||||
}
|
||||
|
||||
// 记录请求日志
|
||||
if j.logger != nil {
|
||||
j.logger.LogRequest(requestID, transactionID, apiCode, j.config.URL, 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", j.config.URL, 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
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
@@ -90,13 +91,14 @@ func (m *MuziService) generateRequestID() string {
|
||||
}
|
||||
|
||||
// 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()
|
||||
now := time.Now()
|
||||
timestamp := strconv.FormatInt(now.UnixMilli(), 10)
|
||||
|
||||
flatParams := flattenParams(params)
|
||||
signParts := collectSignatureValues(params)
|
||||
|
||||
signParts := collectSignatureValues(paramSign)
|
||||
signature := m.GenerateSignature(prodCode, timestamp, signParts...)
|
||||
|
||||
// 从上下文获取链路ID
|
||||
@@ -128,7 +130,21 @@ func (m *MuziService) CallAPI(ctx context.Context, prodCode string, params map[s
|
||||
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 {
|
||||
err := errors.Join(ErrSystem, reqErr)
|
||||
if m.logger != nil {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1106,3 +1106,192 @@ func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
|
||||
|
||||
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, "获取管理端购买记录成功")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,6 +11,9 @@ import (
|
||||
"tyapi-server/internal/application/product/dto/queries"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProductAdminHandler 产品管理员HTTP处理器
|
||||
@@ -296,7 +297,6 @@ func (h *ProductAdminHandler) ListProducts(c *gin.Context) {
|
||||
// 解析查询参数
|
||||
page := h.getIntQuery(c, "page", 1)
|
||||
pageSize := h.getIntQuery(c, "page_size", 10)
|
||||
|
||||
// 构建筛选条件
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
|
||||
@@ -282,7 +282,8 @@ func (h *UIComponentHandler) DeleteUIComponent(c *gin.Context) {
|
||||
h.responseBuilder.NotFound(c, "UI组件不存在")
|
||||
return
|
||||
}
|
||||
h.responseBuilder.InternalError(c, "删除UI组件失败")
|
||||
// 提供更详细的错误信息
|
||||
h.responseBuilder.InternalError(c, fmt.Sprintf("删除UI组件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -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("组件报告订单路由注册完成")
|
||||
}
|
||||
@@ -69,6 +69,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
|
||||
walletGroup.GET("/alipay-order-status", r.financeHandler.GetAlipayOrderStatus) // 获取支付宝订单状态
|
||||
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("/gift-recharge", r.financeHandler.GiftRecharge) // 赠送充值
|
||||
adminFinanceGroup.GET("/recharge-records", r.financeHandler.GetAdminRechargeRecords) // 管理员充值记录分页
|
||||
adminFinanceGroup.GET("/purchase-records", r.financeHandler.GetAdminPurchaseRecords) // 管理员购买记录分页
|
||||
}
|
||||
|
||||
// 管理员发票相关路由组
|
||||
|
||||
@@ -81,8 +81,6 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
subscriptions.POST("/batch-update-prices", r.handler.BatchUpdateSubscriptionPrices)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 消费记录管理
|
||||
walletTransactions := adminGroup.Group("/wallet-transactions")
|
||||
{
|
||||
|
||||
@@ -70,14 +70,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
componentReport.POST("/generate-and-download", r.componentReportHandler.GenerateAndDownloadZip)
|
||||
}
|
||||
|
||||
// 产品组件报告相关接口 - 需要认证
|
||||
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)
|
||||
}
|
||||
// 产品组件报告相关接口 - 已迁移到 ComponentReportOrderRoutes
|
||||
|
||||
// 分类 - 公开接口
|
||||
categories := engine.Group("/api/v1/categories")
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
)
|
||||
|
||||
// UIComponentRoutes UI组件路由
|
||||
type UIComponentRoutes struct {
|
||||
uiComponentHandler *handlers.UIComponentHandler
|
||||
logger *zap.Logger
|
||||
auth *middleware.JWTAuthMiddleware
|
||||
admin *middleware.AdminAuthMiddleware
|
||||
}
|
||||
|
||||
// NewUIComponentRoutes 创建UI组件路由
|
||||
func NewUIComponentRoutes(
|
||||
uiComponentHandler *handlers.UIComponentHandler,
|
||||
logger *zap.Logger,
|
||||
auth *middleware.JWTAuthMiddleware,
|
||||
admin *middleware.AdminAuthMiddleware,
|
||||
) *UIComponentRoutes {
|
||||
return &UIComponentRoutes{
|
||||
uiComponentHandler: uiComponentHandler,
|
||||
logger: logger,
|
||||
auth: auth,
|
||||
admin: admin,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册UI组件路由
|
||||
func (r *UIComponentRoutes) RegisterRoutes(router *gin.RouterGroup, authMiddleware interfaces.Middleware) {
|
||||
uiComponentGroup := router.Group("/ui-components")
|
||||
uiComponentGroup.Use(authMiddleware.Handle())
|
||||
func (r *UIComponentRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
// 管理员路由组
|
||||
engine := router.GetEngine()
|
||||
uiComponentGroup := engine.Group("/api/v1/admin/ui-components")
|
||||
uiComponentGroup.Use(r.admin.Handle()) // 管理员权限验证
|
||||
{
|
||||
// UI组件管理
|
||||
uiComponentGroup.POST("", r.uiComponentHandler.CreateUIComponent) // 创建UI组件
|
||||
|
||||
@@ -193,6 +193,13 @@ componentReportHandler := component_report.NewComponentReportHandler(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
componentReportRepo,
|
||||
purchaseOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
aliPayService,
|
||||
wechatPayService,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
137
internal/shared/component_report/cache_manager.go
Normal 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
|
||||
}
|
||||
102
internal/shared/component_report/check_payment_status_fix.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -2,12 +2,15 @@ package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -21,6 +24,10 @@ type ExampleJSONGenerator struct {
|
||||
docRepo repositories.ProductDocumentationRepository
|
||||
apiConfigRepo repositories.ProductApiConfigRepository
|
||||
logger *zap.Logger
|
||||
// 缓存配置
|
||||
CacheEnabled bool
|
||||
CacheDir string
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewExampleJSONGenerator 创建示例JSON生成器
|
||||
@@ -35,6 +42,30 @@ func NewExampleJSONGenerator(
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
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(可以是组合包或单品)
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
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. 获取产品信息
|
||||
product, err := g.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
@@ -157,12 +202,21 @@ func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productI
|
||||
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
|
||||
}
|
||||
|
||||
// MatchProductCodeToPath 根据产品编码匹配 UI 组件路径,返回路径和类型(folder/file)
|
||||
func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) {
|
||||
basePath := filepath.Join("resources", "Pure Component", "src", "ui")
|
||||
// MatchSubProductCodeToPath 根据子产品编码匹配 UI 组件路径,返回路径和类型(folder/file)
|
||||
func (g *ExampleJSONGenerator) MatchSubProductCodeToPath(ctx context.Context, subProductCode string) (string, string, error) {
|
||||
basePath := filepath.Join("resources", "Pure_Component", "src", "ui")
|
||||
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
@@ -172,18 +226,8 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
|
||||
// 精确匹配
|
||||
if name == productCode {
|
||||
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)) {
|
||||
// 使用改进的相似性匹配算法
|
||||
if isSimilarCode(subProductCode, name) {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
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 提取文件名中的核心编码部分
|
||||
@@ -206,6 +250,44 @@ func extractCoreCode(name string) string {
|
||||
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配置 > 默认值)
|
||||
func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, product *entities.Product) interface{} {
|
||||
var responseData interface{}
|
||||
@@ -216,20 +298,20 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
|
||||
// 尝试直接解析为JSON
|
||||
err := json.Unmarshal([]byte(doc.ResponseExample), &responseData)
|
||||
if err == nil {
|
||||
g.logger.Debug("从产品文档中提取响应示例成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
// g.logger.Debug("从产品文档中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return responseData
|
||||
}
|
||||
|
||||
// 如果解析失败,尝试从Markdown代码块中提取JSON
|
||||
extractedData := extractJSONFromMarkdown(doc.ResponseExample)
|
||||
if extractedData != nil {
|
||||
g.logger.Debug("从Markdown代码块中提取响应示例成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
// g.logger.Debug("从Markdown代码块中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return extractedData
|
||||
}
|
||||
}
|
||||
@@ -240,10 +322,10 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
|
||||
// API配置的响应示例通常是 JSON 字符串
|
||||
err := json.Unmarshal([]byte(apiConfig.ResponseExample), &responseData)
|
||||
if err == nil {
|
||||
g.logger.Debug("从产品API配置中提取响应示例成功",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
// g.logger.Debug("从产品API配置中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return responseData
|
||||
}
|
||||
}
|
||||
@@ -284,3 +366,57 @@ func extractJSONFromMarkdown(markdown string) interface{} {
|
||||
// 如果提取失败,返回 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)
|
||||
}
|
||||
|
||||
172
internal/shared/component_report/handler_fixed.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package component_report
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -15,18 +18,35 @@ import (
|
||||
// ZipGenerator ZIP文件生成器
|
||||
type ZipGenerator struct {
|
||||
logger *zap.Logger
|
||||
// 缓存配置
|
||||
CacheEnabled bool
|
||||
CacheDir string
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewZipGenerator 创建ZIP文件生成器
|
||||
func NewZipGenerator(logger *zap.Logger) *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
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
// subProductCodes: 子产品编码列表(用于过滤和下载匹配的UI组件)
|
||||
// exampleJSONGenerator: 示例JSON生成器
|
||||
// outputPath: 输出ZIP文件路径(如果为空,则使用默认路径)
|
||||
func (g *ZipGenerator) GenerateZipFile(
|
||||
@@ -36,6 +56,29 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
exampleJSONGenerator *ExampleJSONGenerator,
|
||||
outputPath string,
|
||||
) (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 内容
|
||||
exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes)
|
||||
if err != nil {
|
||||
@@ -62,8 +105,8 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 4. 添加 example.json 到 public 目录
|
||||
exampleWriter, err := zipWriter.Create("public/example.json")
|
||||
// 4. 将生成的内容添加到 Pure_Component/public 目录下的 example.json
|
||||
exampleWriter, err := zipWriter.Create("Pure_Component/public/example.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建example.json文件失败: %w", err)
|
||||
}
|
||||
@@ -73,14 +116,14 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
return "", fmt.Errorf("写入example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 添加整个 src 目录,但过滤 ui 目录下的文件
|
||||
srcBasePath := filepath.Join("resources", "Pure Component", "src")
|
||||
uiBasePath := filepath.Join(srcBasePath, "ui")
|
||||
// 5. 添加整个 Pure_Component 目录,但只包含子产品编码匹配的UI组件文件
|
||||
srcBasePath := filepath.Join("resources", "Pure_Component")
|
||||
uiBasePath := filepath.Join(srcBasePath, "src", "ui")
|
||||
|
||||
// 收集所有匹配的组件名称(文件夹名或文件名)
|
||||
// 根据子产品编码收集所有匹配的组件名称(文件夹名或文件名)
|
||||
matchedNames := make(map[string]bool)
|
||||
for _, productCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode)
|
||||
for _, subProductCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchSubProductCodeToPath(ctx, subProductCode)
|
||||
if err == nil && 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 {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算相对于 src 的路径
|
||||
// 计算相对于 Pure_Component 的路径
|
||||
relPath, err := filepath.Rel(srcBasePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ZIP路径格式
|
||||
zipPath := filepath.ToSlash(filepath.Join("src", relPath))
|
||||
// 转换为ZIP路径格式,保持在Pure_Component目录下
|
||||
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", relPath))
|
||||
|
||||
// 检查是否在 ui 目录下
|
||||
uiRelPath, err := filepath.Rel(uiBasePath, path)
|
||||
@@ -120,26 +163,19 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
// 获取文件/文件夹名称
|
||||
fileName := info.Name()
|
||||
|
||||
// 检查是否应该保留:
|
||||
// 1. CBehaviorRiskScan.vue 文件(无论在哪里)
|
||||
// 2. 匹配到的组件文件夹/文件
|
||||
// 检查是否应该保留:匹配到的组件文件夹/文件
|
||||
shouldInclude := false
|
||||
|
||||
// 检查是否是 CBehaviorRiskScan.vue
|
||||
if fileName == "CBehaviorRiskScan.vue" {
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
if matchedNames[fileName] {
|
||||
shouldInclude = true
|
||||
} else {
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
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
|
||||
}
|
||||
// 检查是否在匹配的组件文件夹内
|
||||
// 获取相对于 ui 的路径的第一部分(组件文件夹名)
|
||||
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 {
|
||||
g.logger.Warn("添加src目录失败", zap.Error(err))
|
||||
g.logger.Warn("添加Pure_Component目录失败", zap.Error(err))
|
||||
}
|
||||
|
||||
g.logger.Info("成功生成ZIP文件",
|
||||
@@ -174,6 +210,15 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -263,3 +308,197 @@ func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPat
|
||||
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
|
||||
}
|
||||
|
||||
@@ -35,9 +35,17 @@ type CacheInfo struct {
|
||||
|
||||
// NewPDFCacheManager 创建PDF缓存管理器
|
||||
func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) {
|
||||
// 如果缓存目录为空,使用默认目录
|
||||
// 如果缓存目录为空,使用项目根目录的storage/component-reports/cache目录
|
||||
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.Int64("max_size", maxSize),
|
||||
)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
|
||||
BIN
resources/Pure Component.zip
Normal file
@@ -1,110 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=3, user-scalable=no"
|
||||
/>
|
||||
<title>报告查看器</title>
|
||||
<style>
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 481px) and (max-width: 768px) {
|
||||
html {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
#app-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
z-index: 9999;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid #ccc;
|
||||
border-top: 5px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loadingElement = document.getElementById('app-loading');
|
||||
if (loadingElement) {
|
||||
loadingElement.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
loadingElement.parentNode.removeChild(loadingElement);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "report-viewer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^11.3.0",
|
||||
"axios": "^1.7.7",
|
||||
"echarts": "^5.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"vant": "^4.9.9",
|
||||
"vue": "^3.5.12",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vant/auto-import-resolver": "^1.3.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"sass-embedded": "^1.81.0",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"terser": "^5.43.1",
|
||||
"unplugin-auto-import": "^0.18.5",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^5.4.10"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
3101
resources/Pure Component/pnpm-lock.yaml
generated
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// App 根组件,仅用于路由
|
||||
</script>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
html {
|
||||
margin: auto !important;
|
||||
/* @apply max-w-lg; */
|
||||
min-width: 320px;
|
||||
}
|
||||
body {
|
||||
background-color: #f8f8f8;
|
||||
min-height: 100vh;
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/* 统一颜色变量管理文件 */
|
||||
:root {
|
||||
/* ===== 主题色系 ===== */
|
||||
--color-primary: #5d7eeb;
|
||||
--color-primary-50: #f0f3ff;
|
||||
--color-primary-100: #e1e8ff;
|
||||
--color-primary-200: #c3d1ff;
|
||||
--color-primary-300: #a5baff;
|
||||
--color-primary-400: #87a3ff;
|
||||
--color-primary-500: #5d7eeb;
|
||||
--color-primary-600: #4a63bc;
|
||||
--color-primary-700: #38488d;
|
||||
--color-primary-800: #252d5e;
|
||||
--color-primary-900: #13122f;
|
||||
|
||||
--color-primary-light: rgba(93, 126, 235, 0.1);
|
||||
--color-primary-medium: rgba(93, 126, 235, 0.15);
|
||||
--color-primary-dark: rgba(93, 126, 235, 0.8);
|
||||
|
||||
/* ===== 语义化颜色 ===== */
|
||||
--color-success: #07c160;
|
||||
--color-warning: #ff976a;
|
||||
--color-danger: #ee0a24;
|
||||
--color-info: #1989fa;
|
||||
|
||||
/* ===== 中性色系 ===== */
|
||||
--color-gray-50: #fafafa;
|
||||
--color-gray-100: #f5f5f5;
|
||||
--color-gray-200: #e5e5e5;
|
||||
--color-gray-300: #d4d4d4;
|
||||
--color-gray-400: #a3a3a3;
|
||||
--color-gray-500: #737373;
|
||||
--color-gray-600: #525252;
|
||||
--color-gray-700: #404040;
|
||||
--color-gray-800: #262626;
|
||||
--color-gray-900: #171717;
|
||||
|
||||
/* ===== 文本颜色 ===== */
|
||||
--color-text-primary: #323233;
|
||||
--color-text-secondary: #646566;
|
||||
--color-text-tertiary: #969799;
|
||||
|
||||
/* ===== 背景颜色 ===== */
|
||||
--color-bg-primary: #ffffff;
|
||||
--color-bg-secondary: #fafafa;
|
||||
--color-bg-tertiary: #f8f8f8;
|
||||
|
||||
/* ===== 边框颜色 ===== */
|
||||
--color-border-primary: #ebedf0;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1024 1024" height="1024px" width="1024px">
|
||||
<title>空空如也</title>
|
||||
<defs>
|
||||
<rect rx="22.1405405" height="1024" width="1024" y="0" x="0" id="path-1"></rect>
|
||||
<linearGradient id="linearGradient-3" y2="64.8840762%" x2="50%" y1="-33.7184979%" x1="115.913479%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-4" y2="100%" x2="70.4980572%" y1="-20.569195%" x1="10.5031837%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-5" y2="104.73608%" x2="38.801584%" y1="-97.78046%" x1="100.191761%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-6" y2="100%" x2="50%" y1="-27.9013949%" x1="50%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-7" y2="100%" x2="50%" y1="-27.9013949%" x1="50%">
|
||||
<stop offset="0%" stop-color="#6CADFF"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#FFFFFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-8" y2="100%" x2="50%" y1="-221.1569%" x1="50%">
|
||||
<stop offset="0%" stop-color="#D2D2D2"></stop>
|
||||
<stop offset="100%" stop-opacity="0" stop-color="#D2D2D2"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="linearGradient-9" y2="53.7335012%" x2="73.0360423%" y1="48.1527472%" x1="67.5652976%">
|
||||
<stop offset="0%" stop-opacity="0" stop-color="#858585"></stop>
|
||||
<stop offset="100%" stop-opacity="0.5" stop-color="#616161"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill-rule="evenodd" fill="none" stroke-width="1" stroke="none" id="空空如也">
|
||||
<g>
|
||||
<mask fill="white" id="mask-2">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="蒙版"></g>
|
||||
<g mask="url(#mask-2)" id="编组-3">
|
||||
<g transform="translate(0, 238.0108)">
|
||||
<g fill-rule="evenodd" fill="none" stroke-width="1" stroke="none" id="编组-2">
|
||||
<g fill-rule="nonzero" transform="translate(162.5599, 0)" id="编组">
|
||||
<polygon points="592.450826 498.100707 589.3555 489.432325 587.255101 479.632083 586.398359 469.500567 586.868185 459.341443 588.001295 452.660716 589.852963 446.421689 592.367915 440.541544 595.711972 435.158313 599.857498 430.520452 604.887401 426.517537 606.932527 428.091097 608.342006 430.133964 609.060564 432.508107 609.032927 435.075494 608.286732 439.989418 607.070711 451.308006 606.794343 463.813666 607.540538 469.25211 608.894743 472.371623 610.248947 473.448269 612.072979 473.393057 614.864299 471.791891 619.037461 467.319668 626.084854 456.884481 638.963619 434.136879 651.870021 411.527309 659.000324 400.705634 667.015006 390.325661 673.758394 383.451689 680.225413 378.42734 686.471338 374.921338 693.159452 372.491982 699.156645 371.470549 704.628738 371.691399 709.8521 373.126928 714.191083 375.639102 717.783871 379.283135 720.32646 383.755358 721.818849 389.331833 722.150491 396.343837 721.155565 403.328234 718.530066 411.610128 713.942351 421.493188 708.884811 429.471413 700.400302 443.19175 696.006046 451.031943 692.219799 458.706498 689.345568 466.270628 688.212458 472.09556 688.46119 475.601562 689.760121 477.920492 692.109252 479.438839 695.232214 479.714902 698.299903 479.052351 705.070927 476.540176 711.924862 473.393057 718.778797 470.383969 725.384001 468.258282 729.805894 467.595731 734.144877 467.8994 738.511497 469.224503 741.966102 471.377796 744.702148 474.055608 746.857821 477.368366 748.985858 483.000054 750.008421 489.349506 750.036057 495.864596 749.206952 502.158835 746.526179 511.40695 742.104286 520.737884 736.21764 529.406266 728.866242 537.16364 723.283601 541.608256 717.203498 545.25229 710.570657 548.123346 703.606175 550.000575 696.199503 550.745946 688.212458 550.359458 675.554788 548.09574 661.238907 544.396494 646.923027 539.537783 632.855878 533.464394 623.76336 528.688502 615.582857 523.526121 608.203822 517.977252 601.543344 511.683013 596.375256 505.140317 592.533736 498.349164" fill="url(#linearGradient-3)" id="路径"></polygon>
|
||||
<polygon points="39.9075893 440.458725 31.7823599 436.096928 23.6571305 430.216783 16.222822 423.287598 9.75580266 415.447406 4.58771456 406.834236 1.21602073 397.834578 0.0552736694 391.623157 0 385.411737 1.05019972 379.117498 3.59278851 378.896647 5.99719313 379.421167 8.12522941 380.635845 9.83871316 382.54068 12.6023966 386.654021 19.2905106 395.87453 27.4986505 405.315889 31.6994494 408.849497 34.7947749 410.257419 36.5082587 410.146993 37.8348267 408.877103 38.8297528 405.840409 38.9403001 399.711807 37.1439059 387.26136 31.3401706 361.863552 25.7022563 336.465744 23.7676779 323.601202 22.8003886 310.488203 23.242578 300.881206 24.6520566 292.820163 26.9459139 286.056616 30.2899709 279.789983 34.0762172 275.014091 38.2770161 271.508089 43.1134622 269.078734 48.0051819 268.0573 53.1179963 268.333363 57.9820792 269.934529 62.8185253 273.081649 67.7655187 278.050785 71.6899493 283.903324 75.3103746 291.798729 78.5162474 302.206309 80.1191838 311.509637 83.0210515 327.383267 85.0385404 336.134468 87.4153082 344.361149 90.3448127 351.870067 93.4125013 356.977234 95.9550901 359.378984 98.4700421 360.234779 101.206089 359.765472 103.748678 357.888243 105.572709 355.320856 108.916766 348.916191 111.873907 342.014613 114.913959 335.195853 118.368563 329.23289 121.215157 325.782101 124.642125 323.159501 128.78765 321.254665 132.794991 320.509295 136.636511 320.674933 140.450394 321.696366 145.81194 324.429391 150.841844 328.459913 155.236101 333.291018 158.856526 338.508611 162.670409 345.575827 166.788298 354.823942 170.381086 364.762215 173.448775 375.639102 175.604448 386.764446 176.5441 397.641334 176.378279 404.874188 175.41099 411.499703 173.697506 417.628304 171.04437 423.315205 167.396308 428.173916 162.642772 432.314863 157.032495 435.62762 150.344381 438.691921 142.440246 441.424946 130.058944 444.572066 116.2958 446.835784 102.173378 448.160887 87.9956817 448.547375 74.0390802 447.967642 61.3537731 446.504508 49.4422973 443.964727 40.1563208 440.707182" fill="url(#linearGradient-4)" id="路径"></polygon>
|
||||
<path fill="url(#linearGradient-5)" id="形状结合" d="M648.498327,284.510663 L644.71208,289.9215 L641.589118,295.939676 L639.350534,302.344341 L638.217424,308.914643 L638.355608,315.567765 L640.013818,322.165674 L642.224765,326.058164 L644.988449,328.92922 L647.94559,331.579426 L650.571089,334.505696 L652.284573,338.039304 L652.83731,343.588173 L651.648926,349.689168 L649.576163,355.458887 L646.619022,360.952544 L640.428371,370.780391 L633.408615,379.945687 L625.615028,388.53125 L630.092195,393.086292 L633.159883,398.524736 L634.735183,404.570518 L634.707546,410.947577 L633.215157,417.186603 L630.755479,423.066748 L627.411422,428.477585 L623.210623,433.336296 L620.115297,436.234959 L616.384325,438.25022 L612.459894,438.691921 L608.535464,438.25022 L595.960704,435.517195 L583.77286,431.624705 L571.944295,426.57275 L566.555112,423.729299 L561.608118,420.250904 L557.545504,416.027138 L554.864731,410.91997 L554.035626,406.723811 L554.25672,403.769935 L555.251646,401.699462 L557.877146,399.049256 L561.41466,396.895963 L562.685955,396.178199 L566.30638,393.886875 L569.180611,390.822574 L571.253374,386.129501 L572.524668,380.939514 L574.127604,370.55954 L575.150167,359.075314 L575.896362,352.781075 L577.444025,347.121781 L579.212782,343.864236 L581.783008,341.352061 L585.292886,339.557651 L592.533736,338.260154 L599.74695,336.935051 L604.389938,334.754152 L608.176185,331.827883 L611.243873,328.128637 L614.974846,321.696366 L618.180719,314.518725 L618.899277,312.751921 L622.630249,304.663271 L624.785922,300.908813 L627.273238,297.568449 L630.313289,294.86303 L634.292994,292.461281 L642.611681,288.430759 L646.28738,286.387892 L648.498327,284.510663 Z M619.009824,341.73855 L615.195941,346.514442 L606.158696,359.323771 L600.935334,367.854122 L595.463241,378.013245 L590.626795,388.807313 L586.702364,400.346752 L584.795423,408.269764 L583.717586,416.192776 L583.468855,424.115788 L595.325057,424.115788 L596.319983,416.109957 L599.912771,395.929742 L602.925186,383.313657 L607.153622,369.234437 L612.432257,355.238037 L619.009824,341.73855 Z"></path>
|
||||
<polygon points="125.250135 60.7614951 125.250135 60.7614951 124.559214 54.3016178 122.431178 48.4214731 119.059484 43.2314863 114.55468 38.9249014 109.165497 35.7777818 102.947209 33.9005525 96.4525532 33.5416704 90.3171759 34.7011355 84.6239879 37.21331 79.6769945 40.9677686 75.6972903 45.7712671 72.8783332 51.6238055 66.8258663 52.3415696 61.3537731 54.5500746 56.6278743 58.111289 52.9245385 62.9147875 50.658318 68.5464754 49.9673972 74.3990138 50.8517759 80.2515521 53.3114542 85.8004211 57.1529742 90.4934943 61.9894203 93.8890708 67.5444241 95.931938 73.569254 96.4564579 123.011551 96.4564579 127.626903 95.7663001 131.634244 94.1375276 135.171759 91.5149279 137.963079 88.1193514 139.78711 84.1992549 140.699126 79.6442132 140.367484 74.9787463 139.013279 70.8654057 136.664148 67.1661597 133.513549 64.1294653 129.727302 62.0037792" fill="url(#linearGradient-6)" id="路径"></polygon>
|
||||
<polygon points="329.569254 33.7073083 329.569254 33.5416704 329.127065 28.130833 327.911044 23.0788777 325.921192 18.3305919 321.665119 11.9259273 316.054842 6.65312145 311.715859 3.89249014 306.934686 1.84962298 301.656051 0.496913635 296.266868 0 291.071143 0.35888207 286.041239 1.49074091 278.993846 4.61025428 272.858469 9.22050857 269.376228 13.0301798 266.557271 17.3643709 264.318687 22.305901 259.205873 22.8856335 254.507611 24.2383429 250.196265 26.364029 246.299471 29.2074792 243.010688 32.6030557 240.302278 36.5783648 238.284789 40.9677686 237.096405 45.6608418 236.681853 50.7956161 237.234589 55.902784 238.588794 60.5682509 240.744467 64.8748357 243.591061 68.7673259 246.990392 72.0524771 250.970096 74.7855021 255.364353 76.7731567 260.062615 77.9878345 265.203066 78.3743228 326.612113 78.3743228 332.360574 77.6565587 337.501026 75.5860852 341.950556 72.3285403 345.488071 68.1047744 347.892475 63.1080317 348.997949 57.4211312 348.611033 51.6514118 346.842276 46.378606 343.885134 41.7683517 339.877793 37.9862868 335.041347 35.2808681 329.403433 33.8453398" fill="url(#linearGradient-7)" id="路径"></polygon>
|
||||
</g>
|
||||
<polygon points="1024 550.359458 999.596675 534.403009 973.839145 519.164324 946.672136 504.615797 918.040376 490.785034 889.159883 478.224161 859.118644 466.491478 827.833747 455.669804 795.305193 445.703925 762.693728 437.007936 729.114974 429.360987 694.541293 422.735472 658.917413 417.186603 623.348807 412.880018 587.034006 409.760505 549.945374 407.883276 512 407.193118 474.054626 407.883276 436.965994 409.760505 400.623556 412.880018 365.082587 417.186603 329.458707 422.735472 294.885026 429.360987 261.306272 437.007936 228.694807 445.703925 196.166253 455.669804 164.881356 466.491478 134.840117 478.224161 105.931987 490.785034 77.3278635 504.615797 50.160855 519.164324 24.4033251 534.403009 0 550.359458" fill-rule="nonzero" fill="url(#linearGradient-8)" id="路径"></polygon>
|
||||
</g>
|
||||
<polygon points="168.585366 389.215532 314.612039 389.215532 461.536985 469.266954 317.214646 465.594981" fill-rule="nonzero" fill="url(#linearGradient-9)" stroke="none" id="矩形"></polygon>
|
||||
<polygon points="481.155803 208.25638 479.722777 299.451613 688.569371 236.303345" fill-rule="nonzero" fill="#B8D6FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="314.788219 244.959395 481.155803 208.631248 481.155803 264.00952" fill-rule="nonzero" fill="#9CC6FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="314.788219 244.959395 511.147006 264.384388 511.147006 512.547202 314.788219 465.075243" fill-rule="nonzero" fill="#64ADFF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="314.788219 244.959395 511.283486 263.617612 489.889892 383.428742 314.788219 346.994453" fill-rule="nonzero" fill="#429BFF" stroke="none" id="矩形"></polygon>
|
||||
<polygon points="511.147006 264.384388 688.569371 236.303345 688.569371 458.600245 511.147006 512.547202" opacity="0.99" fill-rule="nonzero" fill="#9CC5FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="511.283486 264.997809 671.897967 239.34913 688.569371 344.292902 535.025228 383.428742" fill-rule="nonzero" fill="#64ADFF" stroke="none" id="矩形"></polygon>
|
||||
<polygon points="314.788219 244.959395 267.566573 324.806343 465.801946 362.565804 511.147006 264.384388" fill-rule="nonzero" fill="#9CC6FF" stroke="none" id="路径"></polygon>
|
||||
<polygon points="511.147006 264.384388 566.898574 362.565804 745.583366 317.786082 688.569371 236.303345" opacity="0.99" fill-rule="nonzero" fill="#9DC6FF" stroke="none" id="路径"></polygon>
|
||||
<path stroke-dasharray="11.05477807439905,8.29108355579929" fill="none" stroke-width="5.52738904" stroke="#9DC6FF" id="路径-19" d="M583.139543,151.843183 C532.695151,184.875351 501.824507,214.257045 511.001904,232.450753 C523.475834,257.179661 544.659409,246.913618 547.874,236.537816 C551.088588,226.162013 542.242035,205.908265 523.475834,216.933951 C504.709635,227.959637 484.261479,247.732311 479.722777,267.098145"></path>
|
||||
<g transform="translate(555.0939, 41.4059)" fill-rule="evenodd" fill="none" stroke-width="1" stroke="none" id="飞机">
|
||||
<polygon points="163.057977 9.09494702e-13 0 30.854292 41.4554178 58.3378535" fill-rule="nonzero" fill="#9DC6FF" id="路径-16备份"></polygon>
|
||||
<polygon points="163.057977 0 41.4554178 58.3378535 41.4554178 104.894966" fill-rule="nonzero" fill="#64ADFF" id="路径-16备份-2"></polygon>
|
||||
<polygon points="163.057977 0 41.4554178 58.3378535 65.4910753 84.1769753" fill-rule="nonzero" fill="#429BFF" id="路径-16备份-2"></polygon>
|
||||
<polygon points="163.057977 0 58.9237102 70.0692202 108.951745 102.134572" fill-rule="nonzero" fill="#9DC6FF" id="路径-16备份"></polygon>
|
||||
<line stroke-linecap="round" stroke-width="2.76369452" stroke="#429BFF" id="路径-16" y2="32.1173295" x2="0.501635492" y1="58.3378535" x1="41.4554178"></line>
|
||||
<line stroke-linecap="round" stroke-width="2.76369452" stroke="#429BFF" id="路径-17" y2="101.744072" x2="107.648858" y1="71.0666294" x1="59.7211085"></line>
|
||||
<line stroke-linecap="round" stroke-width="2.76369452" stroke="#429BFF" id="路径-18" y2="103.502333" x2="41.4554178" y1="58.3378535" x1="41.4554178"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 816 B |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 286 B |