fix
This commit is contained in:
24
config.yaml
24
config.yaml
@@ -90,11 +90,6 @@ ocr:
|
||||
api_key: "your-baidu-api-key"
|
||||
secret_key: "your-baidu-secret-key"
|
||||
|
||||
# 充值配置
|
||||
recharge:
|
||||
min_amount: "100.00" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
|
||||
ratelimit:
|
||||
requests: 5000
|
||||
window: 60s
|
||||
@@ -161,6 +156,16 @@ esign:
|
||||
# ===========================================
|
||||
wallet:
|
||||
default_credit_limit: 50.00
|
||||
min_amount: "100.00" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
# 支付宝充值赠送配置
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 1000.00 # 充值1000元
|
||||
bonus_amount: 50.00 # 赠送50元
|
||||
- recharge_amount: 5000.00 # 充值5000元
|
||||
bonus_amount: 300.00 # 赠送300元
|
||||
- recharge_amount: 10000.00 # 充值10000元
|
||||
bonus_amount: 800.00 # 赠送800元
|
||||
|
||||
# ===========================================
|
||||
# 🌍 西部数据配置
|
||||
@@ -190,16 +195,9 @@ alipay:
|
||||
notify_url: "https://consoletest.tianyuanapi.com/api/v1/finance/alipay/callback"
|
||||
return_url: "https://consoletest.tianyuanapi.com/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 🌐 域名配置
|
||||
# ===========================================
|
||||
domain:
|
||||
api: "" # 开发环境不限制域名,生产环境为 "api.tianyuancha.com"
|
||||
|
||||
|
||||
# ===========================================
|
||||
# 🔍 天眼查配置
|
||||
# ===========================================
|
||||
tianyancha:
|
||||
base_url: http://open.api.tianyancha.com/services
|
||||
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
|
||||
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
|
||||
|
||||
@@ -92,11 +92,20 @@ alipay:
|
||||
return_url: "http://127.0.0.1:8080/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 💰 充值配置
|
||||
# 💰 钱包配置
|
||||
# ===========================================
|
||||
recharge:
|
||||
min_amount: "0.01" # 开发环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
wallet:
|
||||
default_credit_limit: 0.01
|
||||
min_amount: "0.01" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
# 支付宝充值赠送配置
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 0.01 # 充值1000元
|
||||
bonus_amount: 50.00 # 赠送50元
|
||||
- recharge_amount: 0.05 # 充值5000元
|
||||
bonus_amount: 300.00 # 赠送300元
|
||||
- recharge_amount: 0.1 # 充值10000元
|
||||
bonus_amount: 800.00 # 赠送800元
|
||||
|
||||
# ===========================================
|
||||
# 🔍 天眼查配置
|
||||
|
||||
@@ -96,6 +96,9 @@ logger:
|
||||
# ===========================================
|
||||
jwt:
|
||||
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
|
||||
|
||||
api:
|
||||
domain: "apitest.tianyuanapi.com"
|
||||
# ===========================================
|
||||
# 📁 存储服务配置 - 七牛云
|
||||
# ===========================================
|
||||
@@ -155,8 +158,18 @@ alipay:
|
||||
return_url: "https://consoletest.tianyuanapi.com/api/v1/finance/alipay/return"
|
||||
|
||||
# ===========================================
|
||||
# 💰 充值配置
|
||||
# 💰 钱包配置
|
||||
# ===========================================
|
||||
recharge:
|
||||
min_amount: "0.01" # 开发环境最低充值金额
|
||||
wallet:
|
||||
default_credit_limit: 50.00
|
||||
min_amount: "100.00" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
# 支付宝充值赠送配置
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 1000.00 # 充值1000元
|
||||
bonus_amount: 50.00 # 赠送50元
|
||||
- recharge_amount: 5000.00 # 充值5000元
|
||||
bonus_amount: 300.00 # 赠送300元
|
||||
- recharge_amount: 10000.00 # 充值10000元
|
||||
bonus_amount: 800.00 # 赠送800元
|
||||
|
||||
|
||||
209
docs/支付宝充值赠送功能说明.md
Normal file
209
docs/支付宝充值赠送功能说明.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# 支付宝充值赠送功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
本功能实现了支付宝充值的自动赠送机制,根据充值金额自动计算并赠送相应的金额到用户钱包。
|
||||
|
||||
## 赠送规则
|
||||
|
||||
当前配置的赠送规则如下:
|
||||
|
||||
| 充值金额 | 赠送金额 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 1000元 | 50元 | 充值1000元及以上,赠送50元 |
|
||||
| 5000元 | 300元 | 充值5000元及以上,赠送300元 |
|
||||
| 10000元 | 800元 | 充值10000元及以上,赠送800元 |
|
||||
|
||||
**注意**:赠送规则按充值金额从高到低匹配,即充值金额满足多个条件时,按最高档次的赠送金额计算。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 1. 配置文件位置
|
||||
|
||||
- 主配置文件:`config.yaml`
|
||||
- 环境配置文件:`configs/env.*.yaml`
|
||||
|
||||
### 2. 配置结构
|
||||
|
||||
```yaml
|
||||
wallet:
|
||||
default_credit_limit: 50.00
|
||||
# 支付宝充值赠送配置
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 1000.00 # 充值1000元
|
||||
bonus_amount: 50.00 # 赠送50元
|
||||
- recharge_amount: 5000.00 # 充值5000元
|
||||
bonus_amount: 300.00 # 赠送300元
|
||||
- recharge_amount: 10000.00 # 充值10000元
|
||||
bonus_amount: 800.00 # 赠送800元
|
||||
```
|
||||
|
||||
### 3. 配置结构体
|
||||
|
||||
```go
|
||||
type WalletConfig struct {
|
||||
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
|
||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
type AliPayRechargeBonusRule struct {
|
||||
RechargeAmount float64 `mapstructure:"recharge_amount"` // 充值金额
|
||||
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
|
||||
}
|
||||
```
|
||||
|
||||
## 实现逻辑
|
||||
|
||||
### 1. 赠送金额计算
|
||||
|
||||
在 `calculateAlipayRechargeBonus` 函数中实现:
|
||||
|
||||
```go
|
||||
func calculateAlipayRechargeBonus(rechargeAmount decimal.Decimal, walletConfig *config.WalletConfig) decimal.Decimal {
|
||||
if walletConfig == nil || len(walletConfig.AliPayRechargeBonus) == 0 {
|
||||
return decimal.Zero
|
||||
}
|
||||
|
||||
// 按充值金额从高到低排序,找到第一个匹配的赠送规则
|
||||
// 由于配置中规则是按充值金额从低到高排列的,我们需要反向遍历
|
||||
for i := len(walletConfig.AliPayRechargeBonus) - 1; i >= 0; i-- {
|
||||
rule := walletConfig.AliPayRechargeBonus[i]
|
||||
if rechargeAmount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) {
|
||||
return decimal.NewFromFloat(rule.BonusAmount)
|
||||
}
|
||||
}
|
||||
|
||||
return decimal.Zero
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 充值成功处理
|
||||
|
||||
在 `HandleAlipayPaymentSuccess` 方法中实现:
|
||||
|
||||
1. **计算赠送金额**:根据充值金额计算应赠送的金额
|
||||
2. **创建赠送记录**:如果有赠送金额,创建一条赠送类型的充值记录
|
||||
3. **更新钱包余额**:将充值金额和赠送金额的总和添加到用户钱包
|
||||
|
||||
```go
|
||||
// 计算充值赠送金额
|
||||
bonusAmount := calculateAlipayRechargeBonus(amount, &s.cfg.Wallet)
|
||||
totalAmount := amount.Add(bonusAmount)
|
||||
|
||||
// 如果有赠送金额,创建赠送充值记录
|
||||
if bonusAmount.GreaterThan(decimal.Zero) {
|
||||
giftRechargeRecord := entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送")
|
||||
_, err = s.rechargeRecordRepo.Create(txCtx, *giftRechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建赠送充值记录失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额(包含赠送金额)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalAmount)
|
||||
```
|
||||
|
||||
## 数据库记录
|
||||
|
||||
### 1. 充值记录表 (recharge_records)
|
||||
|
||||
每次支付宝充值成功会产生两条记录:
|
||||
|
||||
1. **支付宝充值记录**:
|
||||
- `recharge_type`: "alipay"
|
||||
- `amount`: 实际充值金额
|
||||
- `status`: "success"
|
||||
|
||||
2. **赠送充值记录**(如果有赠送):
|
||||
- `recharge_type`: "gift"
|
||||
- `amount`: 赠送金额
|
||||
- `notes`: "充值活动赠送"
|
||||
- `status`: "success"
|
||||
|
||||
### 2. 钱包余额
|
||||
|
||||
钱包余额会更新为:`原余额 + 充值金额 + 赠送金额`
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 1. 单元测试
|
||||
|
||||
运行测试命令:
|
||||
```bash
|
||||
go test ./internal/domains/finance/services -v
|
||||
```
|
||||
|
||||
测试覆盖的场景:
|
||||
- 充值500元,无赠送
|
||||
- 充值1000元,赠送50元
|
||||
- 充值2000元,赠送50元
|
||||
- 充值5000元,赠送300元
|
||||
- 充值8000元,赠送300元
|
||||
- 充值10000元,赠送800元
|
||||
- 充值15000元,赠送800元
|
||||
|
||||
### 2. 配置测试
|
||||
|
||||
运行配置测试:
|
||||
```bash
|
||||
go test ./internal/config -v
|
||||
```
|
||||
|
||||
验证配置文件中的赠送规则是否正确加载。
|
||||
|
||||
## 日志记录
|
||||
|
||||
系统会记录详细的日志信息:
|
||||
|
||||
```
|
||||
支付宝支付成功回调处理成功
|
||||
- user_id: 用户ID
|
||||
- recharge_amount: 充值金额
|
||||
- bonus_amount: 赠送金额
|
||||
- total_amount: 总金额(充值+赠送)
|
||||
- out_trade_no: 支付宝订单号
|
||||
- trade_no: 支付宝交易号
|
||||
- recharge_id: 充值记录ID
|
||||
- order_id: 支付宝订单ID
|
||||
```
|
||||
|
||||
## 扩展说明
|
||||
|
||||
### 1. 添加新的赠送规则
|
||||
|
||||
在配置文件中添加新的规则:
|
||||
|
||||
```yaml
|
||||
wallet:
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 1000.00
|
||||
bonus_amount: 50.00
|
||||
- recharge_amount: 5000.00
|
||||
bonus_amount: 300.00
|
||||
- recharge_amount: 10000.00
|
||||
bonus_amount: 800.00
|
||||
- recharge_amount: 20000.00 # 新增规则
|
||||
bonus_amount: 2000.00 # 充值20000元赠送2000元
|
||||
```
|
||||
|
||||
### 2. 修改赠送规则
|
||||
|
||||
直接修改配置文件中的 `recharge_amount` 和 `bonus_amount` 值即可。
|
||||
|
||||
### 3. 禁用赠送功能
|
||||
|
||||
将配置文件中的 `alipay_recharge_bonus` 设置为空数组:
|
||||
|
||||
```yaml
|
||||
wallet:
|
||||
alipay_recharge_bonus: []
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **事务保护**:所有数据库操作都在事务中执行,确保数据一致性
|
||||
2. **重复处理防护**:系统会检查订单状态,避免重复处理
|
||||
3. **精确计算**:使用 `decimal.Decimal` 进行金额计算,避免浮点数精度问题
|
||||
4. **日志记录**:详细记录所有操作,便于问题排查
|
||||
5. **配置热更新**:修改配置后需要重启服务才能生效
|
||||
@@ -174,14 +174,13 @@ func (s *ApiApplicationServiceImpl) CallApi(ctx context.Context, cmd *commands.A
|
||||
}
|
||||
// apiCall.ResponseData = &encryptedResponse
|
||||
|
||||
// 8. 更新订阅使用次数
|
||||
subscription.IncrementAPIUsage(1)
|
||||
err = s.productSubscriptionService.SaveSubscription(txCtx, subscription)
|
||||
if err != nil {
|
||||
s.logger.Error("保存订阅失败", zap.Error(err))
|
||||
businessError = ErrSystem
|
||||
return ErrSystem
|
||||
}
|
||||
// 8. 更新订阅使用次数(使用乐观锁)
|
||||
// err = s.productSubscriptionService.IncrementSubscriptionAPIUsage(txCtx, subscription.ID, 1)
|
||||
// if err != nil {
|
||||
// s.logger.Error("更新订阅使用次数失败", zap.Error(err))
|
||||
// businessError = ErrSystem
|
||||
// return ErrSystem
|
||||
// }
|
||||
|
||||
// 9. 扣钱
|
||||
err = s.walletService.Deduct(txCtx, apiUser.UserId, subscription.Price, apiCall.ID, transactionId, product.ID)
|
||||
|
||||
@@ -102,6 +102,13 @@ type AlipayRechargeOrderResponse struct {
|
||||
|
||||
// RechargeConfigResponse 充值配置响应
|
||||
type RechargeConfigResponse struct {
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
// AlipayRechargeBonusRuleResponse 支付宝充值赠送规则响应
|
||||
type AlipayRechargeBonusRuleResponse struct {
|
||||
RechargeAmount float64 `json:"recharge_amount"`
|
||||
BonusAmount float64 `json:"bonus_amount"`
|
||||
}
|
||||
|
||||
@@ -113,15 +113,15 @@ func (s *FinanceApplicationServiceImpl) CreateAlipayRechargeOrder(ctx context.Co
|
||||
}
|
||||
|
||||
// 从配置中获取充值限制
|
||||
minAmount, err := decimal.NewFromString(s.config.Recharge.MinAmount)
|
||||
minAmount, err := decimal.NewFromString(s.config.Wallet.MinAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("配置中的最低充值金额格式错误", zap.String("min_amount", s.config.Recharge.MinAmount), zap.Error(err))
|
||||
s.logger.Error("配置中的最低充值金额格式错误", zap.String("min_amount", s.config.Wallet.MinAmount), zap.Error(err))
|
||||
return nil, fmt.Errorf("系统配置错误: %w", err)
|
||||
}
|
||||
|
||||
maxAmount, err := decimal.NewFromString(s.config.Recharge.MaxAmount)
|
||||
maxAmount, err := decimal.NewFromString(s.config.Wallet.MaxAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("配置中的最高充值金额格式错误", zap.String("max_amount", s.config.Recharge.MaxAmount), zap.Error(err))
|
||||
s.logger.Error("配置中的最高充值金额格式错误", zap.String("max_amount", s.config.Wallet.MaxAmount), zap.Error(err))
|
||||
return nil, fmt.Errorf("系统配置错误: %w", err)
|
||||
}
|
||||
|
||||
@@ -643,8 +643,16 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
|
||||
|
||||
// GetRechargeConfig 获取充值配置
|
||||
func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error) {
|
||||
bonus := make([]responses.AlipayRechargeBonusRuleResponse, 0, len(s.config.Wallet.AliPayRechargeBonus))
|
||||
for _, rule := range s.config.Wallet.AliPayRechargeBonus {
|
||||
bonus = append(bonus, responses.AlipayRechargeBonusRuleResponse{
|
||||
RechargeAmount: rule.RechargeAmount,
|
||||
BonusAmount: rule.BonusAmount,
|
||||
})
|
||||
}
|
||||
return &responses.RechargeConfigResponse{
|
||||
MinAmount: s.config.Recharge.MinAmount,
|
||||
MaxAmount: s.config.Recharge.MaxAmount,
|
||||
MinAmount: s.config.Wallet.MinAmount,
|
||||
MaxAmount: s.config.Wallet.MaxAmount,
|
||||
AlipayRechargeBonus: bonus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package product
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/product/dto/commands"
|
||||
"tyapi-server/internal/application/product/dto/responses"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/services"
|
||||
)
|
||||
|
||||
// DocumentationApplicationServiceInterface 文档应用服务接口
|
||||
type DocumentationApplicationServiceInterface interface {
|
||||
// CreateDocumentation 创建文档
|
||||
CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error)
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error)
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error)
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error)
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
DeleteDocumentation(ctx context.Context, id string) error
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error)
|
||||
}
|
||||
|
||||
// DocumentationApplicationService 文档应用服务
|
||||
type DocumentationApplicationService struct {
|
||||
docService *services.ProductDocumentationService
|
||||
}
|
||||
|
||||
// NewDocumentationApplicationService 创建文档应用服务实例
|
||||
func NewDocumentationApplicationService(docService *services.ProductDocumentationService) *DocumentationApplicationService {
|
||||
return &DocumentationApplicationService{
|
||||
docService: docService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDocumentation 创建文档
|
||||
func (s *DocumentationApplicationService) CreateDocumentation(ctx context.Context, cmd *commands.CreateDocumentationCommand) (*responses.DocumentationResponse, error) {
|
||||
// 创建文档实体
|
||||
doc := &entities.ProductDocumentation{
|
||||
RequestURL: cmd.RequestURL,
|
||||
RequestMethod: cmd.RequestMethod,
|
||||
BasicInfo: cmd.BasicInfo,
|
||||
RequestParams: cmd.RequestParams,
|
||||
ResponseFields: cmd.ResponseFields,
|
||||
ResponseExample: cmd.ResponseExample,
|
||||
ErrorCodes: cmd.ErrorCodes,
|
||||
}
|
||||
|
||||
// 调用领域服务创建文档
|
||||
err := s.docService.CreateDocumentation(ctx, cmd.ProductID, doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
func (s *DocumentationApplicationService) UpdateDocumentation(ctx context.Context, id string, cmd *commands.UpdateDocumentationCommand) (*responses.DocumentationResponse, error) {
|
||||
// 调用领域服务更新文档
|
||||
err := s.docService.UpdateDocumentation(ctx, id,
|
||||
cmd.RequestURL,
|
||||
cmd.RequestMethod,
|
||||
cmd.BasicInfo,
|
||||
cmd.RequestParams,
|
||||
cmd.ResponseFields,
|
||||
cmd.ResponseExample,
|
||||
cmd.ErrorCodes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取更新后的文档
|
||||
doc, err := s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentation(ctx context.Context, id string) (*responses.DocumentationResponse, error) {
|
||||
doc, err := s.docService.GetDocumentation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentationByProductID(ctx context.Context, productID string) (*responses.DocumentationResponse, error) {
|
||||
doc, err := s.docService.GetDocumentationByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
resp := responses.NewDocumentationResponse(doc)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
func (s *DocumentationApplicationService) DeleteDocumentation(ctx context.Context, id string) error {
|
||||
return s.docService.DeleteDocumentation(ctx, id)
|
||||
}
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
func (s *DocumentationApplicationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]responses.DocumentationResponse, error) {
|
||||
docs, err := s.docService.GetDocumentationsByProductIDs(ctx, productIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var docResponses []responses.DocumentationResponse
|
||||
for _, doc := range docs {
|
||||
docResponses = append(docResponses, responses.NewDocumentationResponse(doc))
|
||||
}
|
||||
|
||||
return docResponses, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package commands
|
||||
|
||||
// CreateDocumentationCommand 创建文档命令
|
||||
type CreateDocumentationCommand struct {
|
||||
ProductID string `json:"product_id" binding:"required" validate:"required"`
|
||||
RequestURL string `json:"request_url" binding:"required" validate:"required"`
|
||||
RequestMethod string `json:"request_method" binding:"required" validate:"required"`
|
||||
BasicInfo string `json:"basic_info" validate:"required"`
|
||||
RequestParams string `json:"request_params" validate:"required"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
}
|
||||
|
||||
// UpdateDocumentationCommand 更新文档命令
|
||||
type UpdateDocumentationCommand struct {
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
BasicInfo string `json:"basic_info"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
}
|
||||
@@ -36,6 +36,13 @@ type GetProductQuery struct {
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
}
|
||||
|
||||
// GetProductDetailQuery 获取产品详情查询(支持可选文档)
|
||||
type GetProductDetailQuery struct {
|
||||
ID string `uri:"id" binding:"omitempty,uuid" comment:"产品ID"`
|
||||
Code string `form:"code" binding:"omitempty,product_code" comment:"产品编号"`
|
||||
WithDocument *bool `form:"with_document" comment:"是否包含文档信息"`
|
||||
}
|
||||
|
||||
// GetProductsByIDsQuery 根据ID列表获取产品查询
|
||||
type GetProductsByIDsQuery struct {
|
||||
IDs []string `form:"ids" binding:"required,dive,uuid" comment:"产品ID列表"`
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// DocumentationResponse 文档响应
|
||||
type DocumentationResponse struct {
|
||||
ID string `json:"id"`
|
||||
ProductID string `json:"product_id"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestMethod string `json:"request_method"`
|
||||
BasicInfo string `json:"basic_info"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseFields string `json:"response_fields"`
|
||||
ResponseExample string `json:"response_example"`
|
||||
ErrorCodes string `json:"error_codes"`
|
||||
Version string `json:"version"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// NewDocumentationResponse 从实体创建响应
|
||||
func NewDocumentationResponse(doc *entities.ProductDocumentation) DocumentationResponse {
|
||||
return DocumentationResponse{
|
||||
ID: doc.ID,
|
||||
ProductID: doc.ProductID,
|
||||
RequestURL: doc.RequestURL,
|
||||
RequestMethod: doc.RequestMethod,
|
||||
BasicInfo: doc.BasicInfo,
|
||||
RequestParams: doc.RequestParams,
|
||||
ResponseFields: doc.ResponseFields,
|
||||
ResponseExample: doc.ResponseExample,
|
||||
ErrorCodes: doc.ErrorCodes,
|
||||
Version: doc.Version,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -100,10 +100,19 @@ type ProductAdminInfoResponse struct {
|
||||
// 组合包信息
|
||||
PackageItems []*PackageItemResponse `json:"package_items,omitempty" comment:"组合包项目列表"`
|
||||
|
||||
// 文档信息
|
||||
Documentation *DocumentationResponse `json:"documentation,omitempty" comment:"产品文档"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" comment:"更新时间"`
|
||||
}
|
||||
|
||||
// ProductInfoWithDocumentResponse 包含文档的产品详情响应
|
||||
type ProductInfoWithDocumentResponse struct {
|
||||
ProductInfoResponse
|
||||
Documentation *DocumentationResponse `json:"documentation,omitempty" comment:"产品文档"`
|
||||
}
|
||||
|
||||
// ProductAdminListResponse 管理员产品列表响应
|
||||
type ProductAdminListResponse struct {
|
||||
Total int64 `json:"total" comment:"总数"`
|
||||
|
||||
@@ -22,10 +22,10 @@ type ProductApplicationService interface {
|
||||
|
||||
// 管理员专用方法
|
||||
ListProductsForAdmin(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductAdminListResponse, error)
|
||||
GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductAdminInfoResponse, error)
|
||||
GetProductByIDForAdmin(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductAdminInfoResponse, error)
|
||||
|
||||
// 用户端专用方法
|
||||
GetProductByIDForUser(ctx context.Context, query *queries.GetProductQuery) (*responses.ProductInfoResponse, error)
|
||||
GetProductByIDForUser(ctx context.Context, query *queries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error)
|
||||
|
||||
// 业务查询
|
||||
GetSubscribableProducts(ctx context.Context, query *queries.GetSubscribableProductsQuery) ([]*responses.ProductInfoResponse, error)
|
||||
|
||||
@@ -21,6 +21,7 @@ type ProductApplicationServiceImpl struct {
|
||||
productManagementService *product_service.ProductManagementService
|
||||
productSubscriptionService *product_service.ProductSubscriptionService
|
||||
productApiConfigAppService ProductApiConfigApplicationService
|
||||
documentationAppService DocumentationApplicationServiceInterface
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -29,12 +30,14 @@ func NewProductApplicationService(
|
||||
productManagementService *product_service.ProductManagementService,
|
||||
productSubscriptionService *product_service.ProductSubscriptionService,
|
||||
productApiConfigAppService ProductApiConfigApplicationService,
|
||||
documentationAppService DocumentationApplicationServiceInterface,
|
||||
logger *zap.Logger,
|
||||
) ProductApplicationService {
|
||||
return &ProductApplicationServiceImpl{
|
||||
productManagementService: productManagementService,
|
||||
productSubscriptionService: productSubscriptionService,
|
||||
productApiConfigAppService: productApiConfigAppService,
|
||||
documentationAppService: documentationAppService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -411,18 +414,28 @@ func (s *ProductApplicationServiceImpl) ListProductsForAdmin(ctx context.Context
|
||||
|
||||
// GetProductByIDForAdmin 根据ID获取产品(管理员专用)
|
||||
// 业务流程:1. 获取产品信息 2. 构建管理员响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetProductByIDForAdmin(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductAdminInfoResponse, error) {
|
||||
func (s *ProductApplicationServiceImpl) GetProductByIDForAdmin(ctx context.Context, query *appQueries.GetProductDetailQuery) (*responses.ProductAdminInfoResponse, error) {
|
||||
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.convertToProductAdminInfoResponse(product), nil
|
||||
response := s.convertToProductAdminInfoResponse(product)
|
||||
|
||||
// 如果需要包含文档信息
|
||||
if query.WithDocument != nil && *query.WithDocument {
|
||||
doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, query.ID)
|
||||
if err == nil && doc != nil {
|
||||
response.Documentation = doc
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// GetProductByIDForUser 根据ID获取产品(用户端专用)
|
||||
// 业务流程:1. 获取产品信息 2. 验证产品可见性 3. 构建用户响应数据
|
||||
func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductQuery) (*responses.ProductInfoResponse, error) {
|
||||
func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Context, query *appQueries.GetProductDetailQuery) (*responses.ProductInfoWithDocumentResponse, error) {
|
||||
product, err := s.productManagementService.GetProductWithCategory(ctx, query.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -433,7 +446,19 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex
|
||||
return nil, fmt.Errorf("产品不存在或不可见")
|
||||
}
|
||||
|
||||
return s.convertToProductInfoResponse(product), nil
|
||||
response := &responses.ProductInfoWithDocumentResponse{
|
||||
ProductInfoResponse: *s.convertToProductInfoResponse(product),
|
||||
}
|
||||
|
||||
// 如果需要包含文档信息
|
||||
if query.WithDocument != nil && *query.WithDocument {
|
||||
doc, err := s.documentationAppService.GetDocumentationByProductID(ctx, query.ID)
|
||||
if err == nil && doc != nil {
|
||||
response.Documentation = doc
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// convertToProductInfoResponse 转换为产品信息响应
|
||||
|
||||
@@ -27,10 +27,8 @@ type Config struct {
|
||||
Wallet WalletConfig `mapstructure:"wallet"`
|
||||
WestDex WestDexConfig `mapstructure:"westdex"`
|
||||
AliPay AliPayConfig `mapstructure:"alipay"`
|
||||
Recharge RechargeConfig `mapstructure:"recharge"`
|
||||
Yushan YushanConfig `mapstructure:"yushan"`
|
||||
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
|
||||
Domain DomainConfig `mapstructure:"domain"`
|
||||
}
|
||||
|
||||
// ServerConfig HTTP服务器配置
|
||||
@@ -275,7 +273,16 @@ type SignConfig struct {
|
||||
|
||||
// WalletConfig 钱包配置
|
||||
type WalletConfig struct {
|
||||
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
|
||||
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
// AliPayRechargeBonusRule 支付宝充值赠送规则
|
||||
type AliPayRechargeBonusRule struct {
|
||||
RechargeAmount float64 `mapstructure:"recharge_amount"` // 充值金额
|
||||
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
|
||||
}
|
||||
|
||||
// WestDexConfig WestDex配置
|
||||
@@ -296,12 +303,6 @@ type AliPayConfig struct {
|
||||
ReturnURL string `mapstructure:"return_url"`
|
||||
}
|
||||
|
||||
// RechargeConfig 充值配置
|
||||
type RechargeConfig struct {
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
}
|
||||
|
||||
// YushanConfig 羽山配置
|
||||
type YushanConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
|
||||
53
internal/config/config_test.go
Normal file
53
internal/config/config_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWalletConfig_AlipayRechargeBonus(t *testing.T) {
|
||||
// 切换到项目根目录,这样配置加载器就能找到配置文件
|
||||
originalWd, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 切换到项目根目录(从 internal/config 目录向上两级)
|
||||
err = os.Chdir("../../")
|
||||
assert.NoError(t, err)
|
||||
defer os.Chdir(originalWd) // 测试结束后恢复原目录
|
||||
|
||||
// 加载配置
|
||||
cfg, err := LoadConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
|
||||
// 验证钱包配置
|
||||
assert.NotNil(t, cfg.Wallet)
|
||||
assert.Greater(t, len(cfg.Wallet.AliPayRechargeBonus), 0, "支付宝充值赠送规则不能为空")
|
||||
|
||||
// 验证具体的赠送规则
|
||||
expectedRules := []struct {
|
||||
rechargeAmount float64
|
||||
bonusAmount float64
|
||||
}{
|
||||
{1000.00, 50.00}, // 充1000送50
|
||||
{5000.00, 300.00}, // 充5000送300
|
||||
{10000.00, 800.00}, // 充10000送800
|
||||
}
|
||||
|
||||
for i, expected := range expectedRules {
|
||||
if i < len(cfg.Wallet.AliPayRechargeBonus) {
|
||||
rule := cfg.Wallet.AliPayRechargeBonus[i]
|
||||
assert.Equal(t, expected.rechargeAmount, rule.RechargeAmount,
|
||||
"充值金额不匹配,期望: %f, 实际: %f", expected.rechargeAmount, rule.RechargeAmount)
|
||||
assert.Equal(t, expected.bonusAmount, rule.BonusAmount,
|
||||
"赠送金额不匹配,期望: %f, 实际: %f", expected.bonusAmount, rule.BonusAmount)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("钱包配置加载成功,包含 %d 条支付宝充值赠送规则", len(cfg.Wallet.AliPayRechargeBonus))
|
||||
for i, rule := range cfg.Wallet.AliPayRechargeBonus {
|
||||
t.Logf("规则 %d: 充值 %.2f 元,赠送 %.2f 元", i+1, rule.RechargeAmount, rule.BonusAmount)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,12 @@ import (
|
||||
|
||||
// SetupGormCache 设置GORM缓存插件
|
||||
func SetupGormCache(db *gorm.DB, cacheService interfaces.CacheService, cfg *config.Config, logger *zap.Logger) error {
|
||||
// 缓存功能已完全禁用
|
||||
logger.Info("GORM缓存插件已禁用 - 所有查询将直接访问数据库")
|
||||
return nil
|
||||
|
||||
// 以下是原有的缓存配置代码,已注释掉
|
||||
/*
|
||||
// 创建缓存配置
|
||||
cacheConfig := cache.CacheConfig{
|
||||
DefaultTTL: 30 * time.Minute,
|
||||
@@ -29,16 +35,16 @@ func SetupGormCache(db *gorm.DB, cacheService interfaces.CacheService, cfg *conf
|
||||
|
||||
// 配置启用缓存的表
|
||||
EnabledTables: []string{
|
||||
"product",
|
||||
"product_category",
|
||||
"enterprise_info_submit_records",
|
||||
"sms_codes",
|
||||
"wallets",
|
||||
"subscription",
|
||||
"product_category",
|
||||
"product_documentation",
|
||||
"enterprise_infos",
|
||||
"api_users",
|
||||
// "product",
|
||||
// "product_category",
|
||||
// "enterprise_info_submit_records",
|
||||
// "sms_codes",
|
||||
// "wallets",
|
||||
// "subscription",
|
||||
// "product_category",
|
||||
// "product_documentation",
|
||||
// "enterprise_infos",
|
||||
// "api_users",
|
||||
// 添加更多需要缓存的表
|
||||
},
|
||||
|
||||
@@ -68,8 +74,9 @@ func SetupGormCache(db *gorm.DB, cacheService interfaces.CacheService, cfg *conf
|
||||
zap.Strings("enabled_tables", cacheConfig.EnabledTables),
|
||||
zap.Strings("disabled_tables", cacheConfig.DisabledTables),
|
||||
)
|
||||
*/
|
||||
|
||||
return nil
|
||||
// return nil
|
||||
}
|
||||
|
||||
// GetCacheConfig 根据环境获取缓存配置
|
||||
@@ -89,10 +96,10 @@ func GetCacheConfig(cfg *config.Config) cache.CacheConfig {
|
||||
InvalidateDelay: 50 * time.Millisecond,
|
||||
|
||||
EnabledTables: []string{
|
||||
"product",
|
||||
"product_category",
|
||||
"enterprise_info_submit_records",
|
||||
"sms_codes",
|
||||
// "product",
|
||||
// "product_category",
|
||||
// "enterprise_info_submit_records",
|
||||
// "sms_codes",
|
||||
},
|
||||
|
||||
DisabledTables: []string{
|
||||
@@ -120,10 +127,10 @@ func GetCacheConfig(cfg *config.Config) cache.CacheConfig {
|
||||
InvalidateDelay: 200 * time.Millisecond,
|
||||
|
||||
EnabledTables: []string{
|
||||
"product",
|
||||
"product_category",
|
||||
"enterprise_info_submit_records",
|
||||
"sms_codes",
|
||||
// "product",
|
||||
// "product_category",
|
||||
// "enterprise_info_submit_records",
|
||||
// "sms_codes",
|
||||
},
|
||||
|
||||
DisabledTables: []string{
|
||||
|
||||
@@ -77,12 +77,29 @@ func NewContainer() *Container {
|
||||
|
||||
// 基础设施模块
|
||||
fx.Provide(
|
||||
// 日志器 - 提供自定义Logger和*zap.Logger
|
||||
func(cfg *config.Config) (logger.Logger, error) {
|
||||
if cfg.Logger.EnableLevelSeparation {
|
||||
// 使用按级别分文件的日志器
|
||||
levelConfig := logger.LevelLoggerConfig{
|
||||
BaseConfig: logger.Config{
|
||||
// 日志器 - 提供自定义Logger和*zap.Logger
|
||||
func(cfg *config.Config) (logger.Logger, error) {
|
||||
if cfg.Logger.EnableLevelSeparation {
|
||||
// 使用按级别分文件的日志器
|
||||
levelConfig := logger.LevelLoggerConfig{
|
||||
BaseConfig: logger.Config{
|
||||
Level: cfg.Logger.Level,
|
||||
Format: cfg.Logger.Format,
|
||||
Output: cfg.Logger.Output,
|
||||
LogDir: cfg.Logger.LogDir,
|
||||
MaxSize: cfg.Logger.MaxSize,
|
||||
MaxBackups: cfg.Logger.MaxBackups,
|
||||
MaxAge: cfg.Logger.MaxAge,
|
||||
Compress: cfg.Logger.Compress,
|
||||
UseDaily: cfg.Logger.UseDaily,
|
||||
},
|
||||
EnableLevelSeparation: true,
|
||||
LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs),
|
||||
}
|
||||
return logger.NewLevelLogger(levelConfig)
|
||||
} else {
|
||||
// 使用普通日志器
|
||||
logCfg := logger.Config{
|
||||
Level: cfg.Logger.Level,
|
||||
Format: cfg.Logger.Format,
|
||||
Output: cfg.Logger.Output,
|
||||
@@ -92,44 +109,27 @@ func NewContainer() *Container {
|
||||
MaxAge: cfg.Logger.MaxAge,
|
||||
Compress: cfg.Logger.Compress,
|
||||
UseDaily: cfg.Logger.UseDaily,
|
||||
},
|
||||
EnableLevelSeparation: true,
|
||||
LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs),
|
||||
}
|
||||
return logger.NewLogger(logCfg)
|
||||
}
|
||||
return logger.NewLevelLogger(levelConfig)
|
||||
} else {
|
||||
// 使用普通日志器
|
||||
logCfg := logger.Config{
|
||||
Level: cfg.Logger.Level,
|
||||
Format: cfg.Logger.Format,
|
||||
Output: cfg.Logger.Output,
|
||||
LogDir: cfg.Logger.LogDir,
|
||||
MaxSize: cfg.Logger.MaxSize,
|
||||
MaxBackups: cfg.Logger.MaxBackups,
|
||||
MaxAge: cfg.Logger.MaxAge,
|
||||
Compress: cfg.Logger.Compress,
|
||||
UseDaily: cfg.Logger.UseDaily,
|
||||
},
|
||||
// 提供普通的*zap.Logger(用于大多数场景)
|
||||
func(log logger.Logger) *zap.Logger {
|
||||
// 尝试转换为ZapLogger
|
||||
if zapLogger, ok := log.(*logger.ZapLogger); ok {
|
||||
return zapLogger.GetZapLogger()
|
||||
}
|
||||
return logger.NewLogger(logCfg)
|
||||
}
|
||||
},
|
||||
// 提供普通的*zap.Logger(用于大多数场景)
|
||||
func(log logger.Logger) *zap.Logger {
|
||||
// 尝试转换为ZapLogger
|
||||
if zapLogger, ok := log.(*logger.ZapLogger); ok {
|
||||
return zapLogger.GetZapLogger()
|
||||
}
|
||||
// 尝试转换为LevelLogger
|
||||
if levelLogger, ok := log.(*logger.LevelLogger); ok {
|
||||
// 获取Info级别的日志器作为默认
|
||||
if infoLogger := levelLogger.GetLevelLogger(zapcore.InfoLevel); infoLogger != nil {
|
||||
return infoLogger
|
||||
// 尝试转换为LevelLogger
|
||||
if levelLogger, ok := log.(*logger.LevelLogger); ok {
|
||||
// 获取Info级别的日志器作为默认
|
||||
if infoLogger := levelLogger.GetLevelLogger(zapcore.InfoLevel); infoLogger != nil {
|
||||
return infoLogger
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果类型转换失败,创建一个默认的zap logger
|
||||
defaultLogger, _ := zap.NewProduction()
|
||||
return defaultLogger
|
||||
},
|
||||
// 如果类型转换失败,创建一个默认的zap logger
|
||||
defaultLogger, _ := zap.NewProduction()
|
||||
return defaultLogger
|
||||
},
|
||||
|
||||
// 数据库连接
|
||||
func(cfg *config.Config, cacheService interfaces.CacheService, logger *zap.Logger) (*gorm.DB, error) {
|
||||
@@ -147,7 +147,7 @@ func NewContainer() *Container {
|
||||
}
|
||||
db, err := database.NewConnection(dbCfg)
|
||||
if err != nil {
|
||||
logger.Error("数据库连接失败",
|
||||
logger.Error("数据库连接失败",
|
||||
zap.String("host", cfg.Database.Host),
|
||||
zap.String("port", cfg.Database.Port),
|
||||
zap.String("database", cfg.Database.Name),
|
||||
@@ -155,8 +155,8 @@ func NewContainer() *Container {
|
||||
zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("数据库连接成功",
|
||||
|
||||
logger.Info("数据库连接成功",
|
||||
zap.String("host", cfg.Database.Host),
|
||||
zap.String("port", cfg.Database.Port),
|
||||
zap.String("database", cfg.Database.Name))
|
||||
@@ -445,6 +445,10 @@ func NewContainer() *Container {
|
||||
product_repo.NewGormProductApiConfigRepository,
|
||||
fx.As(new(domain_product_repo.ProductApiConfigRepository)),
|
||||
),
|
||||
fx.Annotate(
|
||||
product_repo.NewGormProductDocumentationRepository,
|
||||
fx.As(new(domain_product_repo.ProductDocumentationRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// API域仓储层
|
||||
@@ -468,6 +472,7 @@ func NewContainer() *Container {
|
||||
product_service.NewProductManagementService,
|
||||
product_service.NewProductSubscriptionService,
|
||||
product_service.NewProductApiConfigService,
|
||||
product_service.NewProductDocumentationService,
|
||||
finance_service.NewWalletAggregateService,
|
||||
finance_service.NewRechargeRecordService,
|
||||
certification_service.NewCertificationAggregateService,
|
||||
@@ -518,6 +523,10 @@ func NewContainer() *Container {
|
||||
product.NewCategoryApplicationService,
|
||||
fx.As(new(product.CategoryApplicationService)),
|
||||
),
|
||||
fx.Annotate(
|
||||
product.NewDocumentationApplicationService,
|
||||
fx.As(new(product.DocumentationApplicationServiceInterface)),
|
||||
),
|
||||
// 订阅应用服务 - 绑定到接口
|
||||
fx.Annotate(
|
||||
product.NewSubscriptionApplicationService,
|
||||
@@ -670,7 +679,7 @@ func RegisterRoutes(
|
||||
logger.Info("HTTP服务器启动成功", zap.String("addr", addr))
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
logger.Info("路由注册完成,HTTP服务器启动中...")
|
||||
}
|
||||
|
||||
@@ -691,7 +700,7 @@ func NewRequestBodyLoggerMiddlewareWrapper(logger *zap.Logger, cfg *config.Confi
|
||||
// convertLevelConfigs 转换级别配置
|
||||
func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.Level]logger.LevelFileConfig {
|
||||
result := make(map[zapcore.Level]logger.LevelFileConfig)
|
||||
|
||||
|
||||
levelMap := map[string]zapcore.Level{
|
||||
"debug": zapcore.DebugLevel,
|
||||
"info": zapcore.InfoLevel,
|
||||
@@ -700,7 +709,7 @@ func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.
|
||||
"fatal": zapcore.FatalLevel,
|
||||
"panic": zapcore.PanicLevel,
|
||||
}
|
||||
|
||||
|
||||
for levelStr, config := range configs {
|
||||
if level, exists := levelMap[levelStr]; exists {
|
||||
result[level] = logger.LevelFileConfig{
|
||||
@@ -711,7 +720,7 @@ func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ package flxg
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
)
|
||||
|
||||
// ProcessFLXG0V3Bequest FLXG0V3B API处理方法
|
||||
@@ -32,20 +31,27 @@ func ProcessFLXG0V3Bequest(ctx context.Context, params []byte, deps *processors.
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"id_card": encryptedIDCard,
|
||||
},
|
||||
// mock 1秒,不用真实请求
|
||||
// 模拟耗时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
|
||||
respBytes, err := deps.WestDexService.CallAPI("G34BJ03", reqData)
|
||||
// 构造模拟响应
|
||||
mockResp := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "mock success",
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"id_card": encryptedIDCard,
|
||||
"result": "mocked",
|
||||
},
|
||||
}
|
||||
respBytes, err := json.Marshal(mockResp)
|
||||
if err != nil {
|
||||
if errors.Is(err, westdex.ErrDatasource) {
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
|
||||
@@ -7,12 +7,31 @@ import (
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
"tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// calculateAlipayRechargeBonus 计算支付宝充值赠送金额
|
||||
func calculateAlipayRechargeBonus(rechargeAmount decimal.Decimal, walletConfig *config.WalletConfig) decimal.Decimal {
|
||||
if walletConfig == nil || len(walletConfig.AliPayRechargeBonus) == 0 {
|
||||
return decimal.Zero
|
||||
}
|
||||
|
||||
// 按充值金额从高到低排序,找到第一个匹配的赠送规则
|
||||
// 由于配置中规则是按充值金额从低到高排列的,我们需要反向遍历
|
||||
for i := len(walletConfig.AliPayRechargeBonus) - 1; i >= 0; i-- {
|
||||
rule := walletConfig.AliPayRechargeBonus[i]
|
||||
if rechargeAmount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) {
|
||||
return decimal.NewFromFloat(rule.BonusAmount)
|
||||
}
|
||||
}
|
||||
|
||||
return decimal.Zero
|
||||
}
|
||||
|
||||
// RechargeRecordService 充值记录服务接口
|
||||
type RechargeRecordService interface {
|
||||
// 对公转账充值
|
||||
@@ -47,6 +66,7 @@ type RechargeRecordServiceImpl struct {
|
||||
walletService WalletAggregateService
|
||||
txManager *database.TransactionManager
|
||||
logger *zap.Logger
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewRechargeRecordService(
|
||||
@@ -56,6 +76,7 @@ func NewRechargeRecordService(
|
||||
walletService WalletAggregateService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) RechargeRecordService {
|
||||
return &RechargeRecordServiceImpl{
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
@@ -64,6 +85,7 @@ func NewRechargeRecordService(
|
||||
walletService: walletService,
|
||||
txManager: txManager,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +295,10 @@ func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Conte
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算充值赠送金额
|
||||
bonusAmount := calculateAlipayRechargeBonus(amount, &s.cfg.Wallet)
|
||||
totalAmount := amount.Add(bonusAmount)
|
||||
|
||||
// 在事务中执行所有更新操作
|
||||
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
|
||||
// 更新支付宝订单状态为成功
|
||||
@@ -291,8 +317,22 @@ func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Conte
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, amount)
|
||||
// 如果有赠送金额,创建赠送充值记录
|
||||
if bonusAmount.GreaterThan(decimal.Zero) {
|
||||
giftRechargeRecord := entities.NewGiftRechargeRecord(rechargeRecord.UserID, bonusAmount, "充值活动赠送")
|
||||
_, err = s.rechargeRecordRepo.Create(txCtx, *giftRechargeRecord)
|
||||
if err != nil {
|
||||
s.logger.Error("创建赠送充值记录失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
s.logger.Info("创建赠送充值记录成功",
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("gift_recharge_id", giftRechargeRecord.ID))
|
||||
}
|
||||
|
||||
// 使用钱包聚合服务更新钱包余额(包含赠送金额)
|
||||
err = s.walletService.Recharge(txCtx, rechargeRecord.UserID, totalAmount)
|
||||
if err != nil {
|
||||
s.logger.Error("更新钱包余额失败", zap.String("user_id", rechargeRecord.UserID), zap.Error(err))
|
||||
return err
|
||||
@@ -307,7 +347,9 @@ func (s *RechargeRecordServiceImpl) HandleAlipayPaymentSuccess(ctx context.Conte
|
||||
|
||||
s.logger.Info("支付宝支付成功回调处理成功",
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("recharge_amount", amount.String()),
|
||||
zap.String("bonus_amount", bonusAmount.String()),
|
||||
zap.String("total_amount", totalAmount.String()),
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", tradeNo),
|
||||
zap.String("recharge_id", rechargeRecord.ID),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
)
|
||||
|
||||
func TestCalculateAlipayRechargeBonus(t *testing.T) {
|
||||
// 创建测试配置
|
||||
walletConfig := &config.WalletConfig{
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{
|
||||
{RechargeAmount: 1000.00, BonusAmount: 50.00}, // 充1000送50
|
||||
{RechargeAmount: 5000.00, BonusAmount: 300.00}, // 充5000送300
|
||||
{RechargeAmount: 10000.00, BonusAmount: 800.00}, // 充10000送800
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rechargeAmount decimal.Decimal
|
||||
expectedBonus decimal.Decimal
|
||||
}{
|
||||
{
|
||||
name: "充值500元,无赠送",
|
||||
rechargeAmount: decimal.NewFromFloat(500.00),
|
||||
expectedBonus: decimal.Zero,
|
||||
},
|
||||
{
|
||||
name: "充值1000元,赠送50元",
|
||||
rechargeAmount: decimal.NewFromFloat(1000.00),
|
||||
expectedBonus: decimal.NewFromFloat(50.00),
|
||||
},
|
||||
{
|
||||
name: "充值2000元,赠送50元",
|
||||
rechargeAmount: decimal.NewFromFloat(2000.00),
|
||||
expectedBonus: decimal.NewFromFloat(50.00),
|
||||
},
|
||||
{
|
||||
name: "充值5000元,赠送300元",
|
||||
rechargeAmount: decimal.NewFromFloat(5000.00),
|
||||
expectedBonus: decimal.NewFromFloat(300.00),
|
||||
},
|
||||
{
|
||||
name: "充值8000元,赠送300元",
|
||||
rechargeAmount: decimal.NewFromFloat(8000.00),
|
||||
expectedBonus: decimal.NewFromFloat(300.00),
|
||||
},
|
||||
{
|
||||
name: "充值10000元,赠送800元",
|
||||
rechargeAmount: decimal.NewFromFloat(10000.00),
|
||||
expectedBonus: decimal.NewFromFloat(800.00),
|
||||
},
|
||||
{
|
||||
name: "充值15000元,赠送800元",
|
||||
rechargeAmount: decimal.NewFromFloat(15000.00),
|
||||
expectedBonus: decimal.NewFromFloat(800.00),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bonus := calculateAlipayRechargeBonus(tt.rechargeAmount, walletConfig)
|
||||
assert.True(t, bonus.Equal(tt.expectedBonus),
|
||||
"充值金额: %s, 期望赠送: %s, 实际赠送: %s",
|
||||
tt.rechargeAmount.String(), tt.expectedBonus.String(), bonus.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateAlipayRechargeBonus_EmptyConfig(t *testing.T) {
|
||||
// 测试空配置
|
||||
walletConfig := &config.WalletConfig{
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{},
|
||||
}
|
||||
|
||||
bonus := calculateAlipayRechargeBonus(decimal.NewFromFloat(1000.00), walletConfig)
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "空配置应该返回零赠送金额")
|
||||
|
||||
// 测试nil配置
|
||||
bonus = calculateAlipayRechargeBonus(decimal.NewFromFloat(1000.00), nil)
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "nil配置应该返回零赠送金额")
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ type Product struct {
|
||||
SEOKeywords string `gorm:"type:text" comment:"SEO关键词"`
|
||||
|
||||
// 关联关系
|
||||
Category *ProductCategory `gorm:"foreignKey:CategoryID" comment:"产品分类"`
|
||||
Category *ProductCategory `gorm:"foreignKey:CategoryID" comment:"产品分类"`
|
||||
Documentation *ProductDocumentation `gorm:"foreignKey:ProductID" comment:"产品文档"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -9,20 +12,20 @@ import (
|
||||
|
||||
// ProductDocumentation 产品文档实体
|
||||
type ProductDocumentation struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"文档ID"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;uniqueIndex" comment:"产品ID"`
|
||||
Title string `gorm:"type:varchar(200);not null" comment:"文档标题"`
|
||||
Content string `gorm:"type:text;not null" comment:"文档内容"`
|
||||
UsageGuide string `gorm:"type:text" comment:"使用指南"`
|
||||
APIDocs string `gorm:"type:text" comment:"API文档"`
|
||||
Examples string `gorm:"type:text" comment:"使用示例"`
|
||||
FAQ string `gorm:"type:text" comment:"常见问题"`
|
||||
Version string `gorm:"type:varchar(20);default:'1.0'" comment:"文档版本"`
|
||||
Published bool `gorm:"default:false" comment:"是否已发布"`
|
||||
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" comment:"文档ID"`
|
||||
ProductID string `gorm:"type:varchar(36);not null;uniqueIndex" comment:"产品ID"`
|
||||
RequestURL string `gorm:"type:varchar(500);not null" comment:"请求链接"`
|
||||
RequestMethod string `gorm:"type:varchar(20);not null" comment:"请求方法"`
|
||||
BasicInfo string `gorm:"type:text" comment:"基础说明(请求头配置、参数加密等)"`
|
||||
RequestParams string `gorm:"type:text" comment:"请求参数"`
|
||||
ResponseFields string `gorm:"type:text" comment:"返回字段说明"`
|
||||
ResponseExample string `gorm:"type:text" comment:"响应示例"`
|
||||
ErrorCodes string `gorm:"type:text" comment:"错误代码"`
|
||||
Version string `gorm:"type:varchar(20);default:'1.0'" comment:"文档版本"`
|
||||
|
||||
// 关联关系
|
||||
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
|
||||
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" comment:"软删除时间"`
|
||||
@@ -41,29 +44,16 @@ func (pd *ProductDocumentation) IsValid() bool {
|
||||
return pd.DeletedAt.Time.IsZero()
|
||||
}
|
||||
|
||||
// IsPublished 检查文档是否已发布
|
||||
func (pd *ProductDocumentation) IsPublished() bool {
|
||||
return pd.Published
|
||||
}
|
||||
|
||||
// Publish 发布文档
|
||||
func (pd *ProductDocumentation) Publish() {
|
||||
pd.Published = true
|
||||
}
|
||||
|
||||
// Unpublish 取消发布文档
|
||||
func (pd *ProductDocumentation) Unpublish() {
|
||||
pd.Published = false
|
||||
}
|
||||
|
||||
// UpdateContent 更新文档内容
|
||||
func (pd *ProductDocumentation) UpdateContent(title, content, usageGuide, apiDocs, examples, faq string) {
|
||||
pd.Title = title
|
||||
pd.Content = content
|
||||
pd.UsageGuide = usageGuide
|
||||
pd.APIDocs = apiDocs
|
||||
pd.Examples = examples
|
||||
pd.FAQ = faq
|
||||
func (pd *ProductDocumentation) UpdateContent(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes string) {
|
||||
pd.RequestURL = requestURL
|
||||
pd.RequestMethod = requestMethod
|
||||
pd.BasicInfo = basicInfo
|
||||
pd.RequestParams = requestParams
|
||||
pd.ResponseFields = responseFields
|
||||
pd.ResponseExample = responseExample
|
||||
pd.ErrorCodes = errorCodes
|
||||
}
|
||||
|
||||
// IncrementVersion 增加版本号
|
||||
@@ -75,4 +65,157 @@ func (pd *ProductDocumentation) IncrementVersion() {
|
||||
// 这里可以实现更复杂的版本号递增逻辑
|
||||
pd.Version = pd.Version + ".1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证文档完整性
|
||||
func (pd *ProductDocumentation) Validate() error {
|
||||
if pd.RequestURL == "" {
|
||||
return errors.New("请求链接不能为空")
|
||||
}
|
||||
if pd.RequestMethod == "" {
|
||||
return errors.New("请求方法不能为空")
|
||||
}
|
||||
if pd.ProductID == "" {
|
||||
return errors.New("产品ID不能为空")
|
||||
}
|
||||
|
||||
// 验证请求方法
|
||||
validMethods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
methodValid := false
|
||||
for _, method := range validMethods {
|
||||
if strings.ToUpper(pd.RequestMethod) == method {
|
||||
methodValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !methodValid {
|
||||
return fmt.Errorf("无效的请求方法: %s", pd.RequestMethod)
|
||||
}
|
||||
|
||||
// 验证URL格式(简单验证)
|
||||
if !strings.HasPrefix(pd.RequestURL, "http://") && !strings.HasPrefix(pd.RequestURL, "https://") {
|
||||
return errors.New("请求链接必须以http://或https://开头")
|
||||
}
|
||||
|
||||
// 验证版本号格式
|
||||
if pd.Version != "" {
|
||||
if !isValidVersion(pd.Version) {
|
||||
return fmt.Errorf("无效的版本号格式: %s", pd.Version)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanPublish 检查是否可以发布
|
||||
func (pd *ProductDocumentation) CanPublish() error {
|
||||
if err := pd.Validate(); err != nil {
|
||||
return fmt.Errorf("文档验证失败: %w", err)
|
||||
}
|
||||
if pd.BasicInfo == "" {
|
||||
return errors.New("基础说明不能为空")
|
||||
}
|
||||
if pd.RequestParams == "" {
|
||||
return errors.New("请求参数不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDocumentation 更新文档内容并自动递增版本
|
||||
func (pd *ProductDocumentation) UpdateDocumentation(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes string) error {
|
||||
// 验证必填字段
|
||||
if requestURL == "" || requestMethod == "" {
|
||||
return errors.New("请求链接和请求方法不能为空")
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
pd.UpdateContent(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes)
|
||||
|
||||
// 自动递增版本
|
||||
pd.IncrementVersion()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// GetDocumentationSummary 获取文档摘要
|
||||
func (pd *ProductDocumentation) GetDocumentationSummary() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": pd.ID,
|
||||
"product_id": pd.ProductID,
|
||||
"request_url": pd.RequestURL,
|
||||
"method": pd.RequestMethod,
|
||||
"version": pd.Version,
|
||||
"created_at": pd.CreatedAt,
|
||||
"updated_at": pd.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// HasRequiredFields 检查是否包含必需字段
|
||||
func (pd *ProductDocumentation) HasRequiredFields() bool {
|
||||
return pd.RequestURL != "" &&
|
||||
pd.RequestMethod != "" &&
|
||||
pd.ProductID != "" &&
|
||||
pd.BasicInfo != "" &&
|
||||
pd.RequestParams != ""
|
||||
}
|
||||
|
||||
// IsComplete 检查文档是否完整
|
||||
func (pd *ProductDocumentation) IsComplete() bool {
|
||||
return pd.HasRequiredFields() &&
|
||||
pd.ResponseFields != "" &&
|
||||
pd.ResponseExample != "" &&
|
||||
pd.ErrorCodes != ""
|
||||
}
|
||||
|
||||
// GetCompletionPercentage 获取文档完成度百分比
|
||||
func (pd *ProductDocumentation) GetCompletionPercentage() int {
|
||||
totalFields := 8 // 总字段数
|
||||
completedFields := 0
|
||||
|
||||
if pd.RequestURL != "" {
|
||||
completedFields++
|
||||
}
|
||||
if pd.RequestMethod != "" {
|
||||
completedFields++
|
||||
}
|
||||
if pd.BasicInfo != "" {
|
||||
completedFields++
|
||||
}
|
||||
if pd.RequestParams != "" {
|
||||
completedFields++
|
||||
}
|
||||
if pd.ResponseFields != "" {
|
||||
completedFields++
|
||||
}
|
||||
if pd.ResponseExample != "" {
|
||||
completedFields++
|
||||
}
|
||||
if pd.ErrorCodes != "" {
|
||||
completedFields++
|
||||
}
|
||||
return (completedFields * 100) / totalFields
|
||||
}
|
||||
|
||||
// isValidVersion 验证版本号格式
|
||||
func isValidVersion(version string) bool {
|
||||
// 简单的版本号验证:x.y.z 格式
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) < 1 || len(parts) > 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
return false
|
||||
}
|
||||
// 检查是否为数字
|
||||
for _, char := range part {
|
||||
if char < '0' || char > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Subscription struct {
|
||||
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:"乐观锁版本号"`
|
||||
|
||||
// 关联关系
|
||||
Product *Product `gorm:"foreignKey:ProductID" comment:"产品"`
|
||||
@@ -40,9 +41,11 @@ func (s *Subscription) IsValid() bool {
|
||||
// IncrementAPIUsage 增加API使用次数
|
||||
func (s *Subscription) IncrementAPIUsage(count int64) {
|
||||
s.APIUsed += count
|
||||
s.Version++ // 增加版本号
|
||||
}
|
||||
|
||||
// ResetAPIUsage 重置API使用次数
|
||||
func (s *Subscription) ResetAPIUsage() {
|
||||
s.APIUsed = 0
|
||||
s.Version++ // 增加版本号
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
)
|
||||
|
||||
// ProductDocumentationRepository 产品文档仓储接口
|
||||
type ProductDocumentationRepository interface {
|
||||
// 基础CRUD操作
|
||||
Create(ctx context.Context, documentation *entities.ProductDocumentation) error
|
||||
Update(ctx context.Context, documentation *entities.ProductDocumentation) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
FindByID(ctx context.Context, id string) (*entities.ProductDocumentation, error)
|
||||
|
||||
// 业务查询操作
|
||||
FindByProductID(ctx context.Context, productID string) (*entities.ProductDocumentation, error)
|
||||
|
||||
// 批量操作
|
||||
FindByProductIDs(ctx context.Context, productIDs []string) ([]*entities.ProductDocumentation, error)
|
||||
UpdateBatch(ctx context.Context, documentations []*entities.ProductDocumentation) error
|
||||
|
||||
// 统计操作
|
||||
CountByProductID(ctx context.Context, productID string) (int64, error)
|
||||
}
|
||||
@@ -22,4 +22,7 @@ type SubscriptionRepository interface {
|
||||
// 统计方法
|
||||
CountByUser(ctx context.Context, userID string) (int64, error)
|
||||
CountByProduct(ctx context.Context, productID string) (int64, error)
|
||||
|
||||
// 乐观锁更新方法
|
||||
IncrementAPIUsageWithOptimisticLock(ctx context.Context, subscriptionID string, increment int64) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
)
|
||||
|
||||
// ProductDocumentationService 产品文档服务
|
||||
type ProductDocumentationService struct {
|
||||
docRepo repositories.ProductDocumentationRepository
|
||||
productRepo repositories.ProductRepository
|
||||
}
|
||||
|
||||
// NewProductDocumentationService 创建文档服务实例
|
||||
func NewProductDocumentationService(
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
productRepo repositories.ProductRepository,
|
||||
) *ProductDocumentationService {
|
||||
return &ProductDocumentationService{
|
||||
docRepo: docRepo,
|
||||
productRepo: productRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDocumentation 创建文档
|
||||
func (s *ProductDocumentationService) CreateDocumentation(ctx context.Context, productID string, doc *entities.ProductDocumentation) error {
|
||||
// 验证产品是否存在
|
||||
product, err := s.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("产品不存在: %w", err)
|
||||
}
|
||||
if !product.IsValid() {
|
||||
return errors.New("产品已禁用或删除")
|
||||
}
|
||||
|
||||
// 检查是否已存在文档
|
||||
existingDoc, err := s.docRepo.FindByProductID(ctx, productID)
|
||||
if err == nil && existingDoc != nil {
|
||||
return errors.New("该产品已存在文档")
|
||||
}
|
||||
|
||||
// 设置产品ID
|
||||
doc.ProductID = productID
|
||||
|
||||
// 验证文档完整性
|
||||
if err := doc.Validate(); err != nil {
|
||||
return fmt.Errorf("文档验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建文档
|
||||
return s.docRepo.Create(ctx, doc)
|
||||
}
|
||||
|
||||
// UpdateDocumentation 更新文档
|
||||
func (s *ProductDocumentationService) UpdateDocumentation(ctx context.Context, id string, requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes string) error {
|
||||
// 查找现有文档
|
||||
doc, err := s.docRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("文档不存在: %w", err)
|
||||
}
|
||||
|
||||
// 使用实体的更新方法
|
||||
err = doc.UpdateDocumentation(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("文档更新失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
return s.docRepo.Update(ctx, doc)
|
||||
}
|
||||
|
||||
// GetDocumentation 获取文档
|
||||
func (s *ProductDocumentationService) GetDocumentation(ctx context.Context, id string) (*entities.ProductDocumentation, error) {
|
||||
return s.docRepo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetDocumentationByProductID 通过产品ID获取文档
|
||||
func (s *ProductDocumentationService) GetDocumentationByProductID(ctx context.Context, productID string) (*entities.ProductDocumentation, error) {
|
||||
return s.docRepo.FindByProductID(ctx, productID)
|
||||
}
|
||||
|
||||
// DeleteDocumentation 删除文档
|
||||
func (s *ProductDocumentationService) DeleteDocumentation(ctx context.Context, id string) error {
|
||||
_, err := s.docRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("文档不存在: %w", err)
|
||||
}
|
||||
|
||||
return s.docRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetDocumentationWithProduct 获取文档及其关联的产品信息
|
||||
func (s *ProductDocumentationService) GetDocumentationWithProduct(ctx context.Context, id string) (*entities.ProductDocumentation, error) {
|
||||
doc, err := s.docRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加载产品信息
|
||||
product, err := s.productRepo.GetByID(ctx, doc.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取产品信息失败: %w", err)
|
||||
}
|
||||
|
||||
doc.Product = &product
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// GetDocumentationsByProductIDs 批量获取文档
|
||||
func (s *ProductDocumentationService) GetDocumentationsByProductIDs(ctx context.Context, productIDs []string) ([]*entities.ProductDocumentation, error) {
|
||||
return s.docRepo.FindByProductIDs(ctx, productIDs)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -190,4 +191,58 @@ func (s *ProductSubscriptionService) SaveSubscription(ctx context.Context, subsc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IncrementSubscriptionAPIUsage 增加订阅API使用次数(使用乐观锁,带重试机制)
|
||||
func (s *ProductSubscriptionService) IncrementSubscriptionAPIUsage(ctx context.Context, subscriptionID string, increment int64) error {
|
||||
const maxRetries = 3
|
||||
const baseDelay = 10 * time.Millisecond
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// 使用乐观锁直接更新数据库
|
||||
err := s.subscriptionRepo.IncrementAPIUsageWithOptimisticLock(ctx, subscriptionID, increment)
|
||||
if err == nil {
|
||||
// 更新成功
|
||||
if attempt > 0 {
|
||||
s.logger.Info("订阅API使用次数更新成功(重试后)",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.Int64("increment", increment),
|
||||
zap.Int("retry_count", attempt))
|
||||
} else {
|
||||
s.logger.Info("订阅API使用次数更新成功",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.Int64("increment", increment))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否是版本冲突错误
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 版本冲突,等待后重试
|
||||
if attempt < maxRetries-1 {
|
||||
delay := time.Duration(attempt+1) * baseDelay
|
||||
s.logger.Debug("订阅版本冲突,准备重试",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.Int("attempt", attempt+1),
|
||||
zap.Duration("delay", delay))
|
||||
time.Sleep(delay)
|
||||
continue
|
||||
}
|
||||
// 最后一次重试失败
|
||||
s.logger.Error("订阅不存在或版本冲突,重试次数已用完",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.Int("max_retries", maxRetries),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("订阅不存在或已被其他操作修改(重试%d次后失败): %w", maxRetries, err)
|
||||
}
|
||||
|
||||
// 其他错误直接返回,不重试
|
||||
s.logger.Error("更新订阅API使用次数失败",
|
||||
zap.String("subscription_id", subscriptionID),
|
||||
zap.Int64("increment", increment),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("更新订阅API使用次数失败: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("更新失败,已重试%d次", maxRetries)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tyapi-server/internal/domains/product/entities"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ProductDocumentationsTable = "product_documentations"
|
||||
)
|
||||
|
||||
type GormProductDocumentationRepository struct {
|
||||
*database.CachedBaseRepositoryImpl
|
||||
}
|
||||
|
||||
func (r *GormProductDocumentationRepository) Delete(ctx context.Context, id string) error {
|
||||
return r.DeleteEntity(ctx, id, &entities.ProductDocumentation{})
|
||||
}
|
||||
|
||||
var _ repositories.ProductDocumentationRepository = (*GormProductDocumentationRepository)(nil)
|
||||
|
||||
func NewGormProductDocumentationRepository(db *gorm.DB, logger *zap.Logger) repositories.ProductDocumentationRepository {
|
||||
return &GormProductDocumentationRepository{
|
||||
CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, ProductDocumentationsTable),
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建文档
|
||||
func (r *GormProductDocumentationRepository) Create(ctx context.Context, documentation *entities.ProductDocumentation) error {
|
||||
return r.CreateEntity(ctx, documentation)
|
||||
}
|
||||
|
||||
// Update 更新文档
|
||||
func (r *GormProductDocumentationRepository) Update(ctx context.Context, documentation *entities.ProductDocumentation) error {
|
||||
return r.UpdateEntity(ctx, documentation)
|
||||
}
|
||||
|
||||
// FindByID 根据ID查找文档
|
||||
func (r *GormProductDocumentationRepository) FindByID(ctx context.Context, id string) (*entities.ProductDocumentation, error) {
|
||||
var entity entities.ProductDocumentation
|
||||
err := r.SmartGetByID(ctx, id, &entity)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &entity, nil
|
||||
}
|
||||
|
||||
// FindByProductID 根据产品ID查找文档
|
||||
func (r *GormProductDocumentationRepository) FindByProductID(ctx context.Context, productID string) (*entities.ProductDocumentation, error) {
|
||||
var entity entities.ProductDocumentation
|
||||
err := r.GetDB(ctx).Where("product_id = ?", productID).First(&entity).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &entity, nil
|
||||
}
|
||||
|
||||
// FindByProductIDs 根据产品ID列表批量查找文档
|
||||
func (r *GormProductDocumentationRepository) FindByProductIDs(ctx context.Context, productIDs []string) ([]*entities.ProductDocumentation, error) {
|
||||
var documentations []entities.ProductDocumentation
|
||||
err := r.GetDB(ctx).Where("product_id IN ?", productIDs).Find(&documentations).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
result := make([]*entities.ProductDocumentation, len(documentations))
|
||||
for i := range documentations {
|
||||
result[i] = &documentations[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateBatch 批量更新文档
|
||||
func (r *GormProductDocumentationRepository) UpdateBatch(ctx context.Context, documentations []*entities.ProductDocumentation) error {
|
||||
if len(documentations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用事务进行批量更新
|
||||
return r.GetDB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, doc := range documentations {
|
||||
if err := tx.Save(doc).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CountByProductID 统计指定产品的文档数量
|
||||
func (r *GormProductDocumentationRepository) CountByProductID(ctx context.Context, productID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.GetDB(ctx).Model(&entities.ProductDocumentation{}).Where("product_id = ?", productID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -289,3 +289,26 @@ func (r *GormSubscriptionRepository) WithTx(tx interface{}) interfaces.Repositor
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// IncrementAPIUsageWithOptimisticLock 使用乐观锁增加API使用次数
|
||||
func (r *GormSubscriptionRepository) IncrementAPIUsageWithOptimisticLock(ctx context.Context, subscriptionID string, increment int64) error {
|
||||
// 使用原生SQL进行乐观锁更新
|
||||
result := r.GetDB(ctx).WithContext(ctx).Exec(`
|
||||
UPDATE subscription
|
||||
SET api_used = api_used + ?, version = version + 1, updated_at = NOW()
|
||||
WHERE id = ? AND version = (
|
||||
SELECT version FROM subscription WHERE id = ?
|
||||
)
|
||||
`, increment, subscriptionID, subscriptionID)
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// 检查是否有行被更新
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
|
||||
// ProductAdminHandler 产品管理员HTTP处理器
|
||||
type ProductAdminHandler struct {
|
||||
productAppService product.ProductApplicationService
|
||||
categoryAppService product.CategoryApplicationService
|
||||
subscriptionAppService product.SubscriptionApplicationService
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
productAppService product.ProductApplicationService
|
||||
categoryAppService product.CategoryApplicationService
|
||||
subscriptionAppService product.SubscriptionApplicationService
|
||||
documentationAppService product.DocumentationApplicationServiceInterface
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewProductAdminHandler 创建产品管理员HTTP处理器
|
||||
@@ -27,17 +28,19 @@ func NewProductAdminHandler(
|
||||
productAppService product.ProductApplicationService,
|
||||
categoryAppService product.CategoryApplicationService,
|
||||
subscriptionAppService product.SubscriptionApplicationService,
|
||||
documentationAppService product.DocumentationApplicationServiceInterface,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
) *ProductAdminHandler {
|
||||
return &ProductAdminHandler{
|
||||
productAppService: productAppService,
|
||||
categoryAppService: categoryAppService,
|
||||
subscriptionAppService: subscriptionAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
productAppService: productAppService,
|
||||
categoryAppService: categoryAppService,
|
||||
subscriptionAppService: subscriptionAppService,
|
||||
documentationAppService: documentationAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,14 +360,15 @@ func (h *ProductAdminHandler) getIntQuery(c *gin.Context, key string, defaultVal
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetProductDetail 获取产品详情(管理员)
|
||||
// GetProductDetail 获取产品详情
|
||||
// @Summary 获取产品详情
|
||||
// @Description 管理员获取产品详细信息,包含可见状态
|
||||
// @Description 管理员获取产品详细信息
|
||||
// @Tags 产品管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "产品ID"
|
||||
// @Param with_document query bool false "是否包含文档信息"
|
||||
// @Success 200 {object} responses.ProductAdminInfoResponse "获取产品详情成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
@@ -372,18 +376,23 @@ func (h *ProductAdminHandler) getIntQuery(c *gin.Context, key string, defaultVal
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/products/{id} [get]
|
||||
func (h *ProductAdminHandler) GetProductDetail(c *gin.Context) {
|
||||
var query queries.GetProductQuery
|
||||
var query queries.GetProductDetailQuery
|
||||
query.ID = c.Param("id")
|
||||
|
||||
if query.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用管理员专用的产品详情获取方法
|
||||
// 解析可选参数
|
||||
if withDocument := c.Query("with_document"); withDocument != "" {
|
||||
if withDoc, err := strconv.ParseBool(withDocument); err == nil {
|
||||
query.WithDocument = &withDoc
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.productAppService.GetProductByIDForAdmin(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取产品详情失败", zap.Error(err), zap.String("product_id", query.ID))
|
||||
h.logger.Error("获取产品详情失败", zap.Error(err))
|
||||
h.responseBuilder.NotFound(c, "产品不存在")
|
||||
return
|
||||
}
|
||||
@@ -793,7 +802,7 @@ func (h *ProductAdminHandler) GetProductApiConfig(c *gin.Context) {
|
||||
h.responseBuilder.Success(c, emptyConfig, "获取API配置成功")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
h.logger.Error("获取产品API配置失败", zap.Error(err), zap.String("product_id", productID))
|
||||
h.responseBuilder.NotFound(c, "产品不存在")
|
||||
return
|
||||
@@ -893,7 +902,7 @@ func (h *ProductAdminHandler) UpdateProductApiConfig(c *gin.Context) {
|
||||
// @Success 200 {object} map[string]interface{} "API配置删除成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "产品或配置不存在"
|
||||
// @Failure 404 {object} map[string]interface{} "产品或API配置不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/products/{id}/api-config [delete]
|
||||
func (h *ProductAdminHandler) DeleteProductApiConfig(c *gin.Context) {
|
||||
@@ -903,19 +912,144 @@ func (h *ProductAdminHandler) DeleteProductApiConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 先获取现有配置以获取配置ID
|
||||
existingConfig, err := h.productAppService.GetProductApiConfig(c.Request.Context(), productID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取现有API配置失败", zap.Error(err), zap.String("product_id", productID))
|
||||
h.responseBuilder.NotFound(c, "产品API配置不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.productAppService.DeleteProductApiConfig(c.Request.Context(), existingConfig.ID); err != nil {
|
||||
h.logger.Error("删除产品API配置失败", zap.Error(err), zap.String("product_id", productID))
|
||||
if err := h.productAppService.DeleteProductApiConfig(c.Request.Context(), productID); err != nil {
|
||||
h.logger.Error("删除产品API配置失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil, "API配置删除成功")
|
||||
}
|
||||
|
||||
// GetProductDocumentation 获取产品文档
|
||||
// @Summary 获取产品文档
|
||||
// @Description 管理员获取产品的文档信息
|
||||
// @Tags 产品管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "产品ID"
|
||||
// @Success 200 {object} responses.DocumentationResponse "获取文档成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "产品或文档不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/products/{id}/documentation [get]
|
||||
func (h *ProductAdminHandler) GetProductDocumentation(c *gin.Context) {
|
||||
productID := c.Param("id")
|
||||
if productID == "" {
|
||||
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
|
||||
if err != nil {
|
||||
// 文档不存在时,返回空数据而不是错误
|
||||
h.logger.Info("产品文档不存在,返回空数据", zap.String("product_id", productID))
|
||||
h.responseBuilder.Success(c, nil, "文档不存在")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, doc, "获取文档成功")
|
||||
}
|
||||
|
||||
// CreateOrUpdateProductDocumentation 创建或更新产品文档
|
||||
// @Summary 创建或更新产品文档
|
||||
// @Description 管理员创建或更新产品的文档信息,如果文档不存在则创建,存在则更新
|
||||
// @Tags 产品管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "产品ID"
|
||||
// @Param request body commands.CreateDocumentationCommand true "文档信息"
|
||||
// @Success 200 {object} responses.DocumentationResponse "文档操作成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "产品不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/products/{id}/documentation [post]
|
||||
func (h *ProductAdminHandler) CreateOrUpdateProductDocumentation(c *gin.Context) {
|
||||
productID := c.Param("id")
|
||||
if productID == "" {
|
||||
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var cmd commands.CreateDocumentationCommand
|
||||
cmd.ProductID = productID
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 先尝试获取现有文档
|
||||
existingDoc, err := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
|
||||
if err != nil {
|
||||
// 文档不存在,创建新文档
|
||||
doc, err := h.documentationAppService.CreateDocumentation(c.Request.Context(), &cmd)
|
||||
if err != nil {
|
||||
h.logger.Error("创建产品文档失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.responseBuilder.Created(c, doc, "文档创建成功")
|
||||
return
|
||||
}
|
||||
|
||||
// 文档存在,更新文档
|
||||
updateCmd := commands.UpdateDocumentationCommand{
|
||||
RequestURL: cmd.RequestURL,
|
||||
RequestMethod: cmd.RequestMethod,
|
||||
BasicInfo: cmd.BasicInfo,
|
||||
RequestParams: cmd.RequestParams,
|
||||
ResponseFields: cmd.ResponseFields,
|
||||
ResponseExample: cmd.ResponseExample,
|
||||
ErrorCodes: cmd.ErrorCodes,
|
||||
}
|
||||
|
||||
doc, err := h.documentationAppService.UpdateDocumentation(c.Request.Context(), existingDoc.ID, &updateCmd)
|
||||
if err != nil {
|
||||
h.logger.Error("更新产品文档失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, doc, "文档更新成功")
|
||||
}
|
||||
|
||||
// DeleteProductDocumentation 删除产品文档
|
||||
// @Summary 删除产品文档
|
||||
// @Description 管理员删除产品的文档
|
||||
// @Tags 产品管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "产品ID"
|
||||
// @Success 200 {object} map[string]interface{} "文档删除成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "产品或文档不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/admin/products/{id}/documentation [delete]
|
||||
func (h *ProductAdminHandler) DeleteProductDocumentation(c *gin.Context) {
|
||||
productID := c.Param("id")
|
||||
if productID == "" {
|
||||
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 先获取文档
|
||||
doc, err := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
|
||||
if err != nil {
|
||||
h.responseBuilder.NotFound(c, "文档不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
if err := h.documentationAppService.DeleteDocumentation(c.Request.Context(), doc.ID); err != nil {
|
||||
h.logger.Error("删除产品文档失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, nil, "文档删除成功")
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type ProductHandler struct {
|
||||
apiConfigService product.ProductApiConfigApplicationService
|
||||
categoryService product.CategoryApplicationService
|
||||
subAppService product.SubscriptionApplicationService
|
||||
documentationAppService product.DocumentationApplicationServiceInterface
|
||||
responseBuilder interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
@@ -28,6 +29,7 @@ func NewProductHandler(
|
||||
apiConfigService product.ProductApiConfigApplicationService,
|
||||
categoryService product.CategoryApplicationService,
|
||||
subAppService product.SubscriptionApplicationService,
|
||||
documentationAppService product.DocumentationApplicationServiceInterface,
|
||||
responseBuilder interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
@@ -37,6 +39,7 @@ func NewProductHandler(
|
||||
apiConfigService: apiConfigService,
|
||||
categoryService: categoryService,
|
||||
subAppService: subAppService,
|
||||
documentationAppService: documentationAppService,
|
||||
responseBuilder: responseBuilder,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
@@ -171,30 +174,36 @@ func (h *ProductHandler) getCurrentUserID(c *gin.Context) string {
|
||||
|
||||
// GetProductDetail 获取产品详情
|
||||
// @Summary 获取产品详情
|
||||
// @Description 根据产品ID获取产品详细信息,只能获取可见的产品
|
||||
// @Description 获取产品详细信息,用户端只能查看可见的产品
|
||||
// @Tags 数据大厅
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "产品ID"
|
||||
// @Success 200 {object} responses.ProductInfoResponse "获取产品详情成功"
|
||||
// @Param with_document query bool false "是否包含文档信息"
|
||||
// @Success 200 {object} responses.ProductInfoWithDocumentResponse "获取产品详情成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 404 {object} map[string]interface{} "产品不存在"
|
||||
// @Failure 404 {object} map[string]interface{} "产品不存在或不可见"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/products/{id} [get]
|
||||
func (h *ProductHandler) GetProductDetail(c *gin.Context) {
|
||||
var query queries.GetProductQuery
|
||||
var query queries.GetProductDetailQuery
|
||||
query.ID = c.Param("id")
|
||||
|
||||
if query.ID == "" {
|
||||
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用用户端专用的产品详情获取方法
|
||||
// 解析可选参数
|
||||
if withDocument := c.Query("with_document"); withDocument != "" {
|
||||
if withDoc, err := strconv.ParseBool(withDocument); err == nil {
|
||||
query.WithDocument = &withDoc
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.appService.GetProductByIDForUser(c.Request.Context(), &query)
|
||||
if err != nil {
|
||||
h.logger.Error("获取产品详情失败", zap.Error(err), zap.String("product_id", query.ID))
|
||||
h.responseBuilder.NotFound(c, "产品不存在")
|
||||
h.logger.Error("获取产品详情失败", zap.Error(err))
|
||||
h.responseBuilder.NotFound(c, "产品不存在或不可见")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -523,31 +532,61 @@ func (h *ProductHandler) GetMySubscriptionDetail(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param id path string true "订阅ID"
|
||||
// @Success 200 {object} responses.SubscriptionUsageResponse "获取使用情况成功"
|
||||
// @Success 200 {object} map[string]interface{} "获取使用情况成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "订阅不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/my/subscriptions/{id}/usage [get]
|
||||
func (h *ProductHandler) GetMySubscriptionUsage(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未登录")
|
||||
return
|
||||
}
|
||||
|
||||
subscriptionID := c.Param("id")
|
||||
if subscriptionID == "" {
|
||||
h.responseBuilder.BadRequest(c, "订阅ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取我的订阅使用情况失败", zap.Error(err), zap.String("user_id", userID), zap.String("subscription_id", subscriptionID))
|
||||
h.responseBuilder.NotFound(c, "订阅不存在")
|
||||
// 获取当前用户ID
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.responseBuilder.Unauthorized(c, "用户未认证")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, result, "获取我的订阅使用情况成功")
|
||||
usage, err := h.subAppService.GetSubscriptionUsage(c.Request.Context(), subscriptionID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取订阅使用情况失败", zap.Error(err))
|
||||
h.responseBuilder.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, usage, "获取使用情况成功")
|
||||
}
|
||||
|
||||
// GetProductDocumentation 获取产品文档
|
||||
// @Summary 获取产品文档
|
||||
// @Description 获取指定产品的文档信息
|
||||
// @Tags 数据大厅
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "产品ID"
|
||||
// @Success 200 {object} responses.DocumentationResponse "获取文档成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 404 {object} map[string]interface{} "产品或文档不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/products/{id}/documentation [get]
|
||||
func (h *ProductHandler) GetProductDocumentation(c *gin.Context) {
|
||||
productID := c.Param("id")
|
||||
if productID == "" {
|
||||
h.responseBuilder.BadRequest(c, "产品ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := h.documentationAppService.GetDocumentationByProductID(c.Request.Context(), productID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取产品文档失败", zap.Error(err))
|
||||
h.responseBuilder.NotFound(c, "文档不存在")
|
||||
return
|
||||
}
|
||||
|
||||
h.responseBuilder.Success(c, doc, "获取文档成功")
|
||||
}
|
||||
|
||||
@@ -55,6 +55,11 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
products.POST("/:id/api-config", r.handler.CreateProductApiConfig)
|
||||
products.PUT("/:id/api-config", r.handler.UpdateProductApiConfig)
|
||||
products.DELETE("/:id/api-config", r.handler.DeleteProductApiConfig)
|
||||
|
||||
// 文档管理
|
||||
products.GET("/:id/documentation", r.handler.GetProductDocumentation)
|
||||
products.POST("/:id/documentation", r.handler.CreateOrUpdateProductDocumentation)
|
||||
products.DELETE("/:id/documentation", r.handler.DeleteProductDocumentation)
|
||||
}
|
||||
|
||||
// 分类管理
|
||||
|
||||
@@ -50,6 +50,7 @@ func (r *ProductRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
// 产品详情和API配置 - 使用具体路径避免冲突
|
||||
products.GET("/:id", r.productHandler.GetProductDetail)
|
||||
products.GET("/:id/api-config", r.productHandler.GetProductApiConfig)
|
||||
products.GET("/:id/documentation", r.productHandler.GetProductDocumentation)
|
||||
|
||||
// 订阅产品(需要认证)
|
||||
products.POST("/:id/subscribe", r.auth.Handle(), r.productHandler.SubscribeProduct)
|
||||
|
||||
52
internal/shared/cache/gorm_cache_plugin.go
vendored
52
internal/shared/cache/gorm_cache_plugin.go
vendored
@@ -22,7 +22,7 @@ type GormCachePlugin struct {
|
||||
cache interfaces.CacheService
|
||||
logger *zap.Logger
|
||||
config CacheConfig
|
||||
|
||||
|
||||
// 缓存失效去重机制
|
||||
invalidationQueue map[string]*time.Timer
|
||||
queueMutex sync.RWMutex
|
||||
@@ -64,7 +64,7 @@ func DefaultCacheConfig() CacheConfig {
|
||||
BloomFilter: false,
|
||||
AutoInvalidate: true,
|
||||
// 增加延迟失效时间,减少频繁的缓存失效操作
|
||||
InvalidateDelay: 500 * time.Millisecond,
|
||||
InvalidateDelay: 500 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +76,9 @@ func NewGormCachePlugin(cache interfaces.CacheService, logger *zap.Logger, confi
|
||||
}
|
||||
|
||||
return &GormCachePlugin{
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
invalidationQueue: make(map[string]*time.Timer),
|
||||
}
|
||||
}
|
||||
@@ -105,16 +105,16 @@ func (p *GormCachePlugin) Initialize(db *gorm.DB) error {
|
||||
func (p *GormCachePlugin) Shutdown() {
|
||||
p.queueMutex.Lock()
|
||||
defer p.queueMutex.Unlock()
|
||||
|
||||
|
||||
// 停止所有定时器
|
||||
for table, timer := range p.invalidationQueue {
|
||||
timer.Stop()
|
||||
p.logger.Debug("停止缓存失效定时器", zap.String("table", table))
|
||||
}
|
||||
|
||||
|
||||
// 清空队列
|
||||
p.invalidationQueue = make(map[string]*time.Timer)
|
||||
|
||||
|
||||
p.logger.Info("GORM缓存插件已关闭")
|
||||
}
|
||||
|
||||
@@ -174,13 +174,13 @@ func (p *GormCachePlugin) beforeQuery(db *gorm.DB) {
|
||||
}
|
||||
return
|
||||
} else {
|
||||
p.logger.Warn("缓存数据恢复失败,将执行数据库查询",
|
||||
zap.String("cache_key", cacheKey),
|
||||
p.logger.Warn("缓存数据恢复失败,将执行数据库查询",
|
||||
zap.String("cache_key", cacheKey),
|
||||
zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
p.logger.Debug("缓存未命中",
|
||||
zap.String("cache_key", cacheKey),
|
||||
p.logger.Debug("缓存未命中",
|
||||
zap.String("cache_key", cacheKey),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ func (p *GormCachePlugin) saveToCache(ctx context.Context, cacheKey string, db *
|
||||
|
||||
// 修复:改进缓存数据保存逻辑
|
||||
var dataToCache interface{}
|
||||
|
||||
|
||||
// 如果dest是切片,需要特殊处理
|
||||
destValue := reflect.ValueOf(dest)
|
||||
if destValue.Kind() == reflect.Ptr && destValue.Elem().Kind() == reflect.Slice {
|
||||
@@ -474,7 +474,7 @@ func (p *GormCachePlugin) restoreFromCache(db *gorm.DB, cachedResult *CachedResu
|
||||
|
||||
// 修复:改进缓存数据恢复逻辑
|
||||
cachedValue := reflect.ValueOf(cachedResult.Data)
|
||||
|
||||
|
||||
// 如果类型完全匹配,直接赋值
|
||||
if cachedValue.Type().AssignableTo(destValue.Elem().Type()) {
|
||||
destValue.Elem().Set(cachedValue)
|
||||
@@ -487,8 +487,8 @@ func (p *GormCachePlugin) restoreFromCache(db *gorm.DB, cachedResult *CachedResu
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonData, db.Statement.Dest); err != nil {
|
||||
p.logger.Error("反序列化缓存数据失败",
|
||||
zap.String("json_data", string(jsonData)),
|
||||
p.logger.Error("反序列化缓存数据失败",
|
||||
zap.String("json_data", string(jsonData)),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("JSON反序列化失败: %w", err)
|
||||
}
|
||||
@@ -497,7 +497,7 @@ func (p *GormCachePlugin) restoreFromCache(db *gorm.DB, cachedResult *CachedResu
|
||||
// 设置影响行数
|
||||
db.Statement.RowsAffected = cachedResult.RowCount
|
||||
|
||||
p.logger.Debug("从缓存恢复数据成功",
|
||||
p.logger.Debug("从缓存恢复数据成功",
|
||||
zap.Int64("rows", cachedResult.RowCount),
|
||||
zap.Time("timestamp", cachedResult.Timestamp))
|
||||
|
||||
@@ -521,34 +521,34 @@ func (p *GormCachePlugin) invalidateTableCache(ctx context.Context, table string
|
||||
// 使用去重机制,避免重复的缓存失效操作
|
||||
p.queueMutex.Lock()
|
||||
defer p.queueMutex.Unlock()
|
||||
|
||||
|
||||
// 如果已经有相同的失效操作在队列中,取消之前的定时器
|
||||
if timer, exists := p.invalidationQueue[table]; exists {
|
||||
timer.Stop()
|
||||
delete(p.invalidationQueue, table)
|
||||
}
|
||||
|
||||
|
||||
// 创建独立的上下文,避免受到原始请求上下文的影响
|
||||
// 设置合理的超时时间,避免缓存失效操作阻塞
|
||||
cacheCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
|
||||
// 创建新的定时器
|
||||
timer := time.AfterFunc(p.config.InvalidateDelay, func() {
|
||||
// 执行缓存失效
|
||||
p.doInvalidateTableCache(cacheCtx, table)
|
||||
|
||||
|
||||
// 清理定时器引用
|
||||
p.queueMutex.Lock()
|
||||
delete(p.invalidationQueue, table)
|
||||
p.queueMutex.Unlock()
|
||||
|
||||
|
||||
// 取消上下文
|
||||
cancel()
|
||||
})
|
||||
|
||||
|
||||
// 将定时器加入队列
|
||||
p.invalidationQueue[table] = timer
|
||||
|
||||
|
||||
p.logger.Debug("缓存失效操作已加入队列",
|
||||
zap.String("table", table),
|
||||
zap.Duration("delay", p.config.InvalidateDelay),
|
||||
@@ -574,7 +574,7 @@ func (p *GormCachePlugin) doInvalidateTableCache(ctx context.Context, table stri
|
||||
time.Sleep(time.Duration(attempt) * 100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
p.logger.Warn("失效表缓存失败",
|
||||
zap.String("table", table),
|
||||
zap.String("pattern", pattern),
|
||||
@@ -583,7 +583,7 @@ func (p *GormCachePlugin) doInvalidateTableCache(ctx context.Context, table stri
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 成功删除,记录日志并退出
|
||||
p.logger.Debug("表缓存已失效",
|
||||
zap.String("table", table),
|
||||
|
||||
@@ -35,9 +35,9 @@ func (r *CachedBaseRepositoryImpl) isTableCacheEnabled() bool {
|
||||
if cache.GlobalCacheConfigManager != nil {
|
||||
return cache.GlobalCacheConfigManager.IsTableCacheEnabled(r.tableName)
|
||||
}
|
||||
|
||||
|
||||
// 如果全局管理器未初始化,默认启用缓存
|
||||
r.logger.Warn("全局缓存配置管理器未初始化,默认启用缓存",
|
||||
r.logger.Warn("全局缓存配置管理器未初始化,默认启用缓存",
|
||||
zap.String("table", r.tableName))
|
||||
return true
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (r *CachedBaseRepositoryImpl) isTableCacheEnabled() bool {
|
||||
func (r *CachedBaseRepositoryImpl) shouldUseCacheForTable() bool {
|
||||
// 检查表是否启用缓存
|
||||
if !r.isTableCacheEnabled() {
|
||||
r.logger.Debug("表未启用缓存,跳过缓存操作",
|
||||
r.logger.Debug("表未启用缓存,跳过缓存操作",
|
||||
zap.String("table", r.tableName))
|
||||
return false
|
||||
}
|
||||
@@ -58,17 +58,17 @@ func (r *CachedBaseRepositoryImpl) shouldUseCacheForTable() bool {
|
||||
// GetWithCache 带缓存的单条查询(智能决策)
|
||||
func (r *CachedBaseRepositoryImpl) GetWithCache(ctx context.Context, dest interface{}, ttl time.Duration, where string, args ...interface{}) error {
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
|
||||
// 智能决策:根据表配置决定是否使用缓存
|
||||
if r.shouldUseCacheForTable() {
|
||||
db = db.Set("cache:enabled", true).Set("cache:ttl", ttl)
|
||||
r.logger.Debug("执行带缓存查询",
|
||||
r.logger.Debug("执行带缓存查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.Duration("ttl", ttl),
|
||||
zap.String("where", where))
|
||||
} else {
|
||||
db = db.Set("cache:disabled", true)
|
||||
r.logger.Debug("执行无缓存查询",
|
||||
r.logger.Debug("执行无缓存查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.String("where", where))
|
||||
}
|
||||
@@ -79,17 +79,17 @@ func (r *CachedBaseRepositoryImpl) GetWithCache(ctx context.Context, dest interf
|
||||
// FindWithCache 带缓存的多条查询(智能决策)
|
||||
func (r *CachedBaseRepositoryImpl) FindWithCache(ctx context.Context, dest interface{}, ttl time.Duration, where string, args ...interface{}) error {
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
|
||||
// 智能决策:根据表配置决定是否使用缓存
|
||||
if r.shouldUseCacheForTable() {
|
||||
db = db.Set("cache:enabled", true).Set("cache:ttl", ttl)
|
||||
r.logger.Debug("执行带缓存批量查询",
|
||||
r.logger.Debug("执行带缓存批量查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.Duration("ttl", ttl),
|
||||
zap.String("where", where))
|
||||
} else {
|
||||
db = db.Set("cache:disabled", true)
|
||||
r.logger.Debug("执行无缓存批量查询",
|
||||
r.logger.Debug("执行无缓存批量查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.String("where", where))
|
||||
}
|
||||
@@ -100,17 +100,17 @@ func (r *CachedBaseRepositoryImpl) FindWithCache(ctx context.Context, dest inter
|
||||
// CountWithCache 带缓存的计数查询(智能决策)
|
||||
func (r *CachedBaseRepositoryImpl) CountWithCache(ctx context.Context, count *int64, ttl time.Duration, entity interface{}, where string, args ...interface{}) error {
|
||||
db := r.GetDB(ctx).Model(entity)
|
||||
|
||||
|
||||
// 智能决策:根据表配置决定是否使用缓存
|
||||
if r.shouldUseCacheForTable() {
|
||||
db = db.Set("cache:enabled", true).Set("cache:ttl", ttl)
|
||||
r.logger.Debug("执行带缓存计数查询",
|
||||
r.logger.Debug("执行带缓存计数查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.Duration("ttl", ttl),
|
||||
zap.String("where", where))
|
||||
} else {
|
||||
db = db.Set("cache:disabled", true)
|
||||
r.logger.Debug("执行无缓存计数查询",
|
||||
r.logger.Debug("执行无缓存计数查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.String("where", where))
|
||||
}
|
||||
@@ -121,16 +121,16 @@ func (r *CachedBaseRepositoryImpl) CountWithCache(ctx context.Context, count *in
|
||||
// ListWithCache 带缓存的列表查询(智能决策)
|
||||
func (r *CachedBaseRepositoryImpl) ListWithCache(ctx context.Context, dest interface{}, ttl time.Duration, options CacheListOptions) error {
|
||||
db := r.GetDB(ctx)
|
||||
|
||||
|
||||
// 智能决策:根据表配置决定是否使用缓存
|
||||
if r.shouldUseCacheForTable() {
|
||||
db = db.Set("cache:enabled", true).Set("cache:ttl", ttl)
|
||||
r.logger.Debug("执行带缓存列表查询",
|
||||
r.logger.Debug("执行带缓存列表查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.Duration("ttl", ttl))
|
||||
} else {
|
||||
db = db.Set("cache:disabled", true)
|
||||
r.logger.Debug("执行无缓存列表查询",
|
||||
r.logger.Debug("执行无缓存列表查询",
|
||||
zap.String("table", r.tableName))
|
||||
}
|
||||
|
||||
@@ -214,10 +214,10 @@ func (r *CachedBaseRepositoryImpl) WithLongCache() *CachedBaseRepositoryImpl {
|
||||
|
||||
// SmartGetByID 智能ID查询(自动缓存)
|
||||
func (r *CachedBaseRepositoryImpl) SmartGetByID(ctx context.Context, id string, dest interface{}) error {
|
||||
r.logger.Debug("执行智能ID查询",
|
||||
r.logger.Debug("执行智能ID查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.String("id", id))
|
||||
|
||||
|
||||
return r.GetWithCache(ctx, dest, 30*time.Minute, "id = ?", id)
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ func (r *CachedBaseRepositoryImpl) SmartList(ctx context.Context, dest interface
|
||||
cacheTTL := r.calculateCacheTTL(options)
|
||||
useCache := r.shouldUseCache(options)
|
||||
|
||||
r.logger.Debug("执行智能列表查询",
|
||||
r.logger.Debug("执行智能列表查询",
|
||||
zap.String("table", r.tableName),
|
||||
zap.Bool("use_cache", useCache),
|
||||
zap.Duration("cache_ttl", cacheTTL))
|
||||
|
||||
Reference in New Issue
Block a user