Compare commits

..

22 Commits

Author SHA1 Message Date
39f799bc41 fix 2025-12-31 18:06:59 +08:00
1d4411a940 add 2025-12-31 17:46:03 +08:00
fe44b452e3 新增:极光config 2025-12-31 16:12:09 +08:00
f1ec9bfe7f 新增:新增极光源,极光婚姻接口(测试) 2025-12-31 15:42:05 +08:00
a70e736cdd fix 2025-12-27 19:18:12 +08:00
53d2c75a9c 更改:给yysy09cd无缝切换换源 2025-12-26 18:48:53 +08:00
0bfa7b4f50 移除王起航 2025-12-26 14:41:31 +08:00
e2e729eec8 add 人脸123 2025-12-25 18:25:37 +08:00
5f7fb43804 fix 2025-12-25 12:40:40 +08:00
89c5c0f9ad 1 2025-12-24 17:52:51 +08:00
6c949a4a1c fix id_car 2025-12-24 17:52:33 +08:00
8556e7331d add fix id_car 2025-12-24 17:50:33 +08:00
311d7a9b01 fix 1 2025-12-24 12:32:25 +08:00
ce45ce3ed0 fix 2025-12-23 18:54:38 +08:00
34e2c1bc41 fixdele 2025-12-23 17:17:41 +08:00
2618105140 fix 2025-12-23 16:16:45 +08:00
6b41f3833a fix 2025-12-23 15:04:53 +08:00
446a5c7661 fix 2025-12-23 11:26:25 +08:00
7f8554fa12 add购买记录功能 2025-12-22 18:32:34 +08:00
65a61d0336 fix2 2025-12-19 18:16:22 +08:00
8dd6f71baf fix 2025-12-19 18:13:31 +08:00
e96653751d fix 2025-12-19 17:50:29 +08:00
358 changed files with 6554 additions and 83745 deletions

5
.gitignore vendored
View File

@@ -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*

View File

@@ -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

View File

@@ -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

View File

@@ -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 # 启用代理检查

View File

@@ -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:

View File

@@ -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 {

View File

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

View File

@@ -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),

View File

@@ -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"`

View File

@@ -109,13 +109,16 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(
)
// 验证验证码
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
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))

View File

@@ -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"`
}

View File

@@ -43,6 +43,10 @@ type FinanceApplicationService interface {
GetUserRechargeRecords(ctx context.Context, userID string, filters map[string]interface{}, options interfaces.ListOptions) (*responses.RechargeRecordListResponse, error)
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)
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,10 @@ type CreateProductCommand struct {
IsVisible bool `json:"is_visible" comment:"是否展示"`
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描述"`

View File

@@ -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:所有)"`
}

View File

@@ -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描述"`

View File

@@ -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:"总收入"`
}

View File

@@ -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,
}
// 添加分类信息

View File

@@ -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(),
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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域名

View File

@@ -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)

View File

@@ -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,17 @@ 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 QCXG5F3AReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type QCXG4D2EReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
UserType string `json:"user_type" validate:"required,oneof=1 2 3"`
}
type COMENT01Req struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
@@ -701,6 +730,11 @@ type IVYZ6M8PReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
}
type IVYZ9H2MReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type YYSY9E4AReq struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}

View File

@@ -18,6 +18,7 @@ import (
"tyapi-server/internal/domains/api/services/processors/yysy"
"tyapi-server/internal/domains/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,9 @@ func registerAllProcessors(combService *comb.CombService) {
"QCXG9P1C": qcxg.ProcessQCXG9P1CRequest,
"QCXG8A3D": qcxg.ProcessQCXG8A3DRequest,
"QCXG6B4E": qcxg.ProcessQCXG6B4ERequest,
"QCXG4896": qcxg.ProcessQCXG4896Request,
"QCXG5F3A": qcxg.ProcessQCXG5F3ARequest, // 极光个人车辆查询
"QCXG4D2E": qcxg.ProcessQCXG4D2ERequest, // 极光名下车辆数量查询
// DWBG系列处理器 - 多维报告
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,

View File

@@ -198,7 +198,14 @@ 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
"QCXG5F3A": &dto.QCXG5F3AReq{}, //极光个人车辆查询
"QCXG4D2E": &dto.QCXG4D2EReq{}, //极光名下车辆数量查询
}
// 优先返回已配置的DTO

View File

@@ -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在调用时设置

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
// 构建天眼查API返回的数据结构
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
}

View File

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

View File

@@ -4,12 +4,38 @@ import (
"context"
"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",
},
}
}

View File

@@ -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,
}
// 序列化请求参数

View File

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

View File

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

View File

@@ -4,29 +4,30 @@ import (
"time"
"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()
}

View File

@@ -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:"产品"`

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -115,7 +115,7 @@ func (s *UserAggregateServiceImpl) CreateUser(ctx context.Context, phone, passwo
func (s *UserAggregateServiceImpl) LoadUser(ctx context.Context, userID string) (*entities.User, error) {
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)
}

View File

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

View File

