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

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

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

View File

@@ -52,6 +52,20 @@ func (h *DeviceHandler) GetByID(c *fiber.Ctx) error {
return response.Success(c, result)
}
func (h *DeviceHandler) GetByIMEI(c *fiber.Ctx) error {
imei := c.Params("imei")
if imei == "" {
return errors.New(errors.CodeInvalidParam, "设备号不能为空")
}
result, err := h.service.GetByDeviceNo(c.UserContext(), imei)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *DeviceHandler) Delete(c *fiber.Ctx) error {
userType := middleware.GetUserTypeFromContext(c.UserContext())
if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {

View File

@@ -33,6 +33,20 @@ func (h *IotCardHandler) ListStandalone(c *fiber.Ctx) error {
return response.SuccessWithPagination(c, result.List, result.Total, result.Page, result.PageSize)
}
func (h *IotCardHandler) GetByICCID(c *fiber.Ctx) error {
iccid := c.Params("iccid")
if iccid == "" {
return errors.New(errors.CodeInvalidParam, "ICCID不能为空")
}
result, err := h.service.GetByICCID(c.UserContext(), iccid)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *IotCardHandler) AllocateCards(c *fiber.Ctx) error {
var req dto.AllocateStandaloneCardsRequest
if err := c.BodyParser(&req); err != nil {

View File

@@ -47,6 +47,10 @@ type GetDeviceRequest struct {
ID uint `path:"id" description:"设备ID" required:"true"`
}
type GetDeviceByIMEIRequest struct {
DeviceNo string `path:"imei" description:"设备号(IMEI)" required:"true"`
}
type DeleteDeviceRequest struct {
ID uint `path:"id" description:"设备ID" required:"true"`
}

View File

@@ -116,3 +116,11 @@ type ImportTaskDetailResponse struct {
type GetImportTaskRequest struct {
ID uint `path:"id" description:"任务ID" required:"true"`
}
type GetIotCardByICCIDRequest struct {
ICCID string `path:"iccid" description:"ICCID" required:"true"`
}
type IotCardDetailResponse struct {
StandaloneIotCardResponse
}

View File

@@ -28,6 +28,14 @@ func registerDeviceRoutes(router fiber.Router, handler *admin.DeviceHandler, imp
Auth: true,
})
Register(devices, doc, groupPath, "GET", "/by-imei/:imei", handler.GetByIMEI, RouteSpec{
Summary: "通过设备号查询设备详情",
Tags: []string{"设备管理"},
Input: new(dto.GetDeviceByIMEIRequest),
Output: new(dto.DeviceResponse),
Auth: true,
})
Register(devices, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
Summary: "删除设备",
Description: "仅平台用户可操作。删除设备时自动解绑所有卡(卡不会被删除)。",

View File

@@ -20,6 +20,14 @@ func registerIotCardRoutes(router fiber.Router, handler *admin.IotCardHandler, i
Auth: true,
})
Register(iotCards, doc, groupPath, "GET", "/by-iccid/:iccid", handler.GetByICCID, RouteSpec{
Summary: "通过ICCID查询单卡详情",
Tags: []string{"IoT卡管理"},
Input: new(dto.GetIotCardByICCIDRequest),
Output: new(dto.IotCardDetailResponse),
Auth: true,
})
Register(iotCards, doc, groupPath, "POST", "/import", importHandler.Import, RouteSpec{
Summary: "批量导入IoT卡ICCID+MSISDN",
Description: `## ⚠️ 接口变更说明BREAKING CHANGE

View File

@@ -134,6 +134,25 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.DeviceResponse, error)
return s.toDeviceResponse(device, shopMap, bindingCounts), nil
}
// GetByDeviceNo 通过设备号获取设备详情
func (s *Service) GetByDeviceNo(ctx context.Context, deviceNo string) (*dto.DeviceResponse, error) {
device, err := s.deviceStore.GetByDeviceNo(ctx, deviceNo)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
}
return nil, err
}
shopMap := s.loadShopData(ctx, []*model.Device{device})
bindingCounts, err := s.getBindingCounts(ctx, []uint{device.ID})
if err != nil {
return nil, err
}
return s.toDeviceResponse(device, shopMap, bindingCounts), nil
}
func (s *Service) Delete(ctx context.Context, id uint) error {
device, err := s.deviceStore.GetByID(ctx, id)
if err != nil {

View File

@@ -110,6 +110,24 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot
}, nil
}
// GetByICCID 通过 ICCID 获取单卡详情
func (s *Service) GetByICCID(ctx context.Context, iccid string) (*dto.IotCardDetailResponse, error) {
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "IoT卡不存在")
}
return nil, err
}
carrierMap, shopMap := s.loadRelatedData(ctx, []*model.IotCard{card})
standaloneResp := s.toStandaloneResponse(card, carrierMap, 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)
shopIDs := make([]uint, 0)