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