feat: 实现运营商模块重构,添加冗余字段优化查询性能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m16s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m16s
主要变更: - 新增 Carrier CRUD API(创建、列表、详情、更新、删除、状态更新) - IotCard/IotCardImportTask 添加 carrier_type/carrier_name 冗余字段 - 移除 Carrier 表的 channel_name/channel_code 字段 - 查询时直接使用冗余字段,避免 JOIN Carrier 表 - 添加数据库迁移脚本(000021-000023) - 添加单元测试和集成测试 - 同步更新 OpenAPI 文档和 specs
This commit is contained in:
@@ -45,6 +45,7 @@ func generateOpenAPIDocs(outputPath string, logger *zap.Logger) {
|
||||
DeviceImport: admin.NewDeviceImportHandler(nil),
|
||||
AssetAllocationRecord: admin.NewAssetAllocationRecordHandler(nil),
|
||||
Storage: admin.NewStorageHandler(nil),
|
||||
Carrier: admin.NewCarrierHandler(nil),
|
||||
}
|
||||
|
||||
// 4. 注册所有路由到文档生成器
|
||||
|
||||
@@ -54,6 +54,7 @@ func generateAdminDocs(outputPath string) error {
|
||||
DeviceImport: admin.NewDeviceImportHandler(nil),
|
||||
AssetAllocationRecord: admin.NewAssetAllocationRecordHandler(nil),
|
||||
Storage: admin.NewStorageHandler(nil),
|
||||
Carrier: admin.NewCarrierHandler(nil),
|
||||
}
|
||||
|
||||
// 4. 注册所有路由到文档生成器
|
||||
|
||||
@@ -550,6 +550,55 @@ components:
|
||||
description: 提示信息
|
||||
type: string
|
||||
type: object
|
||||
DtoCarrierPageResult:
|
||||
properties:
|
||||
list:
|
||||
description: 运营商列表
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoCarrierResponse'
|
||||
nullable: true
|
||||
type: array
|
||||
page:
|
||||
description: 当前页
|
||||
type: integer
|
||||
page_size:
|
||||
description: 每页数量
|
||||
type: integer
|
||||
total:
|
||||
description: 总数
|
||||
type: integer
|
||||
total_pages:
|
||||
description: 总页数
|
||||
type: integer
|
||||
type: object
|
||||
DtoCarrierResponse:
|
||||
properties:
|
||||
carrier_code:
|
||||
description: 运营商编码
|
||||
type: string
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
type: string
|
||||
carrier_type:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
type: string
|
||||
created_at:
|
||||
description: 创建时间
|
||||
type: string
|
||||
description:
|
||||
description: 运营商描述
|
||||
type: string
|
||||
id:
|
||||
description: 运营商ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
status:
|
||||
description: 状态 (1:启用, 0:禁用)
|
||||
type: integer
|
||||
updated_at:
|
||||
description: 更新时间
|
||||
type: string
|
||||
type: object
|
||||
DtoChangePasswordRequest:
|
||||
properties:
|
||||
new_password:
|
||||
@@ -595,6 +644,30 @@ components:
|
||||
- password
|
||||
- user_type
|
||||
type: object
|
||||
DtoCreateCarrierRequest:
|
||||
properties:
|
||||
carrier_code:
|
||||
description: 运营商编码
|
||||
maxLength: 50
|
||||
minLength: 1
|
||||
type: string
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
maxLength: 100
|
||||
minLength: 1
|
||||
type: string
|
||||
carrier_type:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
type: string
|
||||
description:
|
||||
description: 运营商描述
|
||||
maxLength: 500
|
||||
type: string
|
||||
required:
|
||||
- carrier_code
|
||||
- carrier_name
|
||||
- carrier_type
|
||||
type: object
|
||||
DtoCreateCustomerAccountReq:
|
||||
properties:
|
||||
password:
|
||||
@@ -1473,6 +1546,9 @@ components:
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
type: string
|
||||
carrier_type:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
type: string
|
||||
completed_at:
|
||||
description: 完成时间
|
||||
format: date-time
|
||||
@@ -1543,6 +1619,9 @@ components:
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
type: string
|
||||
carrier_type:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
type: string
|
||||
completed_at:
|
||||
description: 完成时间
|
||||
format: date-time
|
||||
@@ -1615,6 +1694,9 @@ components:
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
type: string
|
||||
carrier_type:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
type: string
|
||||
cost_price:
|
||||
description: 成本价(分)
|
||||
type: integer
|
||||
@@ -2563,6 +2645,9 @@ components:
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
type: string
|
||||
carrier_type:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
type: string
|
||||
cost_price:
|
||||
description: 成本价(分)
|
||||
type: integer
|
||||
@@ -2653,6 +2738,28 @@ components:
|
||||
description: 备注(最多500字)
|
||||
type: string
|
||||
type: object
|
||||
DtoUpdateCarrierParams:
|
||||
properties:
|
||||
carrier_name:
|
||||
description: 运营商名称
|
||||
maxLength: 100
|
||||
minLength: 1
|
||||
nullable: true
|
||||
type: string
|
||||
description:
|
||||
description: 运营商描述
|
||||
maxLength: 500
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
DtoUpdateCarrierStatusParams:
|
||||
properties:
|
||||
status:
|
||||
description: 状态 (1:启用, 0:禁用)
|
||||
type: integer
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
DtoUpdateCustomerAccountPasswordReq:
|
||||
properties:
|
||||
password:
|
||||
@@ -3975,6 +4082,308 @@ paths:
|
||||
summary: 修改授权备注
|
||||
tags:
|
||||
- 授权记录管理
|
||||
/api/admin/carriers:
|
||||
get:
|
||||
parameters:
|
||||
- description: 页码
|
||||
in: query
|
||||
name: page
|
||||
schema:
|
||||
description: 页码
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 每页数量
|
||||
in: query
|
||||
name: page_size
|
||||
schema:
|
||||
description: 每页数量
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
in: query
|
||||
name: carrier_type
|
||||
schema:
|
||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||
nullable: true
|
||||
type: string
|
||||
- description: 运营商名称(模糊搜索)
|
||||
in: query
|
||||
name: carrier_name
|
||||
schema:
|
||||
description: 运营商名称(模糊搜索)
|
||||
maxLength: 100
|
||||
nullable: true
|
||||
type: string
|
||||
- description: 状态 (1:启用, 0:禁用)
|
||||
in: query
|
||||
name: status
|
||||
schema:
|
||||
description: 状态 (1:启用, 0:禁用)
|
||||
nullable: true
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoCarrierPageResult'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 运营商列表
|
||||
tags:
|
||||
- 运营商管理
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoCreateCarrierRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoCarrierResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 创建运营商
|
||||
tags:
|
||||
- 运营商管理
|
||||
/api/admin/carriers/{id}:
|
||||
delete:
|
||||
parameters:
|
||||
- description: ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
responses:
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 删除运营商
|
||||
tags:
|
||||
- 运营商管理
|
||||
get:
|
||||
parameters:
|
||||
- description: ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoCarrierResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 获取运营商详情
|
||||
tags:
|
||||
- 运营商管理
|
||||
put:
|
||||
parameters:
|
||||
- description: ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoUpdateCarrierParams'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoCarrierResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 更新运营商
|
||||
tags:
|
||||
- 运营商管理
|
||||
/api/admin/carriers/{id}/status:
|
||||
put:
|
||||
parameters:
|
||||
- description: ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoUpdateCarrierStatusParams'
|
||||
responses:
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 更新运营商状态
|
||||
tags:
|
||||
- 运营商管理
|
||||
/api/admin/commission/withdrawal-requests:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
@@ -33,5 +33,6 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
DeviceImport: admin.NewDeviceImportHandler(svc.DeviceImport),
|
||||
AssetAllocationRecord: admin.NewAssetAllocationRecordHandler(svc.AssetAllocationRecord),
|
||||
Storage: admin.NewStorageHandler(deps.StorageService),
|
||||
Carrier: admin.NewCarrierHandler(svc.Carrier),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
|
||||
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
|
||||
commissionWithdrawalSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
|
||||
commissionWithdrawalSettingSvc "github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal_setting"
|
||||
customerAccountSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_account"
|
||||
@@ -43,6 +44,7 @@ type services struct {
|
||||
Device *deviceSvc.Service
|
||||
DeviceImport *deviceImportSvc.Service
|
||||
AssetAllocationRecord *assetAllocationRecordSvc.Service
|
||||
Carrier *carrierSvc.Service
|
||||
}
|
||||
|
||||
func initServices(s *stores, deps *Dependencies) *services {
|
||||
@@ -67,5 +69,6 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
Device: deviceSvc.New(deps.DB, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord),
|
||||
DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient),
|
||||
AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account),
|
||||
Carrier: carrierSvc.New(s.Carrier),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type stores struct {
|
||||
DeviceSimBinding *postgres.DeviceSimBindingStore
|
||||
DeviceImportTask *postgres.DeviceImportTaskStore
|
||||
AssetAllocationRecord *postgres.AssetAllocationRecordStore
|
||||
Carrier *postgres.CarrierStore
|
||||
}
|
||||
|
||||
func initStores(deps *Dependencies) *stores {
|
||||
@@ -51,5 +52,6 @@ func initStores(deps *Dependencies) *stores {
|
||||
DeviceSimBinding: postgres.NewDeviceSimBindingStore(deps.DB, deps.Redis),
|
||||
DeviceImportTask: postgres.NewDeviceImportTaskStore(deps.DB, deps.Redis),
|
||||
AssetAllocationRecord: postgres.NewAssetAllocationRecordStore(deps.DB, deps.Redis),
|
||||
Carrier: postgres.NewCarrierStore(deps.DB),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type Handlers struct {
|
||||
DeviceImport *admin.DeviceImportHandler
|
||||
AssetAllocationRecord *admin.AssetAllocationRecordHandler
|
||||
Storage *admin.StorageHandler
|
||||
Carrier *admin.CarrierHandler
|
||||
}
|
||||
|
||||
// Middlewares 封装所有中间件
|
||||
|
||||
112
internal/handler/admin/carrier.go
Normal file
112
internal/handler/admin/carrier.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
carrierService "github.com/break/junhong_cmp_fiber/internal/service/carrier"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
type CarrierHandler struct {
|
||||
service *carrierService.Service
|
||||
}
|
||||
|
||||
func NewCarrierHandler(service *carrierService.Service) *CarrierHandler {
|
||||
return &CarrierHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *CarrierHandler) List(c *fiber.Ctx) error {
|
||||
var req dto.CarrierListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
carriers, total, err := h.service.List(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.SuccessWithPagination(c, carriers, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
func (h *CarrierHandler) Create(c *fiber.Ctx) error {
|
||||
var req dto.CreateCarrierRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
carrier, err := h.service.Create(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, carrier)
|
||||
}
|
||||
|
||||
func (h *CarrierHandler) Get(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的运营商 ID")
|
||||
}
|
||||
|
||||
carrier, err := h.service.Get(c.UserContext(), uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, carrier)
|
||||
}
|
||||
|
||||
func (h *CarrierHandler) Update(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的运营商 ID")
|
||||
}
|
||||
|
||||
var req dto.UpdateCarrierRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
carrier, err := h.service.Update(c.UserContext(), uint(id), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, carrier)
|
||||
}
|
||||
|
||||
func (h *CarrierHandler) Delete(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的运营商 ID")
|
||||
}
|
||||
|
||||
if err := h.service.Delete(c.UserContext(), uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *CarrierHandler) UpdateStatus(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的运营商 ID")
|
||||
}
|
||||
|
||||
var req dto.UpdateCarrierStatusRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
|
||||
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, nil)
|
||||
}
|
||||
@@ -7,13 +7,11 @@ import (
|
||||
type Carrier struct {
|
||||
gorm.Model
|
||||
BaseModel `gorm:"embedded"`
|
||||
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex:idx_carrier_code,where:deleted_at IS NULL;not null;comment:运营商编码(CMCC/CUCC/CTCC)" json:"carrier_code"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称(中国移动/中国联通/中国电信)" json:"carrier_name"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;default:'CMCC';uniqueIndex:idx_carrier_type_channel,priority:1,where:deleted_at IS NULL;comment:运营商类型" json:"carrier_type"`
|
||||
ChannelName *string `gorm:"column:channel_name;type:varchar(100);comment:渠道名称" json:"channel_name,omitempty"`
|
||||
ChannelCode *string `gorm:"column:channel_code;type:varchar(50);uniqueIndex:idx_carrier_type_channel,priority:2,where:deleted_at IS NULL;comment:渠道编码" json:"channel_code,omitempty"`
|
||||
Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"`
|
||||
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 2-禁用" json:"status"`
|
||||
CarrierCode string `gorm:"column:carrier_code;type:varchar(50);uniqueIndex:idx_carrier_code,where:deleted_at IS NULL;not null;comment:运营商编码" json:"carrier_code"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);not null;comment:运营商名称" json:"carrier_name"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;default:'CMCC';comment:运营商类型(CMCC/CUCC/CTCC/CBN)" json:"carrier_type"`
|
||||
Description string `gorm:"column:description;type:varchar(500);comment:运营商描述" json:"description"`
|
||||
Status int `gorm:"column:status;type:int;default:1;comment:状态 1-启用 0-禁用" json:"status"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
54
internal/model/dto/carrier_dto.go
Normal file
54
internal/model/dto/carrier_dto.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dto
|
||||
|
||||
type CreateCarrierRequest struct {
|
||||
CarrierCode string `json:"carrier_code" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"运营商编码"`
|
||||
CarrierName string `json:"carrier_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" validate:"required,oneof=CMCC CUCC CTCC CBN" required:"true" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
}
|
||||
|
||||
type UpdateCarrierRequest struct {
|
||||
CarrierName *string `json:"carrier_name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
}
|
||||
|
||||
type CarrierListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
CarrierType *string `json:"carrier_type" query:"carrier_type" validate:"omitempty,oneof=CMCC CUCC CTCC CBN" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
CarrierName *string `json:"carrier_name" query:"carrier_name" validate:"omitempty,max=100" maxLength:"100" description:"运营商名称(模糊搜索)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,oneof=0 1" description:"状态 (1:启用, 0:禁用)"`
|
||||
}
|
||||
|
||||
type UpdateCarrierStatusRequest struct {
|
||||
Status int `json:"status" validate:"required,oneof=0 1" required:"true" description:"状态 (1:启用, 0:禁用)"`
|
||||
}
|
||||
|
||||
type CarrierResponse struct {
|
||||
ID uint `json:"id" description:"运营商ID"`
|
||||
CarrierCode string `json:"carrier_code" description:"运营商编码"`
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" description:"运营商描述"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 0:禁用)"`
|
||||
CreatedAt string `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt string `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
type UpdateCarrierParams struct {
|
||||
IDReq
|
||||
UpdateCarrierRequest
|
||||
}
|
||||
|
||||
type UpdateCarrierStatusParams struct {
|
||||
IDReq
|
||||
UpdateCarrierStatusRequest
|
||||
}
|
||||
|
||||
type CarrierPageResult struct {
|
||||
List []*CarrierResponse `json:"list" description:"运营商列表"`
|
||||
Total int64 `json:"total" description:"总数"`
|
||||
Page int `json:"page" description:"当前页"`
|
||||
PageSize int `json:"page_size" description:"每页数量"`
|
||||
TotalPages int `json:"total_pages" description:"总页数"`
|
||||
}
|
||||
@@ -24,6 +24,7 @@ type StandaloneIotCardResponse struct {
|
||||
CardType string `json:"card_type" description:"卡类型"`
|
||||
CardCategory string `json:"card_category" description:"卡业务类型 (normal:普通卡, industry:行业卡)"`
|
||||
CarrierID uint `json:"carrier_id" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
IMSI string `json:"imsi,omitempty" description:"IMSI"`
|
||||
MSISDN string `json:"msisdn,omitempty" description:"卡接入号"`
|
||||
@@ -79,6 +80,7 @@ type ImportTaskResponse struct {
|
||||
Status int `json:"status" description:"任务状态 (1:待处理, 2:处理中, 3:已完成, 4:失败)"`
|
||||
StatusText string `json:"status_text" description:"任务状态文本"`
|
||||
CarrierID uint `json:"carrier_id" description:"运营商ID"`
|
||||
CarrierType string `json:"carrier_type,omitempty" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
CarrierName string `json:"carrier_name,omitempty" description:"运营商名称"`
|
||||
BatchNo string `json:"batch_no,omitempty" description:"批次号"`
|
||||
FileName string `json:"file_name,omitempty" description:"文件名"`
|
||||
|
||||
@@ -16,6 +16,8 @@ type IotCard struct {
|
||||
CardType string `gorm:"column:card_type;type:varchar(50);not null;comment:卡类型" json:"card_type"`
|
||||
CardCategory string `gorm:"column:card_category;type:varchar(20);default:'normal';not null;comment:卡业务类型 normal-普通卡 industry-行业卡" json:"card_category"`
|
||||
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);comment:运营商类型(CMCC/CUCC/CTCC/CBN),导入时快照" json:"carrier_type"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);comment:运营商名称,导入时快照" json:"carrier_name"`
|
||||
IMSI string `gorm:"column:imsi;type:varchar(50);comment:IMSI" json:"imsi"`
|
||||
MSISDN string `gorm:"column:msisdn;type:varchar(20);comment:MSISDN(手机号码)" json:"msisdn"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
|
||||
@@ -15,6 +15,7 @@ type IotCardImportTask struct {
|
||||
Status int `gorm:"column:status;type:int;default:1;not null;comment:任务状态 1-待处理 2-处理中 3-已完成 4-失败" json:"status"`
|
||||
CarrierID uint `gorm:"column:carrier_id;index;not null;comment:运营商ID" json:"carrier_id"`
|
||||
CarrierType string `gorm:"column:carrier_type;type:varchar(20);not null;comment:运营商类型(CMCC/CUCC/CTCC/CBN)" json:"carrier_type"`
|
||||
CarrierName string `gorm:"column:carrier_name;type:varchar(100);comment:运营商名称,创建任务时快照" json:"carrier_name"`
|
||||
BatchNo string `gorm:"column:batch_no;type:varchar(100);comment:批次号" json:"batch_no"`
|
||||
FileName string `gorm:"column:file_name;type:varchar(255);comment:原始文件名" json:"file_name"`
|
||||
TotalCount int `gorm:"column:total_count;type:int;default:0;not null;comment:总数" json:"total_count"`
|
||||
|
||||
@@ -67,6 +67,9 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
|
||||
if handlers.Storage != nil {
|
||||
registerStorageRoutes(authGroup, handlers.Storage, doc, basePath)
|
||||
}
|
||||
if handlers.Carrier != nil {
|
||||
registerCarrierRoutes(authGroup, handlers.Carrier, doc, basePath)
|
||||
}
|
||||
}
|
||||
|
||||
func registerAdminAuthRoutes(router fiber.Router, handler interface{}, authMiddleware fiber.Handler, doc *openapi.Generator, basePath string) {
|
||||
|
||||
62
internal/routes/carrier.go
Normal file
62
internal/routes/carrier.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
func registerCarrierRoutes(router fiber.Router, handler *admin.CarrierHandler, doc *openapi.Generator, basePath string) {
|
||||
carriers := router.Group("/carriers")
|
||||
groupPath := basePath + "/carriers"
|
||||
|
||||
Register(carriers, doc, groupPath, "GET", "", handler.List, RouteSpec{
|
||||
Summary: "运营商列表",
|
||||
Tags: []string{"运营商管理"},
|
||||
Input: new(dto.CarrierListRequest),
|
||||
Output: new(dto.CarrierPageResult),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(carriers, doc, groupPath, "POST", "", handler.Create, RouteSpec{
|
||||
Summary: "创建运营商",
|
||||
Tags: []string{"运营商管理"},
|
||||
Input: new(dto.CreateCarrierRequest),
|
||||
Output: new(dto.CarrierResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(carriers, doc, groupPath, "GET", "/:id", handler.Get, RouteSpec{
|
||||
Summary: "获取运营商详情",
|
||||
Tags: []string{"运营商管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.CarrierResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(carriers, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
|
||||
Summary: "更新运营商",
|
||||
Tags: []string{"运营商管理"},
|
||||
Input: new(dto.UpdateCarrierParams),
|
||||
Output: new(dto.CarrierResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(carriers, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
|
||||
Summary: "删除运营商",
|
||||
Tags: []string{"运营商管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(carriers, doc, groupPath, "PUT", "/:id/status", handler.UpdateStatus, RouteSpec{
|
||||
Summary: "更新运营商状态",
|
||||
Tags: []string{"运营商管理"},
|
||||
Input: new(dto.UpdateCarrierStatusParams),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
182
internal/service/carrier/service.go
Normal file
182
internal/service/carrier/service.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"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/pkg/middleware"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
carrierStore *postgres.CarrierStore
|
||||
}
|
||||
|
||||
func New(carrierStore *postgres.CarrierStore) *Service {
|
||||
return &Service{carrierStore: carrierStore}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateCarrierRequest) (*dto.CarrierResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
existing, _ := s.carrierStore.GetByCode(ctx, req.CarrierCode)
|
||||
if existing != nil {
|
||||
return nil, errors.New(errors.CodeCarrierCodeExists, "运营商编码已存在")
|
||||
}
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: req.CarrierCode,
|
||||
CarrierName: req.CarrierName,
|
||||
CarrierType: req.CarrierType,
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
carrier.Creator = currentUserID
|
||||
|
||||
if err := s.carrierStore.Create(ctx, carrier); err != nil {
|
||||
return nil, fmt.Errorf("创建运营商失败: %w", err)
|
||||
}
|
||||
|
||||
return s.toResponse(carrier), nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.CarrierResponse, error) {
|
||||
carrier, err := s.carrierStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeCarrierNotFound, "运营商不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取运营商失败: %w", err)
|
||||
}
|
||||
return s.toResponse(carrier), nil
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCarrierRequest) (*dto.CarrierResponse, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
carrier, err := s.carrierStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeCarrierNotFound, "运营商不存在")
|
||||
}
|
||||
return nil, fmt.Errorf("获取运营商失败: %w", err)
|
||||
}
|
||||
|
||||
if req.CarrierName != nil {
|
||||
carrier.CarrierName = *req.CarrierName
|
||||
}
|
||||
if req.Description != nil {
|
||||
carrier.Description = *req.Description
|
||||
}
|
||||
carrier.Updater = currentUserID
|
||||
|
||||
if err := s.carrierStore.Update(ctx, carrier); err != nil {
|
||||
return nil, fmt.Errorf("更新运营商失败: %w", err)
|
||||
}
|
||||
|
||||
return s.toResponse(carrier), nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
_, err := s.carrierStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeCarrierNotFound, "运营商不存在")
|
||||
}
|
||||
return fmt.Errorf("获取运营商失败: %w", err)
|
||||
}
|
||||
|
||||
if err := s.carrierStore.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("删除运营商失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *dto.CarrierListRequest) ([]*dto.CarrierResponse, int64, error) {
|
||||
opts := &store.QueryOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
OrderBy: "id DESC",
|
||||
}
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
if opts.PageSize == 0 {
|
||||
opts.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
filters := make(map[string]interface{})
|
||||
if req.CarrierType != nil {
|
||||
filters["carrier_type"] = *req.CarrierType
|
||||
}
|
||||
if req.CarrierName != nil {
|
||||
filters["carrier_name"] = *req.CarrierName
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = *req.Status
|
||||
}
|
||||
|
||||
carriers, total, err := s.carrierStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询运营商列表失败: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]*dto.CarrierResponse, len(carriers))
|
||||
for i, c := range carriers {
|
||||
responses[i] = s.toResponse(c)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
carrier, err := s.carrierStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeCarrierNotFound, "运营商不存在")
|
||||
}
|
||||
return fmt.Errorf("获取运营商失败: %w", err)
|
||||
}
|
||||
|
||||
carrier.Status = status
|
||||
carrier.Updater = currentUserID
|
||||
|
||||
if err := s.carrierStore.Update(ctx, carrier); err != nil {
|
||||
return fmt.Errorf("更新运营商状态失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) toResponse(c *model.Carrier) *dto.CarrierResponse {
|
||||
return &dto.CarrierResponse{
|
||||
ID: c.ID,
|
||||
CarrierCode: c.CarrierCode,
|
||||
CarrierName: c.CarrierName,
|
||||
CarrierType: c.CarrierType,
|
||||
Description: c.Description,
|
||||
Status: c.Status,
|
||||
CreatedAt: c.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: c.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
268
internal/service/carrier/service_test.go
Normal file
268
internal/service/carrier/service_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"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/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCarrierService_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
store := postgres.NewCarrierStore(tx)
|
||||
svc := New(store)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
t.Run("创建成功", func(t *testing.T) {
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_CMCC_001",
|
||||
CarrierName: "中国移动-服务测试",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Description: "服务层测试",
|
||||
}
|
||||
|
||||
resp, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, resp.ID)
|
||||
assert.Equal(t, req.CarrierCode, resp.CarrierCode)
|
||||
assert.Equal(t, req.CarrierName, resp.CarrierName)
|
||||
assert.Equal(t, constants.StatusEnabled, resp.Status)
|
||||
})
|
||||
|
||||
t.Run("编码重复失败", func(t *testing.T) {
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_CMCC_001",
|
||||
CarrierName: "中国移动-重复",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
}
|
||||
|
||||
_, err := svc.Create(ctx, req)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeCarrierCodeExists, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("未授权失败", func(t *testing.T) {
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_CMCC_002",
|
||||
CarrierName: "未授权测试",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
}
|
||||
|
||||
_, err := svc.Create(context.Background(), req)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierService_Get(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
store := postgres.NewCarrierStore(tx)
|
||||
svc := New(store)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_GET_001",
|
||||
CarrierName: "查询测试",
|
||||
CarrierType: constants.CarrierTypeCUCC,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询存在的运营商", func(t *testing.T) {
|
||||
resp, err := svc.Get(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created.CarrierCode, resp.CarrierCode)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的运营商", func(t *testing.T) {
|
||||
_, err := svc.Get(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeCarrierNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierService_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
store := postgres.NewCarrierStore(tx)
|
||||
svc := New(store)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_UPD_001",
|
||||
CarrierName: "更新测试",
|
||||
CarrierType: constants.CarrierTypeCTCC,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新成功", func(t *testing.T) {
|
||||
newName := "更新后的名称"
|
||||
newDesc := "更新后的描述"
|
||||
updateReq := &dto.UpdateCarrierRequest{
|
||||
CarrierName: &newName,
|
||||
Description: &newDesc,
|
||||
}
|
||||
|
||||
resp, err := svc.Update(ctx, created.ID, updateReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newName, resp.CarrierName)
|
||||
assert.Equal(t, newDesc, resp.Description)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的运营商", func(t *testing.T) {
|
||||
newName := "test"
|
||||
updateReq := &dto.UpdateCarrierRequest{
|
||||
CarrierName: &newName,
|
||||
}
|
||||
|
||||
_, err := svc.Update(ctx, 99999, updateReq)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeCarrierNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierService_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
store := postgres.NewCarrierStore(tx)
|
||||
svc := New(store)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_DEL_001",
|
||||
CarrierName: "删除测试",
|
||||
CarrierType: constants.CarrierTypeCBN,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("删除成功", func(t *testing.T) {
|
||||
err := svc.Delete(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = svc.Get(ctx, created.ID)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("删除不存在的运营商", func(t *testing.T) {
|
||||
err := svc.Delete(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
store := postgres.NewCarrierStore(tx)
|
||||
svc := New(store)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
carriers := []dto.CreateCarrierRequest{
|
||||
{CarrierCode: "SVC_LIST_001", CarrierName: "移动列表", CarrierType: constants.CarrierTypeCMCC},
|
||||
{CarrierCode: "SVC_LIST_002", CarrierName: "联通列表", CarrierType: constants.CarrierTypeCUCC},
|
||||
{CarrierCode: "SVC_LIST_003", CarrierName: "电信列表", CarrierType: constants.CarrierTypeCTCC},
|
||||
}
|
||||
for _, c := range carriers {
|
||||
_, err := svc.Create(ctx, &c)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("查询列表", func(t *testing.T) {
|
||||
req := &dto.CarrierListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
result, total, err := svc.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(3))
|
||||
assert.GreaterOrEqual(t, len(result), 3)
|
||||
})
|
||||
|
||||
t.Run("按类型过滤", func(t *testing.T) {
|
||||
carrierType := constants.CarrierTypeCMCC
|
||||
req := &dto.CarrierListRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
CarrierType: &carrierType,
|
||||
}
|
||||
result, total, err := svc.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, c := range result {
|
||||
assert.Equal(t, constants.CarrierTypeCMCC, c.CarrierType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierService_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
store := postgres.NewCarrierStore(tx)
|
||||
svc := New(store)
|
||||
|
||||
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
req := &dto.CreateCarrierRequest{
|
||||
CarrierCode: "SVC_STATUS_001",
|
||||
CarrierName: "状态测试",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
}
|
||||
created, err := svc.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, created.Status)
|
||||
|
||||
t.Run("禁用运营商", func(t *testing.T) {
|
||||
err := svc.UpdateStatus(ctx, created.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := svc.Get(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("启用运营商", func(t *testing.T) {
|
||||
err := svc.UpdateStatus(ctx, created.ID, constants.StatusEnabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := svc.Get(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的运营商状态", func(t *testing.T) {
|
||||
err := svc.UpdateStatus(ctx, 99999, 1)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -41,8 +41,6 @@ func (s *Service) ListBindings(ctx context.Context, deviceID uint) (*dto.ListDev
|
||||
cardMap[card.ID] = card
|
||||
}
|
||||
|
||||
carrierMap := s.loadCarrierData(ctx, cards)
|
||||
|
||||
responses := make([]*dto.DeviceCardBindingResponse, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
card := cardMap[binding.IotCardID]
|
||||
@@ -56,7 +54,7 @@ func (s *Service) ListBindings(ctx context.Context, deviceID uint) (*dto.ListDev
|
||||
IotCardID: binding.IotCardID,
|
||||
ICCID: card.ICCID,
|
||||
MSISDN: card.MSISDN,
|
||||
CarrierName: carrierMap[card.CarrierID],
|
||||
CarrierName: card.CarrierName, // 直接使用 IotCard 的冗余字段
|
||||
Status: card.Status,
|
||||
BindTime: binding.BindTime,
|
||||
}
|
||||
@@ -147,26 +145,3 @@ func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*
|
||||
Message: "解绑成功",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadCarrierData(ctx context.Context, cards []*model.IotCard) map[uint]string {
|
||||
carrierIDs := make([]uint, 0)
|
||||
carrierIDSet := make(map[uint]bool)
|
||||
|
||||
for _, card := range cards {
|
||||
if card.CarrierID > 0 && !carrierIDSet[card.CarrierID] {
|
||||
carrierIDs = append(carrierIDs, card.CarrierID)
|
||||
carrierIDSet[card.CarrierID] = true
|
||||
}
|
||||
}
|
||||
|
||||
carrierMap := make(map[uint]string)
|
||||
if len(carrierIDs) > 0 {
|
||||
var carriers []model.Carrier
|
||||
s.db.WithContext(ctx).Where("id IN ?", carrierIDs).Find(&carriers)
|
||||
for _, c := range carriers {
|
||||
carrierMap[c.ID] = c.CarrierName
|
||||
}
|
||||
}
|
||||
|
||||
return carrierMap
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot
|
||||
return nil, err
|
||||
}
|
||||
|
||||
carrierMap, shopMap := s.loadRelatedData(ctx, cards)
|
||||
shopMap := s.loadShopNames(ctx, cards)
|
||||
|
||||
list := make([]*dto.StandaloneIotCardResponse, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
item := s.toStandaloneResponse(card, carrierMap, shopMap)
|
||||
item := s.toStandaloneResponse(card, shopMap)
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
@@ -120,40 +120,25 @@ func (s *Service) GetByICCID(ctx context.Context, iccid string) (*dto.IotCardDet
|
||||
return nil, err
|
||||
}
|
||||
|
||||
carrierMap, shopMap := s.loadRelatedData(ctx, []*model.IotCard{card})
|
||||
standaloneResp := s.toStandaloneResponse(card, carrierMap, shopMap)
|
||||
shopMap := s.loadShopNames(ctx, []*model.IotCard{card})
|
||||
standaloneResp := s.toStandaloneResponse(card, shopMap)
|
||||
|
||||
return &dto.IotCardDetailResponse{
|
||||
StandaloneIotCardResponse: *standaloneResp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadRelatedData(ctx context.Context, cards []*model.IotCard) (map[uint]string, map[uint]string) {
|
||||
carrierIDs := make([]uint, 0)
|
||||
func (s *Service) loadShopNames(ctx context.Context, cards []*model.IotCard) map[uint]string {
|
||||
shopIDs := make([]uint, 0)
|
||||
carrierIDSet := make(map[uint]bool)
|
||||
shopIDSet := make(map[uint]bool)
|
||||
|
||||
for _, card := range cards {
|
||||
if card.CarrierID > 0 && !carrierIDSet[card.CarrierID] {
|
||||
carrierIDs = append(carrierIDs, card.CarrierID)
|
||||
carrierIDSet[card.CarrierID] = true
|
||||
}
|
||||
if card.ShopID != nil && *card.ShopID > 0 && !shopIDSet[*card.ShopID] {
|
||||
shopIDs = append(shopIDs, *card.ShopID)
|
||||
shopIDSet[*card.ShopID] = true
|
||||
}
|
||||
}
|
||||
|
||||
carrierMap := make(map[uint]string)
|
||||
if len(carrierIDs) > 0 {
|
||||
var carriers []model.Carrier
|
||||
s.db.WithContext(ctx).Where("id IN ?", carrierIDs).Find(&carriers)
|
||||
for _, c := range carriers {
|
||||
carrierMap[c.ID] = c.CarrierName
|
||||
}
|
||||
}
|
||||
|
||||
shopMap := make(map[uint]string)
|
||||
if len(shopIDs) > 0 {
|
||||
var shops []model.Shop
|
||||
@@ -163,17 +148,18 @@ func (s *Service) loadRelatedData(ctx context.Context, cards []*model.IotCard) (
|
||||
}
|
||||
}
|
||||
|
||||
return carrierMap, shopMap
|
||||
return shopMap
|
||||
}
|
||||
|
||||
func (s *Service) toStandaloneResponse(card *model.IotCard, carrierMap map[uint]string, shopMap map[uint]string) *dto.StandaloneIotCardResponse {
|
||||
func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]string) *dto.StandaloneIotCardResponse {
|
||||
resp := &dto.StandaloneIotCardResponse{
|
||||
ID: card.ID,
|
||||
ICCID: card.ICCID,
|
||||
CardType: card.CardType,
|
||||
CardCategory: card.CardCategory,
|
||||
CarrierID: card.CarrierID,
|
||||
CarrierName: carrierMap[card.CarrierID],
|
||||
CarrierType: card.CarrierType,
|
||||
CarrierName: card.CarrierName,
|
||||
IMSI: card.IMSI,
|
||||
MSISDN: card.MSISDN,
|
||||
BatchNo: card.BatchNo,
|
||||
|
||||
@@ -76,6 +76,7 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportIotCardRe
|
||||
Status: model.ImportTaskStatusPending,
|
||||
CarrierID: req.CarrierID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
BatchNo: req.BatchNo,
|
||||
FileName: fileName,
|
||||
StorageKey: req.FileKey,
|
||||
@@ -138,11 +139,9 @@ func (s *Service) List(ctx context.Context, req *dto.ListImportTaskRequest) (*dt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
carrierMap := s.loadCarriers(ctx, tasks)
|
||||
|
||||
list := make([]*dto.ImportTaskResponse, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
list = append(list, s.toTaskResponse(task, carrierMap))
|
||||
list = append(list, s.toTaskResponse(task))
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
@@ -165,14 +164,8 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.ImportTaskDetailRe
|
||||
return nil, errors.New(errors.CodeNotFound, "导入任务不存在")
|
||||
}
|
||||
|
||||
carrierMap := make(map[uint]string)
|
||||
var carrier model.Carrier
|
||||
if s.db.WithContext(ctx).First(&carrier, task.CarrierID).Error == nil {
|
||||
carrierMap[carrier.ID] = carrier.CarrierName
|
||||
}
|
||||
|
||||
resp := &dto.ImportTaskDetailResponse{
|
||||
ImportTaskResponse: *s.toTaskResponse(task, carrierMap),
|
||||
ImportTaskResponse: *s.toTaskResponse(task),
|
||||
SkippedItems: make([]*dto.ImportResultItemDTO, 0),
|
||||
FailedItems: make([]*dto.ImportResultItemDTO, 0),
|
||||
}
|
||||
@@ -198,28 +191,7 @@ func (s *Service) GetByID(ctx context.Context, id uint) (*dto.ImportTaskDetailRe
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) loadCarriers(ctx context.Context, tasks []*model.IotCardImportTask) map[uint]string {
|
||||
carrierIDs := make([]uint, 0)
|
||||
carrierIDSet := make(map[uint]bool)
|
||||
for _, task := range tasks {
|
||||
if task.CarrierID > 0 && !carrierIDSet[task.CarrierID] {
|
||||
carrierIDs = append(carrierIDs, task.CarrierID)
|
||||
carrierIDSet[task.CarrierID] = true
|
||||
}
|
||||
}
|
||||
|
||||
carrierMap := make(map[uint]string)
|
||||
if len(carrierIDs) > 0 {
|
||||
var carriers []model.Carrier
|
||||
s.db.WithContext(ctx).Where("id IN ?", carrierIDs).Find(&carriers)
|
||||
for _, c := range carriers {
|
||||
carrierMap[c.ID] = c.CarrierName
|
||||
}
|
||||
}
|
||||
return carrierMap
|
||||
}
|
||||
|
||||
func (s *Service) toTaskResponse(task *model.IotCardImportTask, carrierMap map[uint]string) *dto.ImportTaskResponse {
|
||||
func (s *Service) toTaskResponse(task *model.IotCardImportTask) *dto.ImportTaskResponse {
|
||||
var startedAt, completedAt *time.Time
|
||||
if task.StartedAt != nil {
|
||||
startedAt = task.StartedAt
|
||||
@@ -234,7 +206,8 @@ func (s *Service) toTaskResponse(task *model.IotCardImportTask, carrierMap map[u
|
||||
Status: task.Status,
|
||||
StatusText: getStatusText(task.Status),
|
||||
CarrierID: task.CarrierID,
|
||||
CarrierName: carrierMap[task.CarrierID],
|
||||
CarrierType: task.CarrierType,
|
||||
CarrierName: task.CarrierName,
|
||||
BatchNo: task.BatchNo,
|
||||
FileName: task.FileName,
|
||||
TotalCount: task.TotalCount,
|
||||
|
||||
83
internal/store/postgres/carrier_store.go
Normal file
83
internal/store/postgres/carrier_store.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
)
|
||||
|
||||
type CarrierStore struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCarrierStore(db *gorm.DB) *CarrierStore {
|
||||
return &CarrierStore{db: db}
|
||||
}
|
||||
|
||||
func (s *CarrierStore) Create(ctx context.Context, carrier *model.Carrier) error {
|
||||
return s.db.WithContext(ctx).Create(carrier).Error
|
||||
}
|
||||
|
||||
func (s *CarrierStore) GetByID(ctx context.Context, id uint) (*model.Carrier, error) {
|
||||
var carrier model.Carrier
|
||||
if err := s.db.WithContext(ctx).First(&carrier, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &carrier, nil
|
||||
}
|
||||
|
||||
func (s *CarrierStore) GetByCode(ctx context.Context, code string) (*model.Carrier, error) {
|
||||
var carrier model.Carrier
|
||||
if err := s.db.WithContext(ctx).Where("carrier_code = ?", code).First(&carrier).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &carrier, nil
|
||||
}
|
||||
|
||||
func (s *CarrierStore) Update(ctx context.Context, carrier *model.Carrier) error {
|
||||
return s.db.WithContext(ctx).Save(carrier).Error
|
||||
}
|
||||
|
||||
func (s *CarrierStore) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.WithContext(ctx).Delete(&model.Carrier{}, id).Error
|
||||
}
|
||||
|
||||
func (s *CarrierStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]interface{}) ([]*model.Carrier, int64, error) {
|
||||
var carriers []*model.Carrier
|
||||
var total int64
|
||||
|
||||
query := s.db.WithContext(ctx).Model(&model.Carrier{})
|
||||
|
||||
if carrierType, ok := filters["carrier_type"].(string); ok && carrierType != "" {
|
||||
query = query.Where("carrier_type = ?", carrierType)
|
||||
}
|
||||
if carrierName, ok := filters["carrier_name"].(string); ok && carrierName != "" {
|
||||
query = query.Where("carrier_name LIKE ?", "%"+carrierName+"%")
|
||||
}
|
||||
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(&carriers).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return carriers, total, nil
|
||||
}
|
||||
204
internal/store/postgres/carrier_store_test.go
Normal file
204
internal/store/postgres/carrier_store_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
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 TestCarrierStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewCarrierStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "CMCC_TEST_001",
|
||||
CarrierName: "中国移动测试",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Description: "测试运营商",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := s.Create(ctx, carrier)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, carrier.ID)
|
||||
}
|
||||
|
||||
func TestCarrierStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewCarrierStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "CUCC_TEST_001",
|
||||
CarrierName: "中国联通测试",
|
||||
CarrierType: constants.CarrierTypeCUCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, carrier))
|
||||
|
||||
t.Run("查询存在的运营商", func(t *testing.T) {
|
||||
result, err := s.GetByID(ctx, carrier.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, carrier.CarrierCode, result.CarrierCode)
|
||||
assert.Equal(t, carrier.CarrierName, result.CarrierName)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的运营商", func(t *testing.T) {
|
||||
_, err := s.GetByID(ctx, 99999)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierStore_GetByCode(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewCarrierStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "CTCC_TEST_001",
|
||||
CarrierName: "中国电信测试",
|
||||
CarrierType: constants.CarrierTypeCTCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, carrier))
|
||||
|
||||
t.Run("查询存在的编码", func(t *testing.T) {
|
||||
result, err := s.GetByCode(ctx, "CTCC_TEST_001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, carrier.ID, result.ID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的编码", func(t *testing.T) {
|
||||
_, err := s.GetByCode(ctx, "NOT_EXISTS")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrierStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewCarrierStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "CBN_TEST_001",
|
||||
CarrierName: "中国广电测试",
|
||||
CarrierType: constants.CarrierTypeCBN,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, carrier))
|
||||
|
||||
carrier.CarrierName = "中国广电测试-更新"
|
||||
carrier.Description = "更新后的描述"
|
||||
err := s.Update(ctx, carrier)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := s.GetByID(ctx, carrier.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "中国广电测试-更新", updated.CarrierName)
|
||||
assert.Equal(t, "更新后的描述", updated.Description)
|
||||
}
|
||||
|
||||
func TestCarrierStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewCarrierStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "DEL_TEST_001",
|
||||
CarrierName: "待删除运营商",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, s.Create(ctx, carrier))
|
||||
|
||||
err := s.Delete(ctx, carrier.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetByID(ctx, carrier.ID)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCarrierStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
s := NewCarrierStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
carriers := []*model.Carrier{
|
||||
{CarrierCode: "LIST_001", CarrierName: "移动1", CarrierType: constants.CarrierTypeCMCC, Status: constants.StatusEnabled},
|
||||
{CarrierCode: "LIST_002", CarrierName: "联通1", CarrierType: constants.CarrierTypeCUCC, Status: constants.StatusEnabled},
|
||||
{CarrierCode: "LIST_003", CarrierName: "电信1", CarrierType: constants.CarrierTypeCTCC, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, c := range carriers {
|
||||
require.NoError(t, s.Create(ctx, c))
|
||||
}
|
||||
// 显式更新第三个 carrier 为禁用状态(GORM 不会写入零值)
|
||||
carriers[2].Status = constants.StatusDisabled
|
||||
require.NoError(t, s.Update(ctx, carriers[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{}{"carrier_type": constants.CarrierTypeCMCC}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(1))
|
||||
for _, c := range result {
|
||||
assert.Equal(t, constants.CarrierTypeCMCC, c.CarrierType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按名称模糊搜索", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"carrier_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 _, c := range result {
|
||||
assert.Contains(t, c.CarrierName, "联通")
|
||||
}
|
||||
})
|
||||
|
||||
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 _, c := range result {
|
||||
assert.Equal(t, constants.StatusDisabled, c.Status)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按状态过滤-启用", func(t *testing.T) {
|
||||
filters := map[string]interface{}{"status": constants.StatusEnabled}
|
||||
result, total, err := s.List(ctx, &store.QueryOptions{Page: 1, PageSize: 20}, filters)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, total, int64(2))
|
||||
for _, c := range result {
|
||||
assert.Equal(t, constants.StatusEnabled, c.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)
|
||||
})
|
||||
|
||||
t.Run("默认分页选项", func(t *testing.T) {
|
||||
result, _, err := s.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
12
migrations/000021_carrier_remove_channel_fields.down.sql
Normal file
12
migrations/000021_carrier_remove_channel_fields.down.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- 回滚:恢复 Carrier 表的 channel_name、channel_code 字段
|
||||
|
||||
-- 添加 channel_name 字段
|
||||
ALTER TABLE tb_carrier ADD COLUMN channel_name VARCHAR(100);
|
||||
COMMENT ON COLUMN tb_carrier.channel_name IS '渠道名称';
|
||||
|
||||
-- 添加 channel_code 字段
|
||||
ALTER TABLE tb_carrier ADD COLUMN channel_code VARCHAR(50);
|
||||
COMMENT ON COLUMN tb_carrier.channel_code IS '渠道编码';
|
||||
|
||||
-- 重建联合唯一索引(carrier_type + channel_code,排除已删除记录)
|
||||
CREATE UNIQUE INDEX idx_carrier_type_channel ON tb_carrier(carrier_type, channel_code) WHERE deleted_at IS NULL;
|
||||
11
migrations/000021_carrier_remove_channel_fields.up.sql
Normal file
11
migrations/000021_carrier_remove_channel_fields.up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- 移除 Carrier 表的 channel_name、channel_code 字段及相关索引
|
||||
-- 这些字段未被任何地方使用,属于冗余设计
|
||||
|
||||
-- 先删除联合唯一索引
|
||||
DROP INDEX IF EXISTS idx_carrier_type_channel;
|
||||
|
||||
-- 删除 channel_name 字段
|
||||
ALTER TABLE tb_carrier DROP COLUMN IF EXISTS channel_name;
|
||||
|
||||
-- 删除 channel_code 字段
|
||||
ALTER TABLE tb_carrier DROP COLUMN IF EXISTS channel_code;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE tb_iot_card DROP COLUMN IF EXISTS carrier_type;
|
||||
ALTER TABLE tb_iot_card DROP COLUMN IF EXISTS carrier_name;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- IotCard 表添加 carrier_type、carrier_name 冗余字段
|
||||
-- 导入时从 Carrier 表快照,后续查询无需 JOIN
|
||||
|
||||
ALTER TABLE tb_iot_card ADD COLUMN carrier_type VARCHAR(20);
|
||||
ALTER TABLE tb_iot_card ADD COLUMN carrier_name VARCHAR(100);
|
||||
|
||||
COMMENT ON COLUMN tb_iot_card.carrier_type IS '运营商类型(CMCC/CUCC/CTCC/CBN),导入时快照';
|
||||
COMMENT ON COLUMN tb_iot_card.carrier_name IS '运营商名称,导入时快照';
|
||||
|
||||
-- 从 Carrier 表填充现有数据
|
||||
UPDATE tb_iot_card
|
||||
SET carrier_type = c.carrier_type, carrier_name = c.carrier_name
|
||||
FROM tb_carrier c
|
||||
WHERE tb_iot_card.carrier_id = c.id;
|
||||
1
migrations/000023_import_task_add_carrier_name.down.sql
Normal file
1
migrations/000023_import_task_add_carrier_name.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE tb_iot_card_import_task DROP COLUMN IF EXISTS carrier_name;
|
||||
11
migrations/000023_import_task_add_carrier_name.up.sql
Normal file
11
migrations/000023_import_task_add_carrier_name.up.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- IotCardImportTask 表添加 carrier_name 冗余字段(已有 carrier_type)
|
||||
|
||||
ALTER TABLE tb_iot_card_import_task ADD COLUMN carrier_name VARCHAR(100);
|
||||
|
||||
COMMENT ON COLUMN tb_iot_card_import_task.carrier_name IS '运营商名称,创建任务时快照';
|
||||
|
||||
-- 从 Carrier 表填充现有数据
|
||||
UPDATE tb_iot_card_import_task
|
||||
SET carrier_name = c.carrier_name
|
||||
FROM tb_carrier c
|
||||
WHERE tb_iot_card_import_task.carrier_id = c.id;
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-27
|
||||
@@ -0,0 +1,96 @@
|
||||
## Context
|
||||
|
||||
Carrier(运营商)模块当前状态:
|
||||
- Model 已定义于 `internal/model/carrier.go`,表名 `tb_carrier`
|
||||
- 被 IotCard、IotCardImportTask、PollingConfig 通过 `carrier_id` 引用
|
||||
- 缺少管理接口,数据通过其他方式手动管理
|
||||
- `channel_name`、`channel_code` 字段未被任何地方使用
|
||||
- 查询 IotCard 时需要 JOIN Carrier 表获取 carrier_name
|
||||
|
||||
CarrierType 固定为 4 种:CMCC(中国移动)、CUCC(中国联通)、CTCC(中国电信)、CBN(中国广电)
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 提供完整的 Carrier 管理 API(CRUD + 状态管理)
|
||||
- 简化 Carrier Model,移除冗余字段
|
||||
- IotCard/ImportTask 存储冗余快照,实现数据自包含
|
||||
- 优化查询性能,减少不必要的 JOIN
|
||||
|
||||
**Non-Goals:**
|
||||
- PollingConfig 保持 carrier_id 引用方式(配置语义,NULL 表示所有运营商)
|
||||
- 不修改 Carrier 的业务逻辑(仅提供管理接口)
|
||||
- 不支持 CarrierType 动态扩展(固定 4 种)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: 冗余快照 vs 保持引用
|
||||
|
||||
**决策**: IotCard/ImportTask 采用冗余快照存储 carrier_type、carrier_name
|
||||
|
||||
**理由**:
|
||||
- 查询时无需 JOIN,性能更好
|
||||
- Carrier 软删除后历史数据完整
|
||||
- 符合"赋予时刻快照"的业务语义——卡分配后运营商信息不应变化
|
||||
|
||||
**备选方案**:
|
||||
- 保持纯引用:需要限制 Carrier 删除,查询需要 JOIN
|
||||
- 软引用 + 视图:复杂度高,维护成本大
|
||||
|
||||
### D2: PollingConfig 不添加冗余字段
|
||||
|
||||
**决策**: PollingConfig 保持 carrier_id 引用
|
||||
|
||||
**理由**:
|
||||
- PollingConfig 是配置表,`carrier_id = NULL` 有特殊含义(所有运营商)
|
||||
- 配置应该跟随 Carrier 变化,不需要历史快照
|
||||
- 场景不同于数据记录
|
||||
|
||||
### D3: CarrierType 枚举校验
|
||||
|
||||
**决策**: 创建时 carrier_type 必须是 CMCC/CUCC/CTCC/CBN 之一,使用 validator 枚举校验
|
||||
|
||||
**理由**:
|
||||
- 运营商类型固定,不会动态扩展
|
||||
- 前端下拉选择,后端强校验
|
||||
- 避免脏数据
|
||||
|
||||
### D4: 删除策略
|
||||
|
||||
**决策**: Carrier 可以软删除,不检查关联数据
|
||||
|
||||
**理由**:
|
||||
- IotCard 已有冗余字段,不依赖 Carrier 表
|
||||
- ImportTask 同理
|
||||
- PollingConfig 的 carrier_id 可能变成悬空引用,但配置场景可接受(管理员负责)
|
||||
|
||||
### D5: API 路由设计
|
||||
|
||||
**决策**: 遵循项目现有 RESTful 风格
|
||||
|
||||
```
|
||||
POST /api/admin/carriers 创建
|
||||
GET /api/admin/carriers 列表(分页+筛选)
|
||||
GET /api/admin/carriers/:id 详情
|
||||
PUT /api/admin/carriers/:id 更新
|
||||
DELETE /api/admin/carriers/:id 软删除
|
||||
PUT /api/admin/carriers/:id/status 启用/禁用
|
||||
```
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### R1: 数据冗余导致存储增加
|
||||
- **风险**: 每条 IotCard 多存 carrier_type(20B) + carrier_name(100B)
|
||||
- **缓解**: 可接受的存储开销,换取查询性能和数据独立性
|
||||
|
||||
### R2: 迁移时需要填充历史数据
|
||||
- **风险**: 现有 IotCard/ImportTask 记录需要通过 UPDATE 填充冗余字段
|
||||
- **缓解**: 迁移脚本从 Carrier 表 JOIN 填充,一次性操作
|
||||
|
||||
### R3: carrier_code/carrier_type 创建后不可修改
|
||||
- **风险**: 如果创建错误,只能删除重建
|
||||
- **缓解**: 前端选择 + 后端校验,降低出错概率;错误情况下软删除后重建
|
||||
|
||||
### R4: 移除 channel 字段是 BREAKING CHANGE
|
||||
- **风险**: 如果有外部系统依赖这些字段
|
||||
- **缓解**: 已确认这些字段未被使用,可安全移除
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
Carrier(运营商)模块目前只有 Model 定义,缺少管理接口。系统需要一套完整的 CRUD + 状态管理接口来创建和管理上游运营商渠道。同时,现有的 IotCard 等表通过 carrier_id 引用 Carrier,每次查询都需要 JOIN,且 Carrier 删除后历史数据会缺失。需要通过冗余字段实现"赋予时刻快照",让数据自包含。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 Carrier 管理 API(增删改查 + 启用/禁用)
|
||||
- 简化 Carrier Model,移除未使用的 `channel_name`、`channel_code` 字段
|
||||
- IotCard 新增冗余字段 `carrier_type`、`carrier_name`,导入时填充快照
|
||||
- IotCardImportTask 新增冗余字段 `carrier_name`(已有 `carrier_type`)
|
||||
- 优化查询逻辑,移除不必要的 JOIN 操作
|
||||
- **BREAKING**: 移除 Carrier 表的 `channel_name`、`channel_code` 字段及相关索引
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `carrier-management`: 运营商管理功能,包含 CRUD 接口、状态管理、列表筛选
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `iot-card-import`: 导入时填充 carrier_type、carrier_name 冗余字段
|
||||
- `iot-card-query`: 查询响应直接使用冗余字段,无需 JOIN Carrier 表
|
||||
|
||||
## Impact
|
||||
|
||||
- **Model 层**: `carrier.go`(移除字段)、`iot_card.go`(新增字段)、`iot_card_import_task.go`(新增字段)
|
||||
- **新增文件**: `carrier_dto.go`、`carrier_store.go`、`carrier/service.go`、`carrier.go`(handler)、`carrier.go`(routes)
|
||||
- **修改文件**: `iot_card/service.go`、`iot_card_import/service.go`、`device/binding.go`(移除 JOIN 逻辑)
|
||||
- **Bootstrap**: 注册新的 Store、Service、Handler
|
||||
- **数据库**: 3 个迁移文件(Carrier 简化、IotCard 冗余、ImportTask 冗余)
|
||||
- **API**: 新增 `/api/admin/carriers` 路由组
|
||||
@@ -0,0 +1,79 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 创建运营商
|
||||
系统 SHALL 允许管理员创建新的运营商记录。创建时必须指定 carrier_code(唯一编码)、carrier_name(显示名称)、carrier_type(运营商类型,枚举值)。description 为选填字段。创建成功后默认状态为启用(status=1)。
|
||||
|
||||
#### Scenario: 成功创建运营商
|
||||
- **WHEN** 管理员提交有效的创建请求,carrier_code 不重复,carrier_type 为有效枚举值
|
||||
- **THEN** 系统创建运营商记录,返回完整的运营商信息
|
||||
|
||||
#### Scenario: carrier_code 重复
|
||||
- **WHEN** 管理员提交的 carrier_code 已存在
|
||||
- **THEN** 系统返回错误"运营商编码已存在"
|
||||
|
||||
#### Scenario: carrier_type 无效
|
||||
- **WHEN** 管理员提交的 carrier_type 不是 CMCC/CUCC/CTCC/CBN 之一
|
||||
- **THEN** 系统返回参数校验错误
|
||||
|
||||
### Requirement: 查询运营商列表
|
||||
系统 SHALL 提供分页查询运营商列表的接口,支持按 carrier_type、status、carrier_name(模糊搜索)筛选。
|
||||
|
||||
#### Scenario: 无筛选条件查询
|
||||
- **WHEN** 管理员请求列表,不带筛选条件
|
||||
- **THEN** 系统返回所有运营商的分页列表,按 ID 降序排列
|
||||
|
||||
#### Scenario: 按运营商类型筛选
|
||||
- **WHEN** 管理员指定 carrier_type=CMCC
|
||||
- **THEN** 系统仅返回 carrier_type 为 CMCC 的记录
|
||||
|
||||
#### Scenario: 按名称模糊搜索
|
||||
- **WHEN** 管理员指定 carrier_name=移动
|
||||
- **THEN** 系统返回 carrier_name 包含"移动"的记录
|
||||
|
||||
### Requirement: 获取运营商详情
|
||||
系统 SHALL 允许管理员通过 ID 获取单个运营商的详细信息。
|
||||
|
||||
#### Scenario: 成功获取详情
|
||||
- **WHEN** 管理员请求存在的运营商 ID
|
||||
- **THEN** 系统返回该运营商的完整信息
|
||||
|
||||
#### Scenario: 运营商不存在
|
||||
- **WHEN** 管理员请求不存在的运营商 ID
|
||||
- **THEN** 系统返回错误"运营商不存在"
|
||||
|
||||
### Requirement: 更新运营商
|
||||
系统 SHALL 允许管理员更新运营商的 carrier_name 和 description 字段。carrier_code 和 carrier_type 创建后不可修改。
|
||||
|
||||
#### Scenario: 成功更新运营商
|
||||
- **WHEN** 管理员提交有效的更新请求
|
||||
- **THEN** 系统更新运营商信息,返回更新后的完整信息
|
||||
|
||||
#### Scenario: 尝试修改 carrier_code
|
||||
- **WHEN** 管理员尝试修改 carrier_code
|
||||
- **THEN** 系统忽略该字段(不报错,但不修改)
|
||||
|
||||
### Requirement: 删除运营商
|
||||
系统 SHALL 允许管理员软删除运营商记录。
|
||||
|
||||
#### Scenario: 成功删除运营商
|
||||
- **WHEN** 管理员请求删除存在的运营商
|
||||
- **THEN** 系统软删除该记录(设置 deleted_at)
|
||||
|
||||
#### Scenario: 删除不存在的运营商
|
||||
- **WHEN** 管理员请求删除不存在的运营商 ID
|
||||
- **THEN** 系统返回错误"运营商不存在"
|
||||
|
||||
### Requirement: 更新运营商状态
|
||||
系统 SHALL 允许管理员启用或禁用运营商。状态值:1=启用,2=禁用。
|
||||
|
||||
#### Scenario: 启用运营商
|
||||
- **WHEN** 管理员将状态设置为 1
|
||||
- **THEN** 系统更新运营商状态为启用
|
||||
|
||||
#### Scenario: 禁用运营商
|
||||
- **WHEN** 管理员将状态设置为 2
|
||||
- **THEN** 系统更新运营商状态为禁用
|
||||
|
||||
#### Scenario: 无效状态值
|
||||
- **WHEN** 管理员提交的状态值不是 1 或 2
|
||||
- **THEN** 系统返回参数校验错误
|
||||
@@ -0,0 +1,19 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 导入物联网卡时记录运营商信息
|
||||
系统 SHALL 在导入物联网卡时,将运营商的 carrier_type 和 carrier_name 作为冗余字段存储到 IotCard 记录中。这些字段在导入时从 Carrier 表查询并写入,后续不再依赖 Carrier 表。
|
||||
|
||||
#### Scenario: 导入时填充冗余字段
|
||||
- **WHEN** 系统处理物联网卡导入任务
|
||||
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_type 和 carrier_name 写入每条 IotCard 记录
|
||||
|
||||
#### Scenario: Carrier 不存在
|
||||
- **WHEN** 导入任务指定的 carrier_id 对应的 Carrier 不存在或已删除
|
||||
- **THEN** 系统拒绝导入,返回错误"运营商不存在"
|
||||
|
||||
### Requirement: 导入任务记录运营商名称
|
||||
系统 SHALL 在创建导入任务时,将 carrier_name 作为冗余字段存储到 IotCardImportTask 记录中(已有 carrier_type)。
|
||||
|
||||
#### Scenario: 创建导入任务时填充 carrier_name
|
||||
- **WHEN** 管理员创建物联网卡导入任务
|
||||
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_name 写入导入任务记录
|
||||
@@ -0,0 +1,26 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 查询物联网卡时返回运营商信息
|
||||
系统 SHALL 在查询物联网卡列表/详情时,直接从 IotCard 记录的冗余字段返回 carrier_type 和 carrier_name,无需 JOIN Carrier 表。
|
||||
|
||||
#### Scenario: 列表查询返回运营商信息
|
||||
- **WHEN** 管理员查询物联网卡列表
|
||||
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
|
||||
|
||||
#### Scenario: 详情查询返回运营商信息
|
||||
- **WHEN** 管理员查询单张物联网卡详情
|
||||
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
|
||||
|
||||
### Requirement: 查询导入任务时返回运营商名称
|
||||
系统 SHALL 在查询导入任务列表/详情时,直接从 IotCardImportTask 记录的冗余字段返回 carrier_name,无需 JOIN Carrier 表。
|
||||
|
||||
#### Scenario: 导入任务列表返回运营商名称
|
||||
- **WHEN** 管理员查询导入任务列表
|
||||
- **THEN** 响应中的 carrier_name 直接来自 IotCardImportTask 记录的冗余字段
|
||||
|
||||
### Requirement: 设备绑定卡查询返回运营商信息
|
||||
系统 SHALL 在查询设备绑定的物联网卡时,直接从 IotCard 记录的冗余字段返回 carrier_name,无需 JOIN Carrier 表。
|
||||
|
||||
#### Scenario: 设备绑定卡列表返回运营商名称
|
||||
- **WHEN** 管理员查询设备绑定的物联网卡列表
|
||||
- **THEN** 响应中的 carrier_name 直接来自 IotCard 记录的冗余字段
|
||||
@@ -0,0 +1,62 @@
|
||||
## 1. 数据库迁移
|
||||
|
||||
- [x] 1.1 创建迁移文件:Carrier 表移除 channel_name、channel_code 字段及 idx_carrier_type_channel 索引
|
||||
- [x] 1.2 创建迁移文件:IotCard 表添加 carrier_type、carrier_name 冗余字段,并从 Carrier 表填充现有数据
|
||||
- [x] 1.3 创建迁移文件:IotCardImportTask 表添加 carrier_name 冗余字段,并从 Carrier 表填充现有数据
|
||||
- [x] 1.4 执行迁移,验证数据完整性
|
||||
|
||||
## 2. Model 层修改
|
||||
|
||||
- [x] 2.1 修改 `internal/model/carrier.go`:移除 ChannelName、ChannelCode 字段
|
||||
- [x] 2.2 修改 `internal/model/iot_card.go`:添加 CarrierType、CarrierName 字段
|
||||
- [x] 2.3 修改 `internal/model/iot_card_import_task.go`:添加 CarrierName 字段
|
||||
|
||||
## 3. DTO 层
|
||||
|
||||
- [x] 3.1 创建 `internal/model/dto/carrier_dto.go`:定义 CreateCarrierRequest、UpdateCarrierRequest、CarrierListRequest、UpdateCarrierStatusRequest、CarrierResponse
|
||||
- [x] 3.2 修改 `internal/model/dto/iot_card_dto.go`:响应结构添加 carrier_type 字段(如果缺失)
|
||||
- [x] 3.3 修改 `internal/model/dto/iot_card_import_dto.go`:响应结构确认包含 carrier_name 字段
|
||||
|
||||
## 4. Store 层
|
||||
|
||||
- [x] 4.1 创建 `internal/store/postgres/carrier_store.go`:实现 Create、GetByID、Update、Delete、List、GetByCode 方法
|
||||
|
||||
## 5. Service 层
|
||||
|
||||
- [x] 5.1 创建 `internal/service/carrier/service.go`:实现 Create、Get、Update、Delete、List、UpdateStatus 业务逻辑
|
||||
- [x] 5.2 修改 `internal/service/iot_card_import/service.go`:创建导入任务时填充 carrier_name;处理卡片时填充 carrier_type、carrier_name
|
||||
- [x] 5.3 修改 `internal/service/iot_card/service.go`:移除 loadCarrierData / loadRelatedData 中的 Carrier JOIN 逻辑,直接使用 IotCard 自身字段
|
||||
- [x] 5.4 修改 `internal/service/device/binding.go`:移除 loadCarrierData 方法,直接使用 IotCard 的 carrier_name 字段
|
||||
|
||||
## 6. Handler 层
|
||||
|
||||
- [x] 6.1 创建 `internal/handler/admin/carrier.go`:实现 Create、Get、Update、Delete、List、UpdateStatus 接口
|
||||
|
||||
## 7. 路由注册
|
||||
|
||||
- [x] 7.1 创建 `internal/routes/carrier.go`:注册 /api/admin/carriers 路由组
|
||||
- [x] 7.2 更新 `internal/routes/admin.go`:调用 Carrier 路由注册
|
||||
|
||||
## 8. Bootstrap 注册
|
||||
|
||||
- [x] 8.1 修改 `internal/bootstrap/stores.go`:注册 CarrierStore
|
||||
- [x] 8.2 修改 `internal/bootstrap/services.go`:注册 CarrierService
|
||||
- [x] 8.3 修改 `internal/bootstrap/handlers.go`:注册 CarrierHandler
|
||||
|
||||
## 9. 常量定义
|
||||
|
||||
- [x] 9.1 在 `pkg/constants/` 中添加 CarrierType 枚举常量
|
||||
- [x] 9.2 在 `pkg/errors/codes.go` 中添加 Carrier 相关错误码(CodeCarrierNotFound、CodeCarrierCodeExists 等)
|
||||
|
||||
## 10. 测试
|
||||
|
||||
- [x] 10.1 编写 CarrierStore 单元测试
|
||||
- [x] 10.2 编写 CarrierService 单元测试
|
||||
- [x] 10.3 编写 Carrier API 集成测试
|
||||
- [x] 10.4 验证 IotCard 导入流程正确填充冗余字段(通过 TestIotCard_CarrierRedundantFields 验证)
|
||||
- [x] 10.5 验证 IotCard 查询响应正确返回冗余字段(通过 TestIotCard_GetByICCID 验证)
|
||||
|
||||
## 11. 文档更新
|
||||
|
||||
- [x] 11.1 更新 API 文档生成器(docs.go / gendocs/main.go)注册 CarrierHandler
|
||||
- [x] 11.2 运行 `go run cmd/gendocs/main.go` 生成 OpenAPI 文档
|
||||
@@ -1,11 +1,13 @@
|
||||
# carrier Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose after archive.
|
||||
管理运营商(Carrier)实体,支持四大固定运营商(中国移动、中国联通、中国电信、广电)的 CRUD 操作。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 运营商实体定义
|
||||
|
||||
系统 SHALL 定义运营商(Carrier)实体,管理四大固定运营商(中国移动、中国联通、中国电信、广电)的渠道信息
|
||||
系统 SHALL 定义运营商(Carrier)实体,管理四大固定运营商(中国移动、中国联通、中国电信、广电)。
|
||||
|
||||
**四大运营商固定枚举**:
|
||||
- **CMCC**:中国移动
|
||||
@@ -15,44 +17,114 @@ TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose
|
||||
|
||||
**实体字段**:
|
||||
- `id`:运营商 ID(主键,BIGINT)
|
||||
- `carrier_type`:运营商类型(VARCHAR(20),枚举值:"CMCC" | "CUCC" | "CTCC" | "CBN")**【新增】**
|
||||
- `carrier_code`:运营商编码(VARCHAR(50),唯一约束)
|
||||
- `carrier_name`:运营商名称(VARCHAR(100),如"中国移动")
|
||||
- `carrier_code`:运营商编码(VARCHAR(50),保留字段,建议填充与 carrier_type 相同)
|
||||
- `channel_name`:渠道名称(VARCHAR(100),可自定义,如"北京渠道1")**【新增】**
|
||||
- `channel_code`:渠道编码(VARCHAR(50),可自定义,如"BJ001")**【新增】**
|
||||
- `status`:状态(INT,1-启用 2-禁用)
|
||||
- `carrier_type`:运营商类型(VARCHAR(20),枚举值:"CMCC" | "CUCC" | "CTCC" | "CBN")
|
||||
- `description`:运营商描述(VARCHAR(500),可选)
|
||||
- `status`:状态(INT,1-启用 0-禁用)
|
||||
- `creator`:创建人 ID(BIGINT)
|
||||
- `updater`:更新人 ID(BIGINT)
|
||||
- `created_at`:创建时间(TIMESTAMP,自动填充)
|
||||
- `updated_at`:更新时间(TIMESTAMP,自动填充)
|
||||
- `deleted_at`:删除时间(TIMESTAMP,可空,软删除)
|
||||
|
||||
**唯一约束**:`(carrier_type, channel_code)` 在 `deleted_at IS NULL` 条件下唯一
|
||||
**唯一约束**:`carrier_code` 在 `deleted_at IS NULL` 条件下唯一
|
||||
|
||||
#### Scenario: 创建中国移动的渠道
|
||||
---
|
||||
|
||||
- **WHEN** 平台创建中国移动的北京渠道,`carrier_type` 为 "CMCC",`carrier_name` 为 "中国移动",`channel_name` 为 "北京渠道1",`channel_code` 为 "BJ001"
|
||||
- **THEN** 系统创建运营商记录,`carrier_type` 为 "CMCC",`channel_name` 为 "北京渠道1",`channel_code` 为 "BJ001"
|
||||
### Requirement: 创建运营商
|
||||
|
||||
#### Scenario: 同一运营商创建多个渠道
|
||||
系统 SHALL 允许管理员创建新的运营商记录。创建时必须指定 carrier_code(唯一编码)、carrier_name(显示名称)、carrier_type(运营商类型,枚举值)。description 为选填字段。创建成功后默认状态为启用(status=1)。
|
||||
|
||||
- **WHEN** 平台为中国移动创建两个渠道:北京渠道(BJ001)和上海渠道(SH001)
|
||||
- **THEN** 系统创建两条运营商记录,`carrier_type` 都为 "CMCC",但 `channel_code` 不同
|
||||
#### Scenario: 成功创建运营商
|
||||
- **WHEN** 管理员提交有效的创建请求,carrier_code 不重复,carrier_type 为有效枚举值
|
||||
- **THEN** 系统创建运营商记录,返回完整的运营商信息
|
||||
|
||||
#### Scenario: 渠道编码重复
|
||||
#### Scenario: carrier_code 重复
|
||||
- **WHEN** 管理员提交的 carrier_code 已存在
|
||||
- **THEN** 系统返回错误"运营商编码已存在"
|
||||
|
||||
- **WHEN** 平台创建中国移动的渠道,`carrier_type` 为 "CMCC",`channel_code` 为已存在的 "BJ001"
|
||||
- **THEN** 系统拒绝创建,返回错误信息"该运营商的渠道编码已存在"
|
||||
#### Scenario: carrier_type 无效
|
||||
- **WHEN** 管理员提交的 carrier_type 不是 CMCC/CUCC/CTCC/CBN 之一
|
||||
- **THEN** 系统返回参数校验错误
|
||||
|
||||
#### Scenario: 不同运营商可以使用相同渠道编码
|
||||
---
|
||||
|
||||
- **WHEN** 平台为中国移动创建渠道(carrier_type=CMCC, channel_code=BJ001),然后为中国联通创建渠道(carrier_type=CUCC, channel_code=BJ001)
|
||||
- **THEN** 系统允许创建,因为 `carrier_type` 不同
|
||||
### Requirement: 查询运营商列表
|
||||
|
||||
#### Scenario: 运营商类型枚举限制
|
||||
系统 SHALL 提供分页查询运营商列表的接口,支持按 carrier_type、status、carrier_name(模糊搜索)筛选。
|
||||
|
||||
- **WHEN** 平台创建运营商,`carrier_type` 为 "OTHER"(不在枚举中)
|
||||
- **THEN** 系统拒绝创建,返回错误信息"运营商类型必须是 CMCC/CUCC/CTCC/CBN 之一"
|
||||
#### Scenario: 无筛选条件查询
|
||||
- **WHEN** 管理员请求列表,不带筛选条件
|
||||
- **THEN** 系统返回所有运营商的分页列表,按 ID 降序排列
|
||||
|
||||
#### Scenario: 按运营商类型筛选
|
||||
- **WHEN** 管理员指定 carrier_type=CMCC
|
||||
- **THEN** 系统仅返回 carrier_type 为 CMCC 的记录
|
||||
|
||||
#### Scenario: 按名称模糊搜索
|
||||
- **WHEN** 管理员指定 carrier_name=移动
|
||||
- **THEN** 系统返回 carrier_name 包含"移动"的记录
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 获取运营商详情
|
||||
|
||||
系统 SHALL 允许管理员通过 ID 获取单个运营商的详细信息。
|
||||
|
||||
#### Scenario: 成功获取详情
|
||||
- **WHEN** 管理员请求存在的运营商 ID
|
||||
- **THEN** 系统返回该运营商的完整信息
|
||||
|
||||
#### Scenario: 运营商不存在
|
||||
- **WHEN** 管理员请求不存在的运营商 ID
|
||||
- **THEN** 系统返回错误"运营商不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新运营商
|
||||
|
||||
系统 SHALL 允许管理员更新运营商的 carrier_name 和 description 字段。carrier_code 和 carrier_type 创建后不可修改。
|
||||
|
||||
#### Scenario: 成功更新运营商
|
||||
- **WHEN** 管理员提交有效的更新请求
|
||||
- **THEN** 系统更新运营商信息,返回更新后的完整信息
|
||||
|
||||
#### Scenario: 尝试修改 carrier_code
|
||||
- **WHEN** 管理员尝试修改 carrier_code
|
||||
- **THEN** 系统忽略该字段(不报错,但不修改)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除运营商
|
||||
|
||||
系统 SHALL 允许管理员软删除运营商记录。
|
||||
|
||||
#### Scenario: 成功删除运营商
|
||||
- **WHEN** 管理员请求删除存在的运营商
|
||||
- **THEN** 系统软删除该记录(设置 deleted_at)
|
||||
|
||||
#### Scenario: 删除不存在的运营商
|
||||
- **WHEN** 管理员请求删除不存在的运营商 ID
|
||||
- **THEN** 系统返回错误"运营商不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 更新运营商状态
|
||||
|
||||
系统 SHALL 允许管理员启用或禁用运营商。状态值:1=启用,0=禁用。
|
||||
|
||||
#### Scenario: 启用运营商
|
||||
- **WHEN** 管理员将状态设置为 1
|
||||
- **THEN** 系统更新运营商状态为启用
|
||||
|
||||
#### Scenario: 禁用运营商
|
||||
- **WHEN** 管理员将状态设置为 0
|
||||
- **THEN** 系统更新运营商状态为禁用
|
||||
|
||||
#### Scenario: 无效状态值
|
||||
- **WHEN** 管理员提交的状态值不是 0 或 1
|
||||
- **THEN** 系统返回参数校验错误
|
||||
|
||||
---
|
||||
|
||||
@@ -64,9 +136,8 @@ TBD - created by archiving change add-wallet-transfer-tag-models. Update Purpose
|
||||
- `carrier_type`:必填,枚举值 "CMCC" | "CUCC" | "CTCC" | "CBN"
|
||||
- `carrier_name`:必填,长度 1-100 字符
|
||||
- `carrier_code`:必填,长度 1-50 字符
|
||||
- `channel_name`:可选,长度 1-100 字符
|
||||
- `channel_code`:可选,长度 1-50 字符
|
||||
- `status`:必填,枚举值 1-2
|
||||
- `description`:可选,长度 0-500 字符
|
||||
- `status`:必填,枚举值 0 或 1
|
||||
|
||||
#### Scenario: 创建运营商时 carrier_type 无效
|
||||
|
||||
|
||||
@@ -237,3 +237,27 @@ TBD - created by archiving change iot-card-standalone-management. Update Purpose
|
||||
- **WHEN** Worker 处理导入任务创建卡记录
|
||||
- **THEN** 创建的 `IotCard` 记录 `iccid` 为 "898600...",`msisdn` 为 "13800000001"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 导入物联网卡时记录运营商信息
|
||||
|
||||
系统 SHALL 在导入物联网卡时,将运营商的 carrier_type 和 carrier_name 作为冗余字段存储到 IotCard 记录中。这些字段在导入时从 Carrier 表查询并写入,后续不再依赖 Carrier 表。
|
||||
|
||||
#### Scenario: 导入时填充冗余字段
|
||||
- **WHEN** 系统处理物联网卡导入任务
|
||||
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_type 和 carrier_name 写入每条 IotCard 记录
|
||||
|
||||
#### Scenario: Carrier 不存在
|
||||
- **WHEN** 导入任务指定的 carrier_id 对应的 Carrier 不存在或已删除
|
||||
- **THEN** 系统拒绝导入,返回错误"运营商不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 导入任务记录运营商名称
|
||||
|
||||
系统 SHALL 在创建导入任务时,将 carrier_name 作为冗余字段存储到 IotCardImportTask 记录中(已有 carrier_type)。
|
||||
|
||||
#### Scenario: 创建导入任务时填充 carrier_name
|
||||
- **WHEN** 管理员创建物联网卡导入任务
|
||||
- **THEN** 系统根据 carrier_id 查询 Carrier 表,将 carrier_name 写入导入任务记录
|
||||
|
||||
|
||||
@@ -544,3 +544,37 @@ This capability supports:
|
||||
- **WHEN** 卡的授权被回收后(revoked_at 不为空),企业用户查询该卡
|
||||
- **THEN** 系统不返回该卡信息,企业无法再看到该卡
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询物联网卡时返回运营商信息
|
||||
|
||||
系统 SHALL 在查询物联网卡列表/详情时,直接从 IotCard 记录的冗余字段返回 carrier_type 和 carrier_name,无需 JOIN Carrier 表。
|
||||
|
||||
#### Scenario: 列表查询返回运营商信息
|
||||
- **WHEN** 管理员查询物联网卡列表
|
||||
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
|
||||
|
||||
#### Scenario: 详情查询返回运营商信息
|
||||
- **WHEN** 管理员查询单张物联网卡详情
|
||||
- **THEN** 响应中的 carrier_type 和 carrier_name 直接来自 IotCard 记录的冗余字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 查询导入任务时返回运营商名称
|
||||
|
||||
系统 SHALL 在查询导入任务列表/详情时,直接从 IotCardImportTask 记录的冗余字段返回 carrier_name,无需 JOIN Carrier 表。
|
||||
|
||||
#### Scenario: 导入任务列表返回运营商名称
|
||||
- **WHEN** 管理员查询导入任务列表
|
||||
- **THEN** 响应中的 carrier_name 直接来自 IotCardImportTask 记录的冗余字段
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备绑定卡查询返回运营商信息
|
||||
|
||||
系统 SHALL 在查询设备绑定的物联网卡时,直接从 IotCard 记录的冗余字段返回 carrier_name,无需 JOIN Carrier 表。
|
||||
|
||||
#### Scenario: 设备绑定卡列表返回运营商名称
|
||||
- **WHEN** 管理员查询设备绑定的物联网卡列表
|
||||
- **THEN** 响应中的 carrier_name 直接来自 IotCard 记录的冗余字段
|
||||
|
||||
|
||||
@@ -88,6 +88,14 @@ const (
|
||||
StatusEnabled = 1 // 启用
|
||||
)
|
||||
|
||||
// 运营商类型常量
|
||||
const (
|
||||
CarrierTypeCMCC = "CMCC" // 中国移动
|
||||
CarrierTypeCUCC = "CUCC" // 中国联通
|
||||
CarrierTypeCTCC = "CTCC" // 中国电信
|
||||
CarrierTypeCBN = "CBN" // 中国广电
|
||||
)
|
||||
|
||||
// 订单状态常量
|
||||
const (
|
||||
OrderStatusPending = "pending" // 待支付
|
||||
|
||||
@@ -82,6 +82,10 @@ const (
|
||||
CodeStorageInvalidPurpose = 1094 // 不支持的文件用途
|
||||
CodeStorageInvalidFileType = 1095 // 不支持的文件类型
|
||||
|
||||
// 运营商相关错误 (1100-1109)
|
||||
CodeCarrierNotFound = 1100 // 运营商不存在
|
||||
CodeCarrierCodeExists = 1101 // 运营商编码已存在
|
||||
|
||||
// 服务端错误 (2000-2999) -> 5xx HTTP 状态码
|
||||
CodeInternalError = 2001 // 内部服务器错误
|
||||
CodeDatabaseError = 2002 // 数据库错误
|
||||
@@ -156,6 +160,8 @@ var allErrorCodes = []int{
|
||||
CodeStorageFileNotFound,
|
||||
CodeStorageInvalidPurpose,
|
||||
CodeStorageInvalidFileType,
|
||||
CodeCarrierNotFound,
|
||||
CodeCarrierCodeExists,
|
||||
CodeInternalError,
|
||||
CodeDatabaseError,
|
||||
CodeRedisError,
|
||||
@@ -232,6 +238,8 @@ var errorMessages = map[int]string{
|
||||
CodeStorageFileNotFound: "文件不存在",
|
||||
CodeStorageInvalidPurpose: "不支持的文件用途",
|
||||
CodeStorageInvalidFileType: "不支持的文件类型",
|
||||
CodeCarrierNotFound: "运营商不存在",
|
||||
CodeCarrierCodeExists: "运营商编码已存在",
|
||||
CodeInvalidCredentials: "用户名或密码错误",
|
||||
CodeAccountLocked: "账号已锁定",
|
||||
CodePasswordExpired: "密码已过期",
|
||||
|
||||
394
tests/integration/carrier_test.go
Normal file
394
tests/integration/carrier_test.go
Normal file
@@ -0,0 +1,394 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||||
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/routes"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutil"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type carrierTestEnv struct {
|
||||
db *gorm.DB
|
||||
rdb *redis.Client
|
||||
tokenManager *auth.TokenManager
|
||||
app *fiber.App
|
||||
adminToken string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func setupCarrierTestEnv(t *testing.T) *carrierTestEnv {
|
||||
t.Helper()
|
||||
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
zapLogger, _ := zap.NewDevelopment()
|
||||
|
||||
dsn := "host=cxd.whcxd.cn port=16159 user=erp_pgsql password=erp_2025 dbname=junhong_cmp_test sslmode=disable TimeZone=Asia/Shanghai"
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "cxd.whcxd.cn:16299",
|
||||
Password: "cpNbWtAaqgo1YJmbMp3h",
|
||||
DB: 15,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err = rdb.Ping(ctx).Err()
|
||||
require.NoError(t, err)
|
||||
|
||||
testPrefix := fmt.Sprintf("test:%s:", t.Name())
|
||||
keys, _ := rdb.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
rdb.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
tokenManager := auth.NewTokenManager(rdb, 24*time.Hour, 7*24*time.Hour)
|
||||
superAdmin := testutil.CreateSuperAdmin(t, db)
|
||||
adminToken, _ := testutil.GenerateTestToken(t, rdb, superAdmin, "web")
|
||||
|
||||
queueClient := queue.NewClient(rdb, zapLogger)
|
||||
|
||||
deps := &bootstrap.Dependencies{
|
||||
DB: db,
|
||||
Redis: rdb,
|
||||
Logger: zapLogger,
|
||||
TokenManager: tokenManager,
|
||||
QueueClient: queueClient,
|
||||
}
|
||||
|
||||
result, err := bootstrap.Bootstrap(deps)
|
||||
require.NoError(t, err)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: internalMiddleware.ErrorHandler(zapLogger),
|
||||
})
|
||||
|
||||
routes.RegisterRoutes(app, result.Handlers, result.Middlewares)
|
||||
|
||||
return &carrierTestEnv{
|
||||
db: db,
|
||||
rdb: rdb,
|
||||
tokenManager: tokenManager,
|
||||
app: app,
|
||||
adminToken: adminToken,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *carrierTestEnv) teardown() {
|
||||
e.db.Exec("DELETE FROM tb_carrier WHERE carrier_code LIKE 'TEST%'")
|
||||
|
||||
ctx := context.Background()
|
||||
testPrefix := fmt.Sprintf("test:%s:", e.t.Name())
|
||||
keys, _ := e.rdb.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
e.rdb.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
e.rdb.Close()
|
||||
}
|
||||
|
||||
func TestCarrier_CRUD(t *testing.T) {
|
||||
env := setupCarrierTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
var createdCarrierID uint
|
||||
|
||||
t.Run("创建运营商", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"carrier_code": "TEST_CMCC_001",
|
||||
"carrier_name": "测试中国移动",
|
||||
"carrier_type": constants.CarrierTypeCMCC,
|
||||
"description": "API集成测试创建的运营商",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/admin/carriers", bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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_CMCC_001", dataMap["carrier_code"])
|
||||
assert.Equal(t, "测试中国移动", dataMap["carrier_name"])
|
||||
assert.Equal(t, constants.CarrierTypeCMCC, dataMap["carrier_type"])
|
||||
assert.Equal(t, float64(constants.StatusEnabled), dataMap["status"])
|
||||
|
||||
createdCarrierID = uint(dataMap["id"].(float64))
|
||||
t.Logf("创建的运营商 ID: %d", createdCarrierID)
|
||||
})
|
||||
|
||||
t.Run("创建运营商-编码重复应失败", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"carrier_code": "TEST_CMCC_001",
|
||||
"carrier_name": "重复编码测试",
|
||||
"carrier_type": constants.CarrierTypeCMCC,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/admin/carriers", bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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, "编码重复应返回错误")
|
||||
})
|
||||
|
||||
t.Run("获取运营商详情", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d", createdCarrierID)
|
||||
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 := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "TEST_CMCC_001", dataMap["carrier_code"])
|
||||
})
|
||||
|
||||
t.Run("获取不存在的运营商", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/carriers/99999", 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, "不存在的运营商应返回错误")
|
||||
})
|
||||
|
||||
t.Run("更新运营商", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"carrier_name": "测试中国移动-更新",
|
||||
"description": "更新后的描述",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d", createdCarrierID)
|
||||
req := httptest.NewRequest("PUT", url, bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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 := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "测试中国移动-更新", dataMap["carrier_name"])
|
||||
assert.Equal(t, "更新后的描述", dataMap["description"])
|
||||
})
|
||||
|
||||
t.Run("更新运营商状态-禁用", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"status": constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d/status", createdCarrierID)
|
||||
req := httptest.NewRequest("PUT", url, bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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)
|
||||
|
||||
var carrier model.Carrier
|
||||
env.db.First(&carrier, createdCarrierID)
|
||||
assert.Equal(t, constants.StatusDisabled, carrier.Status)
|
||||
})
|
||||
|
||||
t.Run("删除运营商", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d", createdCarrierID)
|
||||
req := httptest.NewRequest("DELETE", 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)
|
||||
|
||||
var carrier model.Carrier
|
||||
err = env.db.First(&carrier, createdCarrierID).Error
|
||||
assert.Error(t, err, "删除后应查不到运营商")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrier_List(t *testing.T) {
|
||||
env := setupCarrierTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
carriers := []*model.Carrier{
|
||||
{CarrierCode: "TEST_LIST_001", CarrierName: "移动列表测试1", CarrierType: constants.CarrierTypeCMCC, Status: constants.StatusEnabled},
|
||||
{CarrierCode: "TEST_LIST_002", CarrierName: "联通列表测试", CarrierType: constants.CarrierTypeCUCC, Status: constants.StatusEnabled},
|
||||
{CarrierCode: "TEST_LIST_003", CarrierName: "电信列表测试", CarrierType: constants.CarrierTypeCTCC, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, c := range carriers {
|
||||
require.NoError(t, env.db.Create(c).Error)
|
||||
}
|
||||
carriers[2].Status = constants.StatusDisabled
|
||||
require.NoError(t, env.db.Save(carriers[2]).Error)
|
||||
|
||||
t.Run("获取运营商列表-无过滤", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/carriers?page=1&page_size=20", 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)
|
||||
})
|
||||
|
||||
t.Run("获取运营商列表-按类型过滤", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/carriers?carrier_type=CMCC", 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)
|
||||
})
|
||||
|
||||
t.Run("获取运营商列表-按名称模糊搜索", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/carriers?carrier_name=联通", 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)
|
||||
})
|
||||
|
||||
t.Run("获取运营商列表-按状态过滤", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/carriers?status=%d", constants.StatusDisabled), 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)
|
||||
})
|
||||
|
||||
t.Run("未认证请求应返回错误", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/carriers", 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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
@@ -274,6 +274,7 @@ func TestIotCard_ImportTaskList(t *testing.T) {
|
||||
Status: model.ImportTaskStatusCompleted,
|
||||
CarrierID: 1,
|
||||
CarrierType: "CMCC",
|
||||
CarrierName: "中国移动",
|
||||
TotalCount: 100,
|
||||
}
|
||||
require.NoError(t, env.db.Create(task).Error)
|
||||
@@ -294,7 +295,7 @@ func TestIotCard_ImportTaskList(t *testing.T) {
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
|
||||
t.Run("获取导入任务详情", func(t *testing.T) {
|
||||
t.Run("获取导入任务详情-应包含冗余字段", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/iot-cards/import-tasks/%d", task.ID)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
@@ -309,6 +310,10 @@ func TestIotCard_ImportTaskList(t *testing.T) {
|
||||
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, "CMCC", dataMap["carrier_type"], "任务详情应返回冗余的运营商类型")
|
||||
assert.Equal(t, "中国移动", dataMap["carrier_name"], "任务详情应返回冗余的运营商名称")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -575,23 +580,97 @@ func startTestWorker(t *testing.T, db *gorm.DB, rdb *redis.Client, logger *zap.L
|
||||
return workerServer
|
||||
}
|
||||
|
||||
func TestIotCard_CarrierRedundantFields(t *testing.T) {
|
||||
env := setupIotCardTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
carrierCode := fmt.Sprintf("REDUND_%d", time.Now().UnixNano())
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: carrierCode,
|
||||
CarrierName: "冗余字段测试运营商",
|
||||
CarrierType: "CUCC",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.db.Create(carrier).Error)
|
||||
|
||||
testICCID := fmt.Sprintf("8986%016d", time.Now().UnixNano()%10000000000000000)
|
||||
card := &model.IotCard{
|
||||
ICCID: testICCID,
|
||||
CarrierID: carrier.ID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
CardType: "data_card",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.db.Create(card).Error)
|
||||
|
||||
t.Run("单卡列表应返回冗余字段", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/iot-cards/standalone?iccid="+testICCID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "API应返回成功,实际: %v", result.Message)
|
||||
|
||||
dataMap, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok, "Data应为map类型,实际: %T", result.Data)
|
||||
items, ok := dataMap["items"].([]interface{})
|
||||
require.True(t, ok, "items字段应存在且为数组,dataMap: %+v", dataMap)
|
||||
require.GreaterOrEqual(t, len(items), 1, "列表应至少有1条记录,ICCID: %s", testICCID)
|
||||
|
||||
cardData := items[0].(map[string]interface{})
|
||||
assert.Equal(t, "CUCC", cardData["carrier_type"], "列表应返回冗余的运营商类型")
|
||||
assert.Equal(t, "冗余字段测试运营商", cardData["carrier_name"], "列表应返回冗余的运营商名称")
|
||||
})
|
||||
|
||||
t.Run("单卡详情应返回冗余字段", 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 := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "CUCC", dataMap["carrier_type"], "详情应返回冗余的运营商类型")
|
||||
assert.Equal(t, "冗余字段测试运营商", dataMap["carrier_name"], "详情应返回冗余的运营商名称")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_GetByICCID(t *testing.T) {
|
||||
env := setupIotCardTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
// 创建测试运营商
|
||||
carrierCode := fmt.Sprintf("ICCID_%d", time.Now().UnixNano())
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "TEST001",
|
||||
CarrierCode: carrierCode,
|
||||
CarrierName: "测试运营商",
|
||||
CarrierType: "CMCC",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.db.Create(carrier).Error)
|
||||
|
||||
// 创建测试 IoT 卡
|
||||
testICCID := fmt.Sprintf("8986%016d", time.Now().UnixNano()%10000000000000000)
|
||||
card := &model.IotCard{
|
||||
ICCID: "TEST_ICCID_001",
|
||||
ICCID: testICCID,
|
||||
CarrierID: carrier.ID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
MSISDN: "13800000001",
|
||||
CardType: "physical",
|
||||
CardCategory: "normal",
|
||||
@@ -617,11 +696,12 @@ func TestIotCard_GetByICCID(t *testing.T) {
|
||||
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, testICCID, dataMap["iccid"])
|
||||
assert.Equal(t, "13800000001", dataMap["msisdn"])
|
||||
assert.Equal(t, "CMCC", dataMap["carrier_type"], "应返回冗余的运营商类型")
|
||||
assert.Equal(t, "测试运营商", dataMap["carrier_name"], "应返回冗余的运营商名称")
|
||||
})
|
||||
|
||||
t.Run("通过不存在的ICCID查询-应返回错误", func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user