All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m42s
新增物联网卡独立管理模块,支持单卡查询、批量导入和状态管理。主要变更包括: 功能特性: - 新增物联网卡 CRUD 接口(查询、分页列表、删除) - 支持 CSV/Excel 批量导入物联网卡 - 实现异步导入任务处理和进度跟踪 - 新增 ICCID 号码格式校验器(支持 Luhn 算法) - 新增 CSV 文件解析工具(支持编码检测和错误处理) 数据库变更: - 移除 iot_card 和 device 表的 owner_id/owner_type 字段 - 新增 iot_card_import_task 导入任务表 - 为导入任务添加运营商类型字段 测试覆盖: - 新增 IoT 卡 Store 层单元测试 - 新增 IoT 卡导入任务单元测试 - 新增 IoT 卡集成测试(包含导入流程测试) - 新增 CSV 工具和 ICCID 校验器测试 文档更新: - 更新 OpenAPI 文档(新增 7 个 IoT 卡接口) - 归档 OpenSpec 变更提案 - 更新 API 文档规范和生成器指南 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
172 lines
4.3 KiB
Go
172 lines
4.3 KiB
Go
package iot_card
|
|
|
|
import (
|
|
"context"
|
|
|
|
"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"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Service struct {
|
|
db *gorm.DB
|
|
iotCardStore *postgres.IotCardStore
|
|
}
|
|
|
|
func New(db *gorm.DB, iotCardStore *postgres.IotCardStore) *Service {
|
|
return &Service{
|
|
db: db,
|
|
iotCardStore: iotCardStore,
|
|
}
|
|
}
|
|
|
|
func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIotCardRequest) (*dto.ListStandaloneIotCardResponse, error) {
|
|
page := req.Page
|
|
pageSize := req.PageSize
|
|
if page == 0 {
|
|
page = 1
|
|
}
|
|
if pageSize == 0 {
|
|
pageSize = constants.DefaultPageSize
|
|
}
|
|
|
|
opts := &store.QueryOptions{
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}
|
|
|
|
filters := make(map[string]interface{})
|
|
if req.Status != nil {
|
|
filters["status"] = *req.Status
|
|
}
|
|
if req.CarrierID != nil {
|
|
filters["carrier_id"] = *req.CarrierID
|
|
}
|
|
if req.ShopID != nil {
|
|
filters["shop_id"] = *req.ShopID
|
|
}
|
|
if req.ICCID != "" {
|
|
filters["iccid"] = req.ICCID
|
|
}
|
|
if req.MSISDN != "" {
|
|
filters["msisdn"] = req.MSISDN
|
|
}
|
|
if req.BatchNo != "" {
|
|
filters["batch_no"] = req.BatchNo
|
|
}
|
|
if req.PackageID != nil {
|
|
filters["package_id"] = *req.PackageID
|
|
}
|
|
if req.IsDistributed != nil {
|
|
filters["is_distributed"] = *req.IsDistributed
|
|
}
|
|
if req.ICCIDStart != "" {
|
|
filters["iccid_start"] = req.ICCIDStart
|
|
}
|
|
if req.ICCIDEnd != "" {
|
|
filters["iccid_end"] = req.ICCIDEnd
|
|
}
|
|
if req.IsReplaced != nil {
|
|
filters["is_replaced"] = *req.IsReplaced
|
|
}
|
|
|
|
cards, total, err := s.iotCardStore.ListStandalone(ctx, opts, filters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
carrierMap, shopMap := s.loadRelatedData(ctx, cards)
|
|
|
|
list := make([]*dto.StandaloneIotCardResponse, 0, len(cards))
|
|
for _, card := range cards {
|
|
item := s.toStandaloneResponse(card, carrierMap, shopMap)
|
|
list = append(list, item)
|
|
}
|
|
|
|
totalPages := int(total) / pageSize
|
|
if int(total)%pageSize > 0 {
|
|
totalPages++
|
|
}
|
|
|
|
return &dto.ListStandaloneIotCardResponse{
|
|
List: list,
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
TotalPages: totalPages,
|
|
}, 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)
|
|
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
|
|
s.db.WithContext(ctx).Where("id IN ?", shopIDs).Find(&shops)
|
|
for _, shop := range shops {
|
|
shopMap[shop.ID] = shop.ShopName
|
|
}
|
|
}
|
|
|
|
return carrierMap, shopMap
|
|
}
|
|
|
|
func (s *Service) toStandaloneResponse(card *model.IotCard, carrierMap map[uint]string, 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],
|
|
IMSI: card.IMSI,
|
|
MSISDN: card.MSISDN,
|
|
BatchNo: card.BatchNo,
|
|
Supplier: card.Supplier,
|
|
CostPrice: card.CostPrice,
|
|
DistributePrice: card.DistributePrice,
|
|
Status: card.Status,
|
|
ShopID: card.ShopID,
|
|
ActivatedAt: card.ActivatedAt,
|
|
ActivationStatus: card.ActivationStatus,
|
|
RealNameStatus: card.RealNameStatus,
|
|
NetworkStatus: card.NetworkStatus,
|
|
DataUsageMB: card.DataUsageMB,
|
|
CreatedAt: card.CreatedAt,
|
|
UpdatedAt: card.UpdatedAt,
|
|
}
|
|
|
|
if card.ShopID != nil && *card.ShopID > 0 {
|
|
resp.ShopName = shopMap[*card.ShopID]
|
|
}
|
|
|
|
return resp
|
|
}
|