feat: 实现订单支付功能模块
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
- 新增订单管理、支付回调、购买验证等核心服务 - 实现订单、订单项目的数据存储层和 API 接口 - 添加订单数据库迁移和 DTO 定义 - 更新 API 文档和路由配置 - 同步 3 个新规范到主规范库(订单管理、订单支付、套餐购买验证) - 完成 OpenSpec 变更归档 Ultraworked with Sisyphus
This commit is contained in:
159
internal/service/purchase_validation/service.go
Normal file
159
internal/service/purchase_validation/service.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package purchase_validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
packageStore *postgres.PackageStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
packageStore: packageStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
}
|
||||
}
|
||||
|
||||
type PurchaseValidationResult struct {
|
||||
Card *model.IotCard
|
||||
Device *model.Device
|
||||
Packages []*model.Package
|
||||
TotalPrice int64
|
||||
Allocation *model.ShopSeriesAllocation
|
||||
}
|
||||
|
||||
func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, packageIDs []uint) (*PurchaseValidationResult, error) {
|
||||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if card.SeriesAllocationID == nil || *card.SeriesAllocationID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该卡未关联套餐系列,无法购买套餐")
|
||||
}
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allocation.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用")
|
||||
}
|
||||
|
||||
packages, totalPrice, err := s.validatePackages(ctx, packageIDs, allocation.SeriesID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PurchaseValidationResult{
|
||||
Card: card,
|
||||
Packages: packages,
|
||||
TotalPrice: totalPrice,
|
||||
Allocation: allocation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ValidateDevicePurchase(ctx context.Context, deviceID uint, packageIDs []uint) (*PurchaseValidationResult, error) {
|
||||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if device.SeriesAllocationID == nil || *device.SeriesAllocationID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该设备未关联套餐系列,无法购买套餐")
|
||||
}
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByID(ctx, *device.SeriesAllocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allocation.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用")
|
||||
}
|
||||
|
||||
packages, totalPrice, err := s.validatePackages(ctx, packageIDs, allocation.SeriesID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PurchaseValidationResult{
|
||||
Device: device,
|
||||
Packages: packages,
|
||||
TotalPrice: totalPrice,
|
||||
Allocation: allocation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, seriesID uint) ([]*model.Package, int64, error) {
|
||||
if len(packageIDs) == 0 {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "请选择至少一个套餐")
|
||||
}
|
||||
|
||||
var packages []*model.Package
|
||||
var totalPrice int64
|
||||
|
||||
for _, pkgID := range packageIDs {
|
||||
pkg, err := s.packageStore.GetByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐不存在")
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if pkg.SeriesID != seriesID {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐不在可购买范围内")
|
||||
}
|
||||
|
||||
if pkg.Status != constants.StatusEnabled {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已禁用")
|
||||
}
|
||||
|
||||
if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
totalPrice += pkg.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
return packages, totalPrice, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, buyerType string) int64 {
|
||||
return pkg.SuggestedRetailPrice
|
||||
}
|
||||
179
internal/service/purchase_validation/service_test.go
Normal file
179
internal/service/purchase_validation/service_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package purchase_validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestData(t *testing.T) (context.Context, *Service, *model.IotCard, *model.Device, *model.Package, *model.ShopSeriesAllocation) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
deviceStore := postgres.NewDeviceStore(tx, rdb)
|
||||
packageStore := postgres.NewPackageStore(tx)
|
||||
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
|
||||
carrierStore := postgres.NewCarrierStore(tx)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "TEST_CARRIER_PV",
|
||||
CarrierName: "测试运营商",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, carrierStore.Create(ctx, carrier))
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺PV",
|
||||
ShopCode: "TEST_SHOP_PV",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, shopStore.Create(ctx, shop))
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "TEST_SERIES_PV",
|
||||
SeriesName: "测试套餐系列",
|
||||
Description: "测试用",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, packageSeriesStore.Create(ctx, series))
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shop.ID,
|
||||
SeriesID: series.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_PV",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: series.ID,
|
||||
SuggestedRetailPrice: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: constants.ShelfStatusOn,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, packageStore.Create(ctx, pkg))
|
||||
|
||||
shopIDPtr := &shop.ID
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860000000000000001",
|
||||
ShopID: shopIDPtr,
|
||||
CarrierID: carrier.ID,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, iotCardStore.Create(ctx, card))
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "DEV_TEST_PV_001",
|
||||
ShopID: shopIDPtr,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, deviceStore.Create(ctx, device))
|
||||
|
||||
svc := New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
|
||||
return ctx, svc, card, device, pkg, allocation
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_ValidateCardPurchase(t *testing.T) {
|
||||
ctx, svc, card, _, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
result, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{pkg.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result.Card)
|
||||
assert.Equal(t, card.ID, result.Card.ID)
|
||||
assert.Len(t, result.Packages, 1)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, result.TotalPrice)
|
||||
})
|
||||
|
||||
t.Run("卡不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, 99999, []uint{pkg.ID})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeIotCardNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐列表为空", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{99999})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_ValidateDevicePurchase(t *testing.T) {
|
||||
ctx, svc, _, device, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
result, err := svc.ValidateDevicePurchase(ctx, device.ID, []uint{pkg.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result.Device)
|
||||
assert.Equal(t, device.ID, result.Device.ID)
|
||||
assert.Len(t, result.Packages, 1)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, result.TotalPrice)
|
||||
})
|
||||
|
||||
t.Run("设备不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateDevicePurchase(ctx, 99999, []uint{pkg.ID})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐列表为空", func(t *testing.T) {
|
||||
_, err := svc.ValidateDevicePurchase(ctx, device.ID, []uint{})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_GetPurchasePrice(t *testing.T) {
|
||||
ctx, svc, _, _, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("获取个人客户价格", func(t *testing.T) {
|
||||
price := svc.GetPurchasePrice(ctx, pkg, model.BuyerTypePersonal)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, price)
|
||||
})
|
||||
|
||||
t.Run("获取代理商价格", func(t *testing.T) {
|
||||
price := svc.GetPurchasePrice(ctx, pkg, model.BuyerTypeAgent)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, price)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user