feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
- 新增套餐系列管理 (CRUD + 状态切换) - 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态) - 清理 8 个废弃分佣模型及对应数据库表 - Package 模型新增建议成本价、建议售价、上架状态字段 - 完整的 Store/Service/Handler 三层实现 - 包含单元测试和集成测试 - 归档 add-package-module change - 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
This commit is contained in:
@@ -2,7 +2,9 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
@@ -11,6 +13,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func uniqueICCIDPrefix() string {
|
||||
return fmt.Sprintf("T%d", time.Now().UnixNano()%1000000000)
|
||||
}
|
||||
|
||||
func TestIotCardStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
@@ -94,15 +100,16 @@ func TestIotCardStore_ListStandalone(t *testing.T) {
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
prefix := uniqueICCIDPrefix()
|
||||
standaloneCards := []*model.IotCard{
|
||||
{ICCID: "89860012345678903001", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: "89860012345678903002", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: "89860012345678903003", CardType: "data_card", CarrierID: 2, Status: 2},
|
||||
{ICCID: prefix + "0001", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: prefix + "0002", CardType: "data_card", CarrierID: 1, Status: 1},
|
||||
{ICCID: prefix + "0003", CardType: "data_card", CarrierID: 2, Status: 2},
|
||||
}
|
||||
require.NoError(t, s.CreateBatch(ctx, standaloneCards))
|
||||
|
||||
boundCard := &model.IotCard{
|
||||
ICCID: "89860012345678903004",
|
||||
ICCID: prefix + "0004",
|
||||
CardType: "data_card",
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
@@ -117,7 +124,8 @@ func TestIotCardStore_ListStandalone(t *testing.T) {
|
||||
require.NoError(t, tx.Create(binding).Error)
|
||||
|
||||
t.Run("查询所有单卡", func(t *testing.T) {
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, nil)
|
||||
filters := map[string]interface{}{"iccid": prefix}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, cards, 3)
|
||||
@@ -128,7 +136,7 @@ func TestIotCardStore_ListStandalone(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("按运营商ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"carrier_id": uint(1)}
|
||||
filters := map[string]interface{}{"carrier_id": uint(1), "iccid": prefix}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
@@ -138,7 +146,7 @@ func TestIotCardStore_ListStandalone(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("按状态过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": 2}
|
||||
filters := map[string]interface{}{"status": 2, "iccid": prefix}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
@@ -147,26 +155,28 @@ func TestIotCardStore_ListStandalone(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("按ICCID模糊查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"iccid": "903001"}
|
||||
filters := map[string]interface{}{"iccid": prefix + "0001"}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Contains(t, cards[0].ICCID, "903001")
|
||||
assert.Contains(t, cards[0].ICCID, prefix+"0001")
|
||||
})
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 2}, nil)
|
||||
filters := map[string]interface{}{"iccid": prefix}
|
||||
cards, total, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 1, PageSize: 2}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, cards, 2)
|
||||
|
||||
cards2, _, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 2, PageSize: 2}, nil)
|
||||
cards2, _, err := s.ListStandalone(ctx, &store.QueryOptions{Page: 2, PageSize: 2}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cards2, 1)
|
||||
})
|
||||
|
||||
t.Run("默认分页选项", func(t *testing.T) {
|
||||
cards, total, err := s.ListStandalone(ctx, nil, nil)
|
||||
filters := map[string]interface{}{"iccid": prefix}
|
||||
cards, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(3), total)
|
||||
assert.Len(t, cards, 3)
|
||||
@@ -181,39 +191,43 @@ func TestIotCardStore_ListStandalone_Filters(t *testing.T) {
|
||||
s := NewIotCardStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
shopID := uint(100)
|
||||
prefix := uniqueICCIDPrefix()
|
||||
batchPrefix := "B" + prefix
|
||||
msisdnPrefix := "199" + prefix[1:8]
|
||||
shopID := uint(time.Now().UnixNano() % 1000000)
|
||||
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: "89860012345678904001", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shopID, BatchNo: "BATCH001", MSISDN: "13800000001"},
|
||||
{ICCID: "89860012345678904002", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: nil, BatchNo: "BATCH001", MSISDN: "13800000002"},
|
||||
{ICCID: "89860012345678904003", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: nil, BatchNo: "BATCH002", MSISDN: "13800000003"},
|
||||
{ICCID: prefix + "A001", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shopID, BatchNo: batchPrefix + "01", MSISDN: msisdnPrefix + "01"},
|
||||
{ICCID: prefix + "A002", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: nil, BatchNo: batchPrefix + "01", MSISDN: msisdnPrefix + "02"},
|
||||
{ICCID: prefix + "A003", CardType: "data_card", CarrierID: 1, Status: 1, ShopID: nil, BatchNo: batchPrefix + "02", MSISDN: msisdnPrefix + "03"},
|
||||
}
|
||||
require.NoError(t, s.CreateBatch(ctx, cards))
|
||||
|
||||
t.Run("按店铺ID过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"shop_id": shopID}
|
||||
cards, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
filters := map[string]interface{}{"shop_id": shopID, "iccid": prefix}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, shopID, *cards[0].ShopID)
|
||||
assert.Equal(t, shopID, *result[0].ShopID)
|
||||
})
|
||||
|
||||
t.Run("按批次号过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"batch_no": "BATCH001"}
|
||||
filters := map[string]interface{}{"batch_no": batchPrefix + "01", "iccid": prefix}
|
||||
_, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
})
|
||||
|
||||
t.Run("按MSISDN模糊查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"msisdn": "000001"}
|
||||
filters := map[string]interface{}{"msisdn": msisdnPrefix + "01"}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Contains(t, result[0].MSISDN, "000001")
|
||||
assert.Contains(t, result[0].MSISDN, msisdnPrefix+"01")
|
||||
})
|
||||
|
||||
t.Run("已分销过滤-true", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"is_distributed": true}
|
||||
filters := map[string]interface{}{"is_distributed": true, "iccid": prefix}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
@@ -221,7 +235,7 @@ func TestIotCardStore_ListStandalone_Filters(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("已分销过滤-false", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"is_distributed": false}
|
||||
filters := map[string]interface{}{"is_distributed": false, "iccid": prefix}
|
||||
result, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
@@ -232,8 +246,8 @@ func TestIotCardStore_ListStandalone_Filters(t *testing.T) {
|
||||
|
||||
t.Run("ICCID范围查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"iccid_start": "89860012345678904001",
|
||||
"iccid_end": "89860012345678904002",
|
||||
"iccid_start": prefix + "A001",
|
||||
"iccid_end": prefix + "A002",
|
||||
}
|
||||
_, total, err := s.ListStandalone(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
|
||||
84
internal/store/postgres/package_series_store.go
Normal file
84
internal/store/postgres/package_series_store.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
)
|
||||
|
||||
type PackageSeriesStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPackageSeriesStore(db *gorm.DB) *PackageSeriesStore {
|
||||
return &PackageSeriesStore{db: db}
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) Create(ctx context.Context, series *model.PackageSeries) error {
|
||||
return s.db.WithContext(ctx).Create(series).Error
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) GetByID(ctx context.Context, id uint) (*model.PackageSeries, error) {
|
||||
var series model.PackageSeries
|
||||
if err := s.db.WithContext(ctx).First(&series, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &series, nil
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) GetByCode(ctx context.Context, code string) (*model.PackageSeries, error) {
|
||||
var series model.PackageSeries
|
||||
if err := s.db.WithContext(ctx).Where("series_code = ?", code).First(&series).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &series, nil
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) Update(ctx context.Context, series *model.PackageSeries) error {
|
||||
return s.db.WithContext(ctx).Save(series).Error
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.PackageSeries{}, id).Error
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.PackageSeries, int64, error) {
|
||||
var seriesList []*model.PackageSeries
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.PackageSeries{})
|
||||
|
||||
if seriesName, ok := filters["series_name"].(string); ok && seriesName != "" {
|
||||
query = query.Where("series_name LIKE ?", "%"+seriesName+"%")
|
||||
}
|
||||
if status, ok := filters["status"]; ok {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = store.DefaultQueryOptions()
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
}
|
||||
|
||||
if err := query.Find(&seriesList).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return seriesList, total, nil
|
||||
}
|
||||
|
||||
func (s *PackageSeriesStore) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
return s.db.WithContext(ctx).Model(&model.PackageSeries{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
191
internal/store/postgres/package_series_store_test.go
Normal file
191
internal/store/postgres/package_series_store_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPackageSeriesStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_TEST_001",
|
||||
SeriesName: "测试系列",
|
||||
Description: "测试描述",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, series)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, series.ID)
|
||||
}
|
||||
|
||||
func TestPackageSeriesStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_TEST_002",
|
||||
SeriesName: "测试系列2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, series))
|
||||
|
||||
t.Run("查询存在的系列", func(t *testing.T) {
|
||||
result, err := s.GetByID(ctx, series.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, series.SeriesCode, result.SeriesCode)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的系列", func(t *testing.T) {
|
||||
_, err := s.GetByID(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageSeriesStore_GetByCode(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_TEST_003",
|
||||
SeriesName: "测试系列3",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, series))
|
||||
|
||||
t.Run("查询存在的编码", func(t *testing.T) {
|
||||
result, err := s.GetByCode(ctx, "SERIES_TEST_003")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, series.ID, result.ID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的编码", func(t *testing.T) {
|
||||
_, err := s.GetByCode(ctx, "NOT_EXISTS")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageSeriesStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_TEST_004",
|
||||
SeriesName: "测试系列4",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, series))
|
||||
|
||||
series.SeriesName = "测试系列4-更新"
|
||||
series.Description = "更新后的描述"
|
||||
err := s.Update(ctx, series)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, series.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "测试系列4-更新", updated.SeriesName)
|
||||
assert.Equal(t, "更新后的描述", updated.Description)
|
||||
}
|
||||
|
||||
func TestPackageSeriesStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "SERIES_DEL_001",
|
||||
SeriesName: "待删除系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, series))
|
||||
|
||||
err := s.Delete(ctx, series.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetByID(ctx, series.ID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPackageSeriesStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
seriesList := []*model.PackageSeries{
|
||||
{SeriesCode: "LIST_S_001", SeriesName: "基础套餐", Status: constants.StatusEnabled},
|
||||
{SeriesCode: "LIST_S_002", SeriesName: "高级套餐", Status: constants.StatusEnabled},
|
||||
{SeriesCode: "LIST_S_003", SeriesName: "企业套餐", Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, series := range seriesList {
|
||||
require.NoError(t, s.Create(ctx, series))
|
||||
}
|
||||
seriesList[2].Status = constants.StatusDisabled
|
||||
require.NoError(t, s.Update(ctx, seriesList[2]))
|
||||
|
||||
t.Run("查询所有系列", func(t *testing.T) {
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.GreaterOrEqual(t, len(result), 3)
|
||||
})
|
||||
|
||||
t.Run("按名称模糊搜索", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"series_name": "高级"}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, series := range result {
|
||||
assert.Contains(t, series.SeriesName, "高级")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": constants.StatusDisabled}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, series := range result {
|
||||
assert.Equal(t, constants.StatusDisabled, series.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 2}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.LessOrEqual(t, len(result), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageSeriesStore_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageSeriesStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: "STATUS_TEST_001",
|
||||
SeriesName: "状态测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, series))
|
||||
|
||||
err := s.UpdateStatus(ctx, series.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, series.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
}
|
||||
97
internal/store/postgres/package_store.go
Normal file
97
internal/store/postgres/package_store.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
)
|
||||
|
||||
type PackageStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPackageStore(db *gorm.DB) *PackageStore {
|
||||
return &PackageStore{db: db}
|
||||
}
|
||||
|
||||
func (s *PackageStore) Create(ctx context.Context, pkg *model.Package) error {
|
||||
return s.db.WithContext(ctx).Create(pkg).Error
|
||||
}
|
||||
|
||||
func (s *PackageStore) GetByID(ctx context.Context, id uint) (*model.Package, error) {
|
||||
var pkg model.Package
|
||||
if err := s.db.WithContext(ctx).First(&pkg, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pkg, nil
|
||||
}
|
||||
|
||||
func (s *PackageStore) GetByCode(ctx context.Context, code string) (*model.Package, error) {
|
||||
var pkg model.Package
|
||||
if err := s.db.WithContext(ctx).Where("package_code = ?", code).First(&pkg).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pkg, nil
|
||||
}
|
||||
|
||||
func (s *PackageStore) Update(ctx context.Context, pkg *model.Package) error {
|
||||
return s.db.WithContext(ctx).Save(pkg).Error
|
||||
}
|
||||
|
||||
func (s *PackageStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.Package{}, id).Error
|
||||
}
|
||||
|
||||
func (s *PackageStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.Package, int64, error) {
|
||||
var packages []*model.Package
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.Package{})
|
||||
|
||||
if packageName, ok := filters["package_name"].(string); ok && packageName != "" {
|
||||
query = query.Where("package_name LIKE ?", "%"+packageName+"%")
|
||||
}
|
||||
if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
|
||||
query = query.Where("series_id = ?", seriesID)
|
||||
}
|
||||
if status, ok := filters["status"]; ok {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if shelfStatus, ok := filters["shelf_status"]; ok {
|
||||
query = query.Where("shelf_status = ?", shelfStatus)
|
||||
}
|
||||
if packageType, ok := filters["package_type"].(string); ok && packageType != "" {
|
||||
query = query.Where("package_type = ?", packageType)
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = store.DefaultQueryOptions()
|
||||
}
|
||||
offset := (opts.Page - 1) * opts.PageSize
|
||||
query = query.Offset(offset).Limit(opts.PageSize)
|
||||
|
||||
if opts.OrderBy != "" {
|
||||
query = query.Order(opts.OrderBy)
|
||||
}
|
||||
|
||||
if err := query.Find(&packages).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return packages, total, nil
|
||||
}
|
||||
|
||||
func (s *PackageStore) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
return s.db.WithContext(ctx).Model(&model.Package{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
|
||||
func (s *PackageStore) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus int) error {
|
||||
return s.db.WithContext(ctx).Model(&model.Package{}).Where("id = ?", id).Update("shelf_status", shelfStatus).Error
|
||||
}
|
||||
332
internal/store/postgres/package_store_test.go
Normal file
332
internal/store/postgres/package_store_test.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPackageStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "PKG_TEST_001",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, pkg)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, pkg.ID)
|
||||
}
|
||||
|
||||
func TestPackageStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "PKG_TEST_002",
|
||||
PackageName: "测试套餐2",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 2048,
|
||||
DataAmountMB: 2048,
|
||||
Price: 19900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
t.Run("查询存在的套餐", func(t *testing.T) {
|
||||
result, err := s.GetByID(ctx, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pkg.PackageCode, result.PackageCode)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的套餐", func(t *testing.T) {
|
||||
_, err := s.GetByID(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageStore_GetByCode(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "PKG_TEST_003",
|
||||
PackageName: "测试套餐3",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 3072,
|
||||
DataAmountMB: 3072,
|
||||
Price: 29900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
t.Run("查询存在的编码", func(t *testing.T) {
|
||||
result, err := s.GetByCode(ctx, "PKG_TEST_003")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pkg.ID, result.ID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的编码", func(t *testing.T) {
|
||||
_, err := s.GetByCode(ctx, "NOT_EXISTS")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "PKG_TEST_004",
|
||||
PackageName: "测试套餐4",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 4096,
|
||||
DataAmountMB: 4096,
|
||||
Price: 39900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
pkg.PackageName = "测试套餐4-更新"
|
||||
pkg.Price = 49900
|
||||
err := s.Update(ctx, pkg)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "测试套餐4-更新", updated.PackageName)
|
||||
assert.Equal(t, int64(49900), updated.Price)
|
||||
}
|
||||
|
||||
func TestPackageStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "PKG_DEL_001",
|
||||
PackageName: "待删除套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
err := s.Delete(ctx, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetByID(ctx, pkg.ID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPackageStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkgList := []*model.Package{
|
||||
{
|
||||
PackageCode: "LIST_P_001",
|
||||
PackageName: "基础套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
},
|
||||
{
|
||||
PackageCode: "LIST_P_002",
|
||||
PackageName: "高级套餐",
|
||||
SeriesID: 2,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
DataType: "real",
|
||||
RealDataMB: 10240,
|
||||
DataAmountMB: 10240,
|
||||
Price: 99900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
},
|
||||
{
|
||||
PackageCode: "LIST_P_003",
|
||||
PackageName: "企业套餐",
|
||||
SeriesID: 3,
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
DataType: "virtual",
|
||||
VirtualDataMB: 5120,
|
||||
DataAmountMB: 5120,
|
||||
Price: 4900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
},
|
||||
}
|
||||
for _, pkg := range pkgList {
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
}
|
||||
pkgList[2].Status = constants.StatusDisabled
|
||||
require.NoError(t, s.Update(ctx, pkgList[2]))
|
||||
|
||||
t.Run("查询所有套餐", func(t *testing.T) {
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.GreaterOrEqual(t, len(result), 3)
|
||||
})
|
||||
|
||||
t.Run("按名称模糊搜索", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"package_name": "高级"}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, pkg := range result {
|
||||
assert.Contains(t, pkg.PackageName, "高级")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按系列筛选", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"series_id": uint(2)}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, pkg := range result {
|
||||
assert.Equal(t, uint(2), pkg.SeriesID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": constants.StatusDisabled}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, pkg := range result {
|
||||
assert.Equal(t, constants.StatusDisabled, pkg.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按上架状态过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"shelf_status": 2}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, pkg := range result {
|
||||
assert.Equal(t, 2, pkg.ShelfStatus)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按类型过滤", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"package_type": "addon"}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, pkg := range result {
|
||||
assert.Equal(t, "addon", pkg.PackageType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 2}, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.LessOrEqual(t, len(result), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPackageStore_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "STATUS_TEST_001",
|
||||
PackageName: "状态测试套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
err := s.UpdateStatus(ctx, pkg.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
}
|
||||
|
||||
func TestPackageStore_UpdateShelfStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewPackageStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "SHELF_TEST_001",
|
||||
PackageName: "上架测试套餐",
|
||||
SeriesID: 1,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataType: "real",
|
||||
RealDataMB: 1024,
|
||||
DataAmountMB: 1024,
|
||||
Price: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, pkg))
|
||||
|
||||
err := s.UpdateShelfStatus(ctx, pkg.ID, 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, pkg.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, updated.ShelfStatus)
|
||||
}
|
||||
Reference in New Issue
Block a user