@@ -4,6 +4,7 @@ import (
"context"
"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
}

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ import (
"reflect"
"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 {

View File

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

View File

@@ -1106,3 +1106,192 @@ func (h *FinanceHandler) DebugEventSystem(c *gin.Context) {
h.responseBuilder.Success(c, debugInfo, "事件系统调试信息")
}
// GetUserPurchaseRecords 获取用户购买记录
// @Summary 获取用户购买记录
// @Description 获取当前用户的购买记录列表,支持分页和筛选
// @Tags 财务管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param payment_type query string false "支付类型: alipay, wechat, free"
// @Param pay_channel query string false "支付渠道: alipay, wechat"
// @Param status query string false "订单状态: created, paid, failed, cancelled, refunded, closed"
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
// @Param min_amount query string false "最小金额"
// @Param max_amount query string false "最大金额"
// @Param product_code query string false "产品编号"
// @Success 200 {object} responses.PurchaseRecordListResponse "获取成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/finance/purchase-records [get]
func (h *FinanceHandler) GetUserPurchaseRecords(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
h.responseBuilder.Unauthorized(c, "用户未登录")
return
}
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 支付类型筛选
if paymentType := c.Query("payment_type"); paymentType != "" {
filters["payment_type"] = paymentType
}
// 支付渠道筛选
if payChannel := c.Query("pay_channel"); payChannel != "" {
filters["pay_channel"] = payChannel
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 产品编号筛选
if productCode := c.Query("product_code"); productCode != "" {
filters["product_code"] = productCode
}
// 金额范围筛选
if minAmount := c.Query("min_amount"); minAmount != "" {
filters["min_amount"] = minAmount
}
if maxAmount := c.Query("max_amount"); maxAmount != "" {
filters["max_amount"] = maxAmount
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.appService.GetUserPurchaseRecords(c.Request.Context(), userID, filters, options)
if err != nil {
h.logger.Error("获取用户购买记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取购买记录失败")
return
}
h.responseBuilder.Success(c, result, "获取用户购买记录成功")
}
// GetAdminPurchaseRecords 获取管理端购买记录
// @Summary 获取管理端购买记录
// @Description 获取所有用户的购买记录列表,支持分页和筛选(管理员权限)
// @Tags 管理员-财务管理
// @Accept json
// @Produce json
// @Security Bearer
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(10)
// @Param user_id query string false "用户ID"
// @Param payment_type query string false "支付类型: alipay, wechat, free"
// @Param pay_channel query string false "支付渠道: alipay, wechat"
// @Param status query string false "订单状态: created, paid, failed, cancelled, refunded, closed"
// @Param start_time query string false "开始时间 (格式: 2006-01-02 15:04:05)"
// @Param end_time query string false "结束时间 (格式: 2006-01-02 15:04:05)"
// @Param min_amount query string false "最小金额"
// @Param max_amount query string false "最大金额"
// @Param product_code query string false "产品编号"
// @Success 200 {object} responses.PurchaseRecordListResponse "获取成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Failure 403 {object} map[string]interface{} "权限不足"
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
// @Router /api/v1/admin/finance/purchase-records [get]
func (h *FinanceHandler) GetAdminPurchaseRecords(c *gin.Context) {
// 解析查询参数
page := h.getIntQuery(c, "page", 1)
pageSize := h.getIntQuery(c, "page_size", 10)
// 构建筛选条件
filters := make(map[string]interface{})
// 用户ID筛选
if userID := c.Query("user_id"); userID != "" {
filters["user_id"] = userID
}
// 时间范围筛选
if startTime := c.Query("start_time"); startTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil {
filters["start_time"] = t
}
}
if endTime := c.Query("end_time"); endTime != "" {
if t, err := time.Parse("2006-01-02 15:04:05", endTime); err == nil {
filters["end_time"] = t
}
}
// 支付类型筛选
if paymentType := c.Query("payment_type"); paymentType != "" {
filters["payment_type"] = paymentType
}
// 支付渠道筛选
if payChannel := c.Query("pay_channel"); payChannel != "" {
filters["pay_channel"] = payChannel
}
// 状态筛选
if status := c.Query("status"); status != "" {
filters["status"] = status
}
// 产品编号筛选
if productCode := c.Query("product_code"); productCode != "" {
filters["product_code"] = productCode
}
// 金额范围筛选
if minAmount := c.Query("min_amount"); minAmount != "" {
filters["min_amount"] = minAmount
}
if maxAmount := c.Query("max_amount"); maxAmount != "" {
filters["max_amount"] = maxAmount
}
// 构建分页选项
options := interfaces.ListOptions{
Page: page,
PageSize: pageSize,
Sort: "created_at",
Order: "desc",
}
result, err := h.appService.GetAdminPurchaseRecords(c.Request.Context(), filters, options)
if err != nil {
h.logger.Error("获取管理端购买记录失败", zap.Error(err))
h.responseBuilder.BadRequest(c, "获取购买记录失败")
return
}
h.responseBuilder.Success(c, result, "获取管理端购买记录成功")
}

View File

@@ -1,8 +1,6 @@
package handlers
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{})

View File

@@ -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
}

View File

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

View File

@@ -69,6 +69,7 @@ func (r *FinanceRoutes) Register(router *sharedhttp.GinRouter) {
walletGroup.GET("/recharge-records", r.financeHandler.GetUserRechargeRecords) // 用户充值记录分页
walletGroup.GET("/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) // 管理员购买记录分页
}
// 管理员发票相关路由组

View File

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

View File

@@ -70,14 +70,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
componentReport.POST("/generate-and-download", r.componentReportHandler.GenerateAndDownloadZip)
}
// 产品组件报告相关接口 - 需要认证
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")

View File

@@ -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组件

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,15 @@ package component_report
import (
"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)
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3,11 +3,14 @@ package component_report
import (
"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
}

View File

@@ -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
}

Binary file not shown.

View 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>

View File

@@ -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"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
<template>
<router-view />
</template>
<script setup>
// App 根组件,仅用于路由
</script>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

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