feat: 添加设备IMEI和单卡ICCID查询接口
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled

- 新增 GET /api/admin/devices/by-imei/:imei 接口,支持通过设备号查询设备详情
- 新增 GET /api/admin/iot-cards/by-iccid/:iccid 接口,支持通过ICCID查询单卡详情
- 添加对应的 Service 层方法和 Handler
- 更新 OpenAPI 文档
- 添加集成测试并修复测试环境配置(使用环境变量)
- 归档已完成的 OpenSpec 变更记录

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 09:59:54 +08:00
parent ce0783f96e
commit 477a9fc98d
28 changed files with 1159 additions and 19 deletions

View File

@@ -40,8 +40,17 @@ type deviceTestEnv struct {
func setupDeviceTestEnv(t *testing.T) *deviceTestEnv {
t.Helper()
t.Setenv("CONFIG_ENV", "dev")
t.Setenv("CONFIG_PATH", "../../configs/config.dev.yaml")
// 设置测试环境变量
t.Setenv("JUNHONG_DATABASE_HOST", "cxd.whcxd.cn")
t.Setenv("JUNHONG_DATABASE_PORT", "16159")
t.Setenv("JUNHONG_DATABASE_USER", "erp_pgsql")
t.Setenv("JUNHONG_DATABASE_PASSWORD", "erp_2025")
t.Setenv("JUNHONG_DATABASE_DBNAME", "junhong_cmp_test")
t.Setenv("JUNHONG_REDIS_ADDRESS", "cxd.whcxd.cn")
t.Setenv("JUNHONG_REDIS_PORT", "16299")
t.Setenv("JUNHONG_REDIS_PASSWORD", "cpNbWtAaqgo1YJmbMp3h")
t.Setenv("JUNHONG_JWT_SECRET_KEY", "test_secret_key_for_integration_tests")
cfg, err := config.Load()
require.NoError(t, err)
err = config.Set(cfg)
@@ -331,3 +340,69 @@ func TestDeviceImport_TaskList(t *testing.T) {
assert.Equal(t, 0, result.Code)
})
}
func TestDevice_GetByIMEI(t *testing.T) {
env := setupDeviceTestEnv(t)
defer env.teardown()
// 创建测试设备
device := &model.Device{
DeviceNo: "TEST_IMEI_001",
DeviceName: "测试IMEI查询设备",
DeviceType: "router",
MaxSimSlots: 4,
Status: constants.DeviceStatusInStock,
}
require.NoError(t, env.db.Create(device).Error)
t.Run("通过IMEI查询设备详情-成功", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/by-imei/%s", device.DeviceNo)
req := httptest.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+env.adminToken)
resp, err := env.app.Test(req, -1)
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, ok := result.Data.(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "TEST_IMEI_001", dataMap["device_no"])
assert.Equal(t, "测试IMEI查询设备", dataMap["device_name"])
})
t.Run("通过不存在的IMEI查询-应返回错误", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/admin/devices/by-imei/NONEXISTENT_IMEI", nil)
req.Header.Set("Authorization", "Bearer "+env.adminToken)
resp, err := env.app.Test(req, -1)
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, "不存在的IMEI应返回错误码")
})
t.Run("未认证请求-应返回错误", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/devices/by-imei/%s", device.DeviceNo)
req := httptest.NewRequest("GET", url, nil)
resp, err := env.app.Test(req, -1)
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, "未认证请求应返回错误码")
})
}

View File

@@ -42,8 +42,17 @@ type iotCardTestEnv struct {
func setupIotCardTestEnv(t *testing.T) *iotCardTestEnv {
t.Helper()
t.Setenv("CONFIG_ENV", "dev")
t.Setenv("CONFIG_PATH", "../../configs/config.dev.yaml")
// 设置测试环境变量
t.Setenv("JUNHONG_DATABASE_HOST", "cxd.whcxd.cn")
t.Setenv("JUNHONG_DATABASE_PORT", "16159")
t.Setenv("JUNHONG_DATABASE_USER", "erp_pgsql")
t.Setenv("JUNHONG_DATABASE_PASSWORD", "erp_2025")
t.Setenv("JUNHONG_DATABASE_DBNAME", "junhong_cmp_test")
t.Setenv("JUNHONG_REDIS_ADDRESS", "cxd.whcxd.cn")
t.Setenv("JUNHONG_REDIS_PORT", "16299")
t.Setenv("JUNHONG_REDIS_PASSWORD", "cpNbWtAaqgo1YJmbMp3h")
t.Setenv("JUNHONG_JWT_SECRET_KEY", "test_secret_key_for_integration_tests")
cfg, err := config.Load()
require.NoError(t, err)
err = config.Set(cfg)
@@ -565,3 +574,81 @@ func startTestWorker(t *testing.T, db *gorm.DB, rdb *redis.Client, logger *zap.L
t.Logf("测试 Worker 服务器已启动")
return workerServer
}
func TestIotCard_GetByICCID(t *testing.T) {
env := setupIotCardTestEnv(t)
defer env.teardown()
// 创建测试运营商
carrier := &model.Carrier{
CarrierCode: "TEST001",
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, env.db.Create(carrier).Error)
// 创建测试 IoT 卡
card := &model.IotCard{
ICCID: "TEST_ICCID_001",
CarrierID: carrier.ID,
MSISDN: "13800000001",
CardType: "physical",
CardCategory: "normal",
CostPrice: 1000,
DistributePrice: 1500,
Status: 1,
}
require.NoError(t, env.db.Create(card).Error)
t.Run("通过ICCID查询单卡详情-成功", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
req := httptest.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+env.adminToken)
resp, err := env.app.Test(req, -1)
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, ok := result.Data.(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "TEST_ICCID_001", dataMap["iccid"])
assert.Equal(t, "13800000001", dataMap["msisdn"])
})
t.Run("通过不存在的ICCID查询-应返回错误", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/admin/iot-cards/by-iccid/NONEXISTENT_ICCID", nil)
req.Header.Set("Authorization", "Bearer "+env.adminToken)
resp, err := env.app.Test(req, -1)
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, "不存在的ICCID应返回错误码")
})
t.Run("未认证请求-应返回错误", func(t *testing.T) {
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
req := httptest.NewRequest("GET", url, nil)
resp, err := env.app.Test(req, -1)
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, "未认证请求应返回错误码")
})
}