feat: 实现卡和设备的套餐系列绑定功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m37s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m37s
- 添加 Device 和 IotCard 模型的 SeriesID 字段 - 实现 DeviceService 和 IotCardService 的套餐系列绑定逻辑 - 添加 DeviceStore 和 IotCardStore 的数据库操作方法 - 更新 API 接口和路由支持套餐系列绑定 - 创建数据库迁移脚本(000027_add_series_binding_fields) - 添加完整的单元测试和集成测试 - 更新 OpenAPI 文档 - 归档 OpenSpec 变更文档 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
@@ -474,3 +475,260 @@ func TestIotCard_GetByICCID(t *testing.T) {
|
||||
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_BatchSetSeriesBinding(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试数据
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
agentAccount := env.CreateTestAccount(fmt.Sprintf("agent_%d", time.Now().UnixNano()), "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
// 创建套餐系列和分配
|
||||
series := createTestPackageSeries(t, env, "测试系列")
|
||||
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
|
||||
|
||||
// 创建测试卡(归属于该店铺)
|
||||
timestamp := time.Now().Unix() % 1000000
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: fmt.Sprintf("TEST%06d001", timestamp), CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
{ICCID: fmt.Sprintf("TEST%06d002", timestamp), CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
{ICCID: fmt.Sprintf("TEST%06d003", timestamp), CardType: "data_card", CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
}
|
||||
for _, card := range cards {
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
}
|
||||
|
||||
t.Run("批量设置卡系列绑定-成功", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[0].ICCID, cards[1].ICCID},
|
||||
"series_allocation_id": allocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
|
||||
|
||||
// 验证响应数据
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(2), dataMap["success_count"], "应有2张卡成功绑定")
|
||||
assert.Equal(t, float64(0), dataMap["fail_count"], "应无失败")
|
||||
|
||||
// 验证数据库中数据已更新
|
||||
var updatedCard model.IotCard
|
||||
err = env.RawDB().Where("iccid = ?", cards[0].ICCID).First(&updatedCard).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, updatedCard.SeriesAllocationID)
|
||||
assert.Equal(t, allocation.ID, *updatedCard.SeriesAllocationID)
|
||||
})
|
||||
|
||||
t.Run("清除卡系列绑定-series_allocation_id=0", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[0].ICCID},
|
||||
"series_allocation_id": 0,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
// 验证数据库中绑定已清除
|
||||
var updatedCard model.IotCard
|
||||
err = env.RawDB().Where("iccid = ?", cards[0].ICCID).First(&updatedCard).Error
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, updatedCard.SeriesAllocationID, "系列分配应被清除")
|
||||
})
|
||||
|
||||
t.Run("批量设置-部分卡不存在", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[2].ICCID, "NONEXISTENT_ICCID_999"},
|
||||
"series_allocation_id": allocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
// 验证响应数据
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(1), dataMap["success_count"], "应有1张卡成功")
|
||||
assert.Equal(t, float64(1), dataMap["fail_count"], "应有1张卡失败")
|
||||
|
||||
// 验证失败列表
|
||||
failedItems := dataMap["failed_items"].([]interface{})
|
||||
assert.Len(t, failedItems, 1)
|
||||
failedItem := failedItems[0].(map[string]interface{})
|
||||
assert.Equal(t, "NONEXISTENT_ICCID_999", failedItem["iccid"])
|
||||
})
|
||||
|
||||
t.Run("设置不存在的系列分配-应失败", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[2].ICCID},
|
||||
"series_allocation_id": 999999,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "不存在的系列分配应返回错误")
|
||||
})
|
||||
|
||||
t.Run("设置禁用的系列分配-应失败", func(t *testing.T) {
|
||||
// 创建一个禁用的分配
|
||||
disabledSeries := createTestPackageSeries(t, env, "禁用系列")
|
||||
disabledAllocation := createTestAllocation(t, env, shop.ID, disabledSeries.ID, 0)
|
||||
env.TX.Model(&model.ShopSeriesAllocation{}).Where("id = ?", disabledAllocation.ID).Update("status", constants.StatusDisabled)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[2].ICCID},
|
||||
"series_allocation_id": disabledAllocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "禁用的系列分配应返回错误")
|
||||
})
|
||||
|
||||
t.Run("代理商设置其他店铺的卡-应失败", func(t *testing.T) {
|
||||
// 创建另一个店铺和卡
|
||||
otherShop := env.CreateTestShop("其他店铺", 1, nil)
|
||||
otherCard := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("OTH%010d", time.Now().Unix()%10000000000),
|
||||
CardType: "data_card",
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &otherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(otherCard).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{otherCard.ICCID},
|
||||
"series_allocation_id": allocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证全部失败(因为卡不属于当前店铺)
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(0), dataMap["success_count"], "不应有成功")
|
||||
assert.Equal(t, float64(1), dataMap["fail_count"], "应全部失败")
|
||||
})
|
||||
|
||||
t.Run("超级管理员可以设置任意店铺的卡", func(t *testing.T) {
|
||||
// 创建另一个店铺和卡
|
||||
anotherShop := env.CreateTestShop("另一个店铺", 1, nil)
|
||||
anotherCard := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("ADM%010d", time.Now().Unix()%10000000000),
|
||||
CardType: "data_card",
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &anotherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(anotherCard).Error)
|
||||
|
||||
// 为这个店铺创建系列分配
|
||||
anotherAllocation := createTestAllocation(t, env, anotherShop.ID, series.ID, 0)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{anotherCard.ICCID},
|
||||
"series_allocation_id": anotherAllocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code, "超级管理员应能设置任意店铺的卡")
|
||||
|
||||
// 验证成功
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(1), dataMap["success_count"])
|
||||
})
|
||||
|
||||
t.Run("未认证请求应返回错误", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[0].ICCID},
|
||||
"series_allocation_id": allocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.ClearAuth().Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
|
||||
})
|
||||
|
||||
t.Run("空ICCID列表-返回成功但无操作", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{},
|
||||
"series_allocation_id": allocation.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 0, result.Code, "当前实现:空列表返回成功")
|
||||
|
||||
if result.Data != nil {
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(0), dataMap["success_count"], "空列表无成功项")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user