企业授权增强与资产列表扩展
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m9s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m9s
- 企业卡授权唯一约束:新增 DB 迁移(000154),卡级部分唯一索引防止同一张卡被多个企业同时持有,Service 层新增跨企业冲突检测 - 单卡列表新增 network_status 过滤参数 - 单卡/设备列表新增 asset_status、asset_status_name、generation 响应字段 - 单卡/设备列表新增企业维度过滤(authorized_enterprise_id、is_authorized_to_enterprise)及响应中企业授权信息(批量加载,无 N+1) - 主钱包流水/退款列表新增 asset_identifier 精确过滤参数 - 企业卡授权/收回接口升级为三模式(list/range/filter),企业设备授权/收回升级为双模式(list/filter) - 升级 sonic v1.14.2 → v1.15.2 以兼容 Go 1.26 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,8 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
iotCard.SetRealnameActivator(packageActivation)
|
||||
iotCard.SetStopResumeService(stopResumeService)
|
||||
iotCard.SetDeviceSimBindingStore(s.DeviceSimBinding)
|
||||
iotCard.SetEnterpriseCardAuthStore(s.EnterpriseCardAuthorization)
|
||||
iotCard.SetEnterpriseStore(s.Enterprise)
|
||||
iotCard.SetRedisClient(deps.Redis)
|
||||
device := deviceSvc.New(
|
||||
deps.DB,
|
||||
@@ -187,6 +189,8 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
deps.GatewayClient,
|
||||
s.AssetIdentifier,
|
||||
assetAudit,
|
||||
s.EnterpriseDeviceAuthorization,
|
||||
s.Enterprise,
|
||||
)
|
||||
operationPassword := operationPasswordSvc.New(deps.Redis)
|
||||
shopCommission := shopCommissionSvc.New(s.Shop, s.Account, s.AgentWallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.AgentWalletTransaction, deps.DB, deps.Logger)
|
||||
|
||||
@@ -18,8 +18,10 @@ type ListDeviceRequest struct {
|
||||
Manufacturer string `json:"manufacturer" query:"manufacturer" validate:"omitempty,max=255" maxLength:"255" description:"制造商(模糊查询)"`
|
||||
CreatedAtStart *time.Time `json:"created_at_start" query:"created_at_start" description:"创建时间起始"`
|
||||
CreatedAtEnd *time.Time `json:"created_at_end" query:"created_at_end" description:"创建时间结束"`
|
||||
HasActivePackage *bool `json:"has_active_package" query:"has_active_package" description:"是否有生效中的套餐(true:有生效中主套餐, false:无生效中主套餐)"`
|
||||
Keyword string `json:"keyword" query:"keyword" validate:"omitempty,max=100" maxLength:"100" description:"关键字搜索,匹配虚拟号或IMEI(模糊查询,与virtual_no/imei独立)"`
|
||||
HasActivePackage *bool `json:"has_active_package" query:"has_active_package" description:"是否有生效中的套餐(true:有生效中主套餐, false:无生效中主套餐)"`
|
||||
Keyword string `json:"keyword" query:"keyword" validate:"omitempty,max=100" maxLength:"100" description:"关键字搜索,匹配虚拟号或IMEI(模糊查询,与virtual_no/imei独立)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" query:"authorized_enterprise_id" description:"按有效授权企业ID过滤(只返回当前授权给该企业的设备)"`
|
||||
IsAuthorizedToEnterprise *bool `json:"is_authorized_to_enterprise" query:"is_authorized_to_enterprise" description:"企业授权状态过滤 (true:已授权给某企业, false:未授权任何企业)"`
|
||||
}
|
||||
|
||||
type DeviceResponse struct {
|
||||
@@ -52,6 +54,13 @@ type DeviceResponse struct {
|
||||
SoftwareVersion string `json:"software_version" description:"固件版本号"`
|
||||
SwitchMode string `json:"switch_mode" description:"切卡模式:0=自动,1=手动"`
|
||||
LastGatewaySyncAt *time.Time `json:"last_gateway_sync_at" description:"最后 sync-info 同步时间"`
|
||||
// asset_status=3 表示该资产已换货且尚未执行"旧资产转新",不再流通
|
||||
// generation>1 表示该资产历史上曾执行过换货后转新,目前仍在使用
|
||||
AssetStatus int `json:"asset_status" description:"业务状态 (1:在库, 2:已销售, 3:已换货, 4:已停用)"`
|
||||
AssetStatusName string `json:"asset_status_name" description:"业务状态名称(中文)"`
|
||||
Generation int `json:"generation" description:"资产世代编号(初始值1,每次换货转新后+1)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" description:"当前有效授权的企业ID,未授权时为 null"`
|
||||
AuthorizedEnterpriseName string `json:"authorized_enterprise_name" description:"当前有效授权的企业名称,未授权时为空字符串"`
|
||||
}
|
||||
|
||||
type ListDeviceResponse struct {
|
||||
|
||||
@@ -33,9 +33,17 @@ type AllocateCardsPreviewResp struct {
|
||||
}
|
||||
|
||||
type AllocateCardsReq struct {
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
ICCIDs []string `json:"iccids" validate:"required,min=1,max=1000,dive,required" required:"true" description:"需要授权的 ICCID 列表"`
|
||||
Remark string `json:"remark" validate:"max=500" description:"授权备注"`
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
SelectionType string `json:"selection_type" validate:"required,oneof=list range filter" required:"true" description:"选取模式 (list:指定ICCID列表, range:号段范围, filter:按条件过滤)"`
|
||||
ICCIDs []string `json:"iccids" validate:"omitempty,max=1000,dive,required" description:"[list模式] ICCID列表(最多1000个)"`
|
||||
ICCIDStart string `json:"iccid_start" validate:"omitempty,max=20" maxLength:"20" description:"[range模式] 号段起始ICCID"`
|
||||
ICCIDEnd string `json:"iccid_end" validate:"omitempty,max=20" maxLength:"20" description:"[range模式] 号段结束ICCID"`
|
||||
ICCID string `json:"iccid" validate:"omitempty,max=20" maxLength:"20" description:"[filter模式] ICCID模糊匹配"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"[filter模式] 批次号"`
|
||||
CarrierID *uint `json:"carrier_id" description:"[filter模式] 运营商ID"`
|
||||
ShopID *uint `json:"shop_id" description:"[filter模式] 店铺ID"`
|
||||
ShopIDs []uint `json:"shop_ids" description:"[filter模式] 店铺ID列表"`
|
||||
Remark string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"授权备注(所有模式均可选)"`
|
||||
}
|
||||
|
||||
type AllocateCardsResp struct {
|
||||
@@ -45,8 +53,14 @@ type AllocateCardsResp struct {
|
||||
}
|
||||
|
||||
type RecallCardsReq struct {
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
ICCIDs []string `json:"iccids" validate:"required,min=1,max=1000,dive,required" required:"true" description:"需要回收授权的 ICCID 列表"`
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
SelectionType string `json:"selection_type" validate:"required,oneof=list range filter" required:"true" description:"选取模式 (list:指定ICCID列表, range:号段范围, filter:按条件过滤)"`
|
||||
ICCIDs []string `json:"iccids" validate:"omitempty,max=1000,dive,required" description:"[list模式] 需要回收授权的ICCID列表"`
|
||||
ICCIDStart string `json:"iccid_start" validate:"omitempty,max=20" maxLength:"20" description:"[range模式] 号段起始ICCID"`
|
||||
ICCIDEnd string `json:"iccid_end" validate:"omitempty,max=20" maxLength:"20" description:"[range模式] 号段结束ICCID"`
|
||||
ICCID string `json:"iccid" validate:"omitempty,max=20" maxLength:"20" description:"[filter模式] ICCID模糊匹配"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"[filter模式] 批次号"`
|
||||
CarrierID *uint `json:"carrier_id" description:"[filter模式] 运营商ID"`
|
||||
}
|
||||
|
||||
type RecalledDevice struct {
|
||||
|
||||
@@ -2,10 +2,15 @@ package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// AllocateDevicesReq 企业授权设备请求,支持 list/filter 两种模式
|
||||
type AllocateDevicesReq struct {
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
DeviceNos []string `json:"device_nos" validate:"required,min=1,max=100" description:"设备号列表(最多100个)"`
|
||||
Remark string `json:"remark" validate:"max=500" description:"授权备注"`
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
SelectionType string `json:"selection_type" validate:"required,oneof=list filter" required:"true" description:"选取模式 (list:指定设备号列表, filter:按条件过滤)"`
|
||||
DeviceNos []string `json:"device_nos" validate:"omitempty,max=100" description:"[list模式] 设备号列表(最多100个)"`
|
||||
VirtualNo string `json:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"[filter模式] 设备虚拟号(模糊匹配)"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"[filter模式] 批次号"`
|
||||
ShopID *uint `json:"shop_id" description:"[filter模式] 店铺ID"`
|
||||
Remark string `json:"remark" validate:"omitempty,max=500" maxLength:"500" description:"授权备注(所有模式均可选)"`
|
||||
}
|
||||
|
||||
type AllocateDevicesResp struct {
|
||||
@@ -26,9 +31,13 @@ type AuthorizedDeviceItem struct {
|
||||
CardCount int `json:"card_count" description:"绑定卡数量"`
|
||||
}
|
||||
|
||||
// RecallDevicesReq 企业收回设备授权请求,支持 list/filter 两种模式
|
||||
type RecallDevicesReq struct {
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
DeviceNos []string `json:"device_nos" validate:"required,min=1,max=100" description:"设备号列表(最多100个)"`
|
||||
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
|
||||
SelectionType string `json:"selection_type" validate:"required,oneof=list filter" required:"true" description:"选取模式 (list:指定设备号列表, filter:按条件过滤)"`
|
||||
DeviceNos []string `json:"device_nos" validate:"omitempty,max=100" description:"[list模式] 设备号列表(最多100个)"`
|
||||
VirtualNo string `json:"virtual_no" validate:"omitempty,max=100" maxLength:"100" description:"[filter模式] 设备虚拟号(模糊匹配)"`
|
||||
BatchNo string `json:"batch_no" validate:"omitempty,max=100" maxLength:"100" description:"[filter模式] 批次号"`
|
||||
}
|
||||
|
||||
type RecallDevicesResp struct {
|
||||
|
||||
@@ -23,6 +23,9 @@ type ListStandaloneIotCardRequest struct {
|
||||
ICCIDEnd string `json:"iccid_end" query:"iccid_end" validate:"omitempty,max=20" maxLength:"20" description:"ICCID结束号"`
|
||||
CarrierName string `json:"carrier_name" query:"carrier_name" validate:"omitempty,max=100" maxLength:"100" description:"运营商名称(模糊查询)"`
|
||||
Keyword string `json:"keyword" query:"keyword" validate:"omitempty,max=100" maxLength:"100" description:"关键字搜索,匹配ICCID或卡虚拟号(模糊查询,与iccid/virtual_no独立)"`
|
||||
NetworkStatus *int `json:"network_status" query:"network_status" validate:"omitempty,min=0,max=1" minimum:"0" maximum:"1" description:"网络状态 (0:停机, 1:开机)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" query:"authorized_enterprise_id" description:"按有效授权企业ID过滤(只返回当前授权给该企业的卡)"`
|
||||
IsAuthorizedToEnterprise *bool `json:"is_authorized_to_enterprise" query:"is_authorized_to_enterprise" description:"企业授权状态过滤 (true:已授权给某企业, false:未授权任何企业)"`
|
||||
}
|
||||
|
||||
type StandaloneIotCardResponse struct {
|
||||
@@ -62,8 +65,15 @@ type StandaloneIotCardResponse struct {
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
DeviceVirtualNo string `json:"device_virtual_no" description:"绑定设备的虚拟号(未绑定时为空字符串)"`
|
||||
RealnamePolicy string `json:"realname_policy" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
// asset_status=3 表示该资产已换货且尚未执行"旧资产转新",不再流通
|
||||
// generation>1 表示该资产历史上曾执行过换货后转新,目前仍在使用
|
||||
AssetStatus int `json:"asset_status" description:"业务状态 (1:在库, 2:已销售, 3:已换货, 4:已停用)"`
|
||||
AssetStatusName string `json:"asset_status_name" description:"业务状态名称(中文)"`
|
||||
Generation int `json:"generation" description:"资产世代编号(初始值1,每次换货转新后+1)"`
|
||||
AuthorizedEnterpriseID *uint `json:"authorized_enterprise_id" description:"当前有效授权的企业ID,未授权时为 null"`
|
||||
AuthorizedEnterpriseName string `json:"authorized_enterprise_name" description:"当前有效授权的企业名称,未授权时为空字符串"`
|
||||
CreatedAt time.Time `json:"created_at" description:"创建时间"`
|
||||
UpdatedAt time.Time `json:"updated_at" description:"更新时间"`
|
||||
}
|
||||
|
||||
type ListStandaloneIotCardResponse struct {
|
||||
|
||||
@@ -46,11 +46,12 @@ type ReturnRefundRequest struct {
|
||||
|
||||
// RefundListRequest 退款申请列表查询请求
|
||||
type RefundListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码(默认1)"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量(默认20,最大100)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回)"`
|
||||
OrderID *uint `json:"order_id" query:"order_id" validate:"omitempty" description:"关联订单ID"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"店铺ID"`
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码(默认1)"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量(默认20,最大100)"`
|
||||
Status *int `json:"status" query:"status" validate:"omitempty,min=1,max=4" minimum:"1" maximum:"4" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回)"`
|
||||
OrderID *uint `json:"order_id" query:"order_id" validate:"omitempty" description:"关联订单ID"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" validate:"omitempty" description:"店铺ID"`
|
||||
AssetIdentifier string `json:"asset_identifier" query:"asset_identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识精确检索(ICCID 或 设备虚拟号,非空时精确匹配)"`
|
||||
}
|
||||
|
||||
// RefundResponse 退款申请详情响应
|
||||
|
||||
@@ -50,6 +50,7 @@ type MainWalletTransactionListRequest struct {
|
||||
TransactionType string `json:"transaction_type" query:"transaction_type" validate:"omitempty,max=50" maxLength:"50" description:"交易类型过滤(recharge:充值入账, deduct:套餐扣款, refund:退款)"`
|
||||
StartDate string `json:"start_date" query:"start_date" validate:"omitempty" description:"开始日期(YYYY-MM-DD)"`
|
||||
EndDate string `json:"end_date" query:"end_date" validate:"omitempty" description:"结束日期(YYYY-MM-DD)"`
|
||||
AssetIdentifier string `json:"asset_identifier" query:"asset_identifier" validate:"omitempty,max=100" maxLength:"100" description:"资产标识精确检索(ICCID 或 设备虚拟号,非空时精确匹配)"`
|
||||
}
|
||||
|
||||
// MainWalletTransactionItem 主钱包流水记录项
|
||||
|
||||
@@ -20,19 +20,21 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
deviceStore *postgres.DeviceStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
shopStore *postgres.ShopStore
|
||||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
gatewayClient *gateway.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService AssetAuditService
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
deviceStore *postgres.DeviceStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
shopStore *postgres.ShopStore
|
||||
assetAllocationRecordStore *postgres.AssetAllocationRecordStore
|
||||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
gatewayClient *gateway.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService AssetAuditService
|
||||
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -49,21 +51,25 @@ func New(
|
||||
gatewayClient *gateway.Client,
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
assetAuditService AssetAuditService,
|
||||
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore,
|
||||
enterpriseStore *postgres.EnterpriseStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
redis: rds,
|
||||
deviceStore: deviceStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
iotCardStore: iotCardStore,
|
||||
shopStore: shopStore,
|
||||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
gatewayClient: gatewayClient,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
assetAuditService: assetAuditService,
|
||||
db: db,
|
||||
redis: rds,
|
||||
deviceStore: deviceStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
iotCardStore: iotCardStore,
|
||||
shopStore: shopStore,
|
||||
assetAllocationRecordStore: assetAllocationRecordStore,
|
||||
shopPackageAllocationStore: shopPackageAllocationStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
gatewayClient: gatewayClient,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
assetAuditService: assetAuditService,
|
||||
enterpriseDeviceAuthStore: enterpriseDeviceAuthStore,
|
||||
enterpriseStore: enterpriseStore,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +139,12 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li
|
||||
if req.HasActivePackage != nil {
|
||||
filters["has_active_package"] = *req.HasActivePackage
|
||||
}
|
||||
if req.AuthorizedEnterpriseID != nil {
|
||||
filters["authorized_enterprise_id"] = *req.AuthorizedEnterpriseID
|
||||
}
|
||||
if req.IsAuthorizedToEnterprise != nil {
|
||||
filters["is_authorized_to_enterprise"] = *req.IsAuthorizedToEnterprise
|
||||
}
|
||||
|
||||
devices, total, err := s.deviceStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
@@ -151,9 +163,31 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 批量查询有效企业授权(避免 N+1 查询)
|
||||
deviceEnterpriseMap, err := s.enterpriseDeviceAuthStore.GetActiveAuthEnterpriseByDeviceIDs(ctx, deviceIDs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备企业授权失败")
|
||||
}
|
||||
enterpriseIDs := make([]uint, 0)
|
||||
seen := make(map[uint]struct{})
|
||||
for _, eid := range deviceEnterpriseMap {
|
||||
if _, ok := seen[eid]; !ok {
|
||||
seen[eid] = struct{}{}
|
||||
enterpriseIDs = append(enterpriseIDs, eid)
|
||||
}
|
||||
}
|
||||
enterpriseNameMap, err := s.enterpriseStore.GetNameMapByIDs(ctx, enterpriseIDs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询企业名称失败")
|
||||
}
|
||||
|
||||
list := make([]*dto.DeviceResponse, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
item := s.toDeviceResponse(device, shopMap, seriesMap, bindingCounts, activationStatuses)
|
||||
if eid, ok := deviceEnterpriseMap[device.ID]; ok {
|
||||
item.AuthorizedEnterpriseID = &eid
|
||||
item.AuthorizedEnterpriseName = enterpriseNameMap[eid]
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
@@ -941,6 +975,9 @@ func (s *Service) toDeviceResponse(device *model.Device, shopMap map[uint]string
|
||||
SoftwareVersion: device.SoftwareVersion,
|
||||
SwitchMode: device.SwitchMode,
|
||||
LastGatewaySyncAt: device.LastGatewaySyncAt,
|
||||
AssetStatus: device.AssetStatus,
|
||||
AssetStatusName: constants.GetAssetStatusName(device.AssetStatus),
|
||||
Generation: device.Generation,
|
||||
}
|
||||
|
||||
if device.ShopID != nil && *device.ShopID > 0 {
|
||||
|
||||
@@ -112,6 +112,92 @@ func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, r
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// resolveICCIDsForAllocate 根据选取模式解析待授权的 ICCID 列表
|
||||
func (s *Service) resolveICCIDsForAllocate(ctx context.Context, req *dto.AllocateCardsReq) ([]string, error) {
|
||||
switch req.SelectionType {
|
||||
case "list":
|
||||
return req.ICCIDs, nil
|
||||
case "range":
|
||||
cards, err := s.iotCardStore.GetStandaloneByICCIDRangeForAuth(ctx, req.ICCIDStart, req.ICCIDEnd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "按号段查询卡失败")
|
||||
}
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
iccids = append(iccids, c.ICCID)
|
||||
}
|
||||
return iccids, nil
|
||||
case "filter":
|
||||
filters := map[string]any{}
|
||||
if req.ICCID != "" {
|
||||
filters["iccid"] = req.ICCID
|
||||
}
|
||||
if req.BatchNo != "" {
|
||||
filters["batch_no"] = req.BatchNo
|
||||
}
|
||||
if req.CarrierID != nil {
|
||||
filters["carrier_id"] = *req.CarrierID
|
||||
}
|
||||
if req.ShopID != nil {
|
||||
filters["shop_id"] = *req.ShopID
|
||||
}
|
||||
if len(req.ShopIDs) > 0 {
|
||||
filters["shop_ids"] = req.ShopIDs
|
||||
}
|
||||
cards, err := s.iotCardStore.GetStandaloneByAuthFilters(ctx, filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "按条件查询卡失败")
|
||||
}
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
iccids = append(iccids, c.ICCID)
|
||||
}
|
||||
return iccids, nil
|
||||
default:
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的选取模式")
|
||||
}
|
||||
}
|
||||
|
||||
// resolveICCIDsForRecall 根据选取模式解析待回收的 ICCID 列表
|
||||
func (s *Service) resolveICCIDsForRecall(ctx context.Context, enterpriseID uint, req *dto.RecallCardsReq) ([]string, error) {
|
||||
switch req.SelectionType {
|
||||
case "list":
|
||||
return req.ICCIDs, nil
|
||||
case "range":
|
||||
cards, err := s.iotCardStore.GetAuthorizedStandaloneByICCIDRange(ctx, enterpriseID, req.ICCIDStart, req.ICCIDEnd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "按号段查询已授权卡失败")
|
||||
}
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
iccids = append(iccids, c.ICCID)
|
||||
}
|
||||
return iccids, nil
|
||||
case "filter":
|
||||
filters := map[string]any{}
|
||||
if req.ICCID != "" {
|
||||
filters["iccid"] = req.ICCID
|
||||
}
|
||||
if req.BatchNo != "" {
|
||||
filters["batch_no"] = req.BatchNo
|
||||
}
|
||||
if req.CarrierID != nil {
|
||||
filters["carrier_id"] = *req.CarrierID
|
||||
}
|
||||
cards, err := s.iotCardStore.GetAuthorizedStandaloneByFilters(ctx, enterpriseID, filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "按条件查询已授权卡失败")
|
||||
}
|
||||
iccids := make([]string, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
iccids = append(iccids, c.ICCID)
|
||||
}
|
||||
return iccids, nil
|
||||
default:
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的选取模式")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsReq) (*dto.AllocateCardsResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
@@ -123,7 +209,12 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
|
||||
iccids, err := s.resolveICCIDsForAllocate(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: iccids})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,9 +224,31 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
FailCount: len(preview.FailedItems),
|
||||
}
|
||||
|
||||
cardIDsToAllocate := make([]uint, 0)
|
||||
// 构建 cardID -> ICCID 映射,用于组装失败原因
|
||||
cardIDToICCID := make(map[uint]string, len(preview.StandaloneCards))
|
||||
allCandidateIDs := make([]uint, 0, len(preview.StandaloneCards))
|
||||
for _, card := range preview.StandaloneCards {
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
|
||||
cardIDToICCID[card.IotCardID] = card.ICCID
|
||||
allCandidateIDs = append(allCandidateIDs, card.IotCardID)
|
||||
}
|
||||
|
||||
// 检测已被其他企业授权的卡,阻止重复授权
|
||||
conflictAuths, err := s.enterpriseCardAuthStore.GetConflictingAuthsByCardIDs(ctx, enterpriseID, allCandidateIDs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询冲突授权失败")
|
||||
}
|
||||
|
||||
cardIDsToAllocate := make([]uint, 0, len(allCandidateIDs))
|
||||
for _, cardID := range allCandidateIDs {
|
||||
if _, conflict := conflictAuths[cardID]; conflict {
|
||||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||||
ICCID: cardIDToICCID[cardID],
|
||||
Reason: "卡已授权给其他企业,请先收回",
|
||||
})
|
||||
resp.FailCount++
|
||||
continue
|
||||
}
|
||||
cardIDsToAllocate = append(cardIDsToAllocate, cardID)
|
||||
}
|
||||
|
||||
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDsToAllocate)
|
||||
@@ -181,7 +294,12 @@ func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.R
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
iotCardPtrs, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
|
||||
iccids, err := s.resolveICCIDsForRecall(ctx, enterpriseID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iotCardPtrs, err := s.iotCardStore.GetByICCIDs(ctx, iccids)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||||
}
|
||||
@@ -206,7 +324,7 @@ func (s *Service) RecallCards(ctx context.Context, enterpriseID uint, req *dto.R
|
||||
}
|
||||
|
||||
cardIDsToRecall := make([]uint, 0)
|
||||
for _, iccid := range req.ICCIDs {
|
||||
for _, iccid := range iccids {
|
||||
card, exists := cardMap[iccid]
|
||||
if !exists {
|
||||
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
|
||||
|
||||
@@ -46,7 +46,7 @@ func New(
|
||||
}
|
||||
}
|
||||
|
||||
// AllocateDevices 授权设备给企业
|
||||
// AllocateDevices 授权设备给企业,支持 list/filter 两种模式
|
||||
func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *dto.AllocateDevicesReq) (*dto.AllocateDevicesResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
@@ -59,9 +59,15 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
// 根据选取模式解析候选设备号列表
|
||||
deviceNos, err := s.resolveDeviceNosForAllocate(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询所有设备
|
||||
var devices []model.Device
|
||||
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", req.DeviceNos).Find(&devices).Error; err != nil {
|
||||
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", deviceNos).Find(&devices).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败")
|
||||
}
|
||||
|
||||
@@ -88,8 +94,8 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d
|
||||
}
|
||||
|
||||
devicesToAllocate := make([]*model.Device, 0)
|
||||
seenDeviceNos := make(map[string]struct{}, len(req.DeviceNos))
|
||||
for _, deviceNo := range req.DeviceNos {
|
||||
seenDeviceNos := make(map[string]struct{}, len(deviceNos))
|
||||
for _, deviceNo := range deviceNos {
|
||||
if _, exists := seenDeviceNos[deviceNo]; exists {
|
||||
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
|
||||
VirtualNo: deviceNo,
|
||||
@@ -267,7 +273,62 @@ func isUniqueConstraintViolation(err error, constraintName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// RecallDevices 撤销设备授权
|
||||
// resolveDeviceNosForAllocate 根据选取模式解析授权候选设备号列表
|
||||
// list 模式:直接使用请求中的 device_nos
|
||||
// filter 模式:按过滤条件从 store 查询,代理用户的店铺限制由 ApplyShopFilter 自动应用
|
||||
func (s *Service) resolveDeviceNosForAllocate(ctx context.Context, req *dto.AllocateDevicesReq) ([]string, error) {
|
||||
if req.SelectionType == "list" {
|
||||
return req.DeviceNos, nil
|
||||
}
|
||||
filters := make(map[string]any)
|
||||
if req.VirtualNo != "" {
|
||||
filters["virtual_no"] = req.VirtualNo
|
||||
}
|
||||
if req.BatchNo != "" {
|
||||
filters["batch_no"] = req.BatchNo
|
||||
}
|
||||
if req.ShopID != nil {
|
||||
filters["shop_id"] = req.ShopID
|
||||
}
|
||||
devices, err := s.deviceStore.GetByFilters(ctx, filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "按条件查询设备失败")
|
||||
}
|
||||
nos := make([]string, 0, len(devices))
|
||||
for _, d := range devices {
|
||||
nos = append(nos, d.VirtualNo)
|
||||
}
|
||||
return nos, nil
|
||||
}
|
||||
|
||||
// resolveDeviceNosForRecall 根据选取模式解析收回候选设备号列表
|
||||
// filter 模式仅返回当前企业有效授权的设备(通过子查询限定)
|
||||
func (s *Service) resolveDeviceNosForRecall(ctx context.Context, enterpriseID uint, req *dto.RecallDevicesReq) ([]string, error) {
|
||||
if req.SelectionType == "list" {
|
||||
return req.DeviceNos, nil
|
||||
}
|
||||
// filter 模式:只查询已授权给该企业的设备
|
||||
filters := make(map[string]any)
|
||||
if req.VirtualNo != "" {
|
||||
filters["virtual_no"] = req.VirtualNo
|
||||
}
|
||||
if req.BatchNo != "" {
|
||||
filters["batch_no"] = req.BatchNo
|
||||
}
|
||||
// 仅返回有效授权给该企业的设备
|
||||
filters["authorized_enterprise_id"] = enterpriseID
|
||||
devices, err := s.deviceStore.GetByFilters(ctx, filters)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "按条件查询设备失败")
|
||||
}
|
||||
nos := make([]string, 0, len(devices))
|
||||
for _, d := range devices {
|
||||
nos = append(nos, d.VirtualNo)
|
||||
}
|
||||
return nos, nil
|
||||
}
|
||||
|
||||
// RecallDevices 撤销设备授权,支持 list/filter 两种模式
|
||||
func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto.RecallDevicesReq) (*dto.RecallDevicesResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
@@ -280,9 +341,15 @@ func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
// 根据选取模式解析候选设备号列表
|
||||
deviceNos, err := s.resolveDeviceNosForRecall(ctx, enterpriseID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询设备
|
||||
var devices []model.Device
|
||||
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", req.DeviceNos).Find(&devices).Error; err != nil {
|
||||
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", deviceNos).Find(&devices).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败")
|
||||
}
|
||||
|
||||
@@ -304,7 +371,7 @@ func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto
|
||||
}
|
||||
|
||||
deviceAuthsToRevoke := make([]uint, 0)
|
||||
for _, deviceNo := range req.DeviceNos {
|
||||
for _, deviceNo := range deviceNos {
|
||||
device, exists := deviceMap[deviceNo]
|
||||
if !exists {
|
||||
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
|
||||
|
||||
@@ -69,6 +69,8 @@ type Service struct {
|
||||
redis *redis.Client
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
assetAuditService AssetAuditService
|
||||
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -107,6 +109,16 @@ func (s *Service) SetAssetIdentifierStore(store *postgres.AssetIdentifierStore)
|
||||
s.assetIdentifierStore = store
|
||||
}
|
||||
|
||||
// SetEnterpriseCardAuthStore 注入企业卡授权 store(用于列表响应回填企业信息)
|
||||
func (s *Service) SetEnterpriseCardAuthStore(store *postgres.EnterpriseCardAuthorizationStore) {
|
||||
s.enterpriseCardAuthStore = store
|
||||
}
|
||||
|
||||
// SetEnterpriseStore 注入企业 store(用于批量加载企业名称)
|
||||
func (s *Service) SetEnterpriseStore(store *postgres.EnterpriseStore) {
|
||||
s.enterpriseStore = store
|
||||
}
|
||||
|
||||
// SetDataDeductor 设置流量扣减回调
|
||||
// 在应用启动时由 bootstrap 调用,注入套餐扣减服务
|
||||
func (s *Service) SetDataDeductor(deductor DataDeductor) {
|
||||
@@ -229,6 +241,15 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot
|
||||
if req.Keyword != "" {
|
||||
filters["keyword"] = req.Keyword
|
||||
}
|
||||
if req.NetworkStatus != nil {
|
||||
filters["network_status"] = *req.NetworkStatus
|
||||
}
|
||||
if req.AuthorizedEnterpriseID != nil {
|
||||
filters["authorized_enterprise_id"] = *req.AuthorizedEnterpriseID
|
||||
}
|
||||
if req.IsAuthorizedToEnterprise != nil {
|
||||
filters["is_authorized_to_enterprise"] = *req.IsAuthorizedToEnterprise
|
||||
}
|
||||
|
||||
// 代理用户注入 subordinate_shop_ids,让 Store 层走并行查询路径
|
||||
// 避免 PG 对 shop_id IN (...) + ORDER BY 选择全表扫描
|
||||
@@ -260,9 +281,45 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot
|
||||
//TODO 这里不对,现在已经快照了,这里如果还这样处理明显是浪费的
|
||||
seriesMap := s.loadSeriesNames(ctx, cards)
|
||||
|
||||
// 批量加载企业授权信息
|
||||
cardAuthMap := make(map[uint]uint)
|
||||
enterpriseNameMap := make(map[uint]string)
|
||||
if s.enterpriseCardAuthStore != nil && len(cards) > 0 {
|
||||
cardIDs := make([]uint, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
cardIDs = append(cardIDs, card.ID)
|
||||
}
|
||||
authMap, err := s.enterpriseCardAuthStore.GetActiveAuthEnterpriseByCardIDs(ctx, cardIDs)
|
||||
if err != nil {
|
||||
s.logger.Error("批量加载卡企业授权失败", zap.Error(err))
|
||||
} else {
|
||||
cardAuthMap = authMap
|
||||
}
|
||||
if s.enterpriseStore != nil && len(cardAuthMap) > 0 {
|
||||
eIDs := make([]uint, 0, len(cardAuthMap))
|
||||
seen := make(map[uint]struct{})
|
||||
for _, eid := range cardAuthMap {
|
||||
if _, ok := seen[eid]; !ok {
|
||||
seen[eid] = struct{}{}
|
||||
eIDs = append(eIDs, eid)
|
||||
}
|
||||
}
|
||||
nameMap, err := s.enterpriseStore.GetNameMapByIDs(ctx, eIDs)
|
||||
if err != nil {
|
||||
s.logger.Error("批量加载企业名称失败", zap.Error(err))
|
||||
} else {
|
||||
enterpriseNameMap = nameMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]*dto.StandaloneIotCardResponse, 0, len(cards))
|
||||
for _, card := range cards {
|
||||
item := s.toStandaloneResponse(card, shopMap, seriesMap)
|
||||
if eid, ok := cardAuthMap[card.ID]; ok {
|
||||
item.AuthorizedEnterpriseID = &eid
|
||||
item.AuthorizedEnterpriseName = enterpriseNameMap[eid]
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
@@ -411,6 +468,9 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
|
||||
EnablePolling: card.EnablePolling,
|
||||
SeriesID: card.SeriesID,
|
||||
DeviceVirtualNo: card.DeviceVirtualNo,
|
||||
AssetStatus: card.AssetStatus,
|
||||
AssetStatusName: constants.GetAssetStatusName(card.AssetStatus),
|
||||
Generation: card.Generation,
|
||||
CreatedAt: card.CreatedAt,
|
||||
UpdatedAt: card.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -160,9 +160,10 @@ func (s *Service) List(ctx context.Context, req *dto.RefundListRequest) (*dto.Re
|
||||
}
|
||||
|
||||
filters := &postgres.RefundListFilters{
|
||||
Status: req.Status,
|
||||
OrderID: req.OrderID,
|
||||
ShopID: req.ShopID,
|
||||
Status: req.Status,
|
||||
OrderID: req.OrderID,
|
||||
ShopID: req.ShopID,
|
||||
AssetIdentifier: req.AssetIdentifier,
|
||||
}
|
||||
|
||||
requests, total, err := s.refundStore.List(ctx, opts, filters)
|
||||
|
||||
@@ -709,6 +709,7 @@ func (s *Service) ListMainWalletTransactions(ctx context.Context, shopID uint, r
|
||||
TransactionType: req.TransactionType,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
AssetIdentifier: req.AssetIdentifier,
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
@@ -30,6 +30,8 @@ type AgentWalletTransactionListFilters struct {
|
||||
StartDate string
|
||||
// EndDate 结束日期,格式 YYYY-MM-DD
|
||||
EndDate string
|
||||
// AssetIdentifier 资产标识精确匹配(ICCID 或设备虚拟号),空字符串表示不过滤
|
||||
AssetIdentifier string
|
||||
}
|
||||
|
||||
// CreateWithTx 创建代理钱包交易记录(带事务)
|
||||
@@ -95,5 +97,8 @@ func applyWalletTransactionFilters(query *gorm.DB, filters *AgentWalletTransacti
|
||||
if filters.EndDate != "" {
|
||||
query = query.Where("created_at <= ?", filters.EndDate+" 23:59:59")
|
||||
}
|
||||
if filters.AssetIdentifier != "" {
|
||||
query = query.Where("asset_identifier = ?", filters.AssetIdentifier)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filter
|
||||
query := s.db.WithContext(ctx).Model(&model.Device{})
|
||||
// 应用数据权限过滤(NULL shop_id 对代理用户不可见)
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
query = s.applyDeviceFilters(query, filters)
|
||||
query = s.applyDeviceFilters(ctx, query, filters)
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
@@ -127,7 +127,7 @@ func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filter
|
||||
return devices, total, nil
|
||||
}
|
||||
|
||||
func (s *DeviceStore) applyDeviceFilters(query *gorm.DB, filters map[string]any) *gorm.DB {
|
||||
func (s *DeviceStore) applyDeviceFilters(ctx context.Context, query *gorm.DB, filters map[string]any) *gorm.DB {
|
||||
if virtualNo, ok := filters["virtual_no"].(string); ok && virtualNo != "" {
|
||||
query = query.Where("virtual_no LIKE ?", "%"+virtualNo+"%")
|
||||
}
|
||||
@@ -186,6 +186,23 @@ func (s *DeviceStore) applyDeviceFilters(query *gorm.DB, filters map[string]any)
|
||||
if hasActive, ok := filters["has_active_package"].(bool); ok {
|
||||
query = s.applyHasActivePackageFilter(query, hasActive)
|
||||
}
|
||||
if enterpriseID, ok := filters["authorized_enterprise_id"].(uint); ok && enterpriseID > 0 {
|
||||
// 只返回当前有效授权给该企业的设备
|
||||
query = query.Where("id IN (?)",
|
||||
s.db.WithContext(ctx).Table("tb_enterprise_device_authorization").
|
||||
Select("device_id").
|
||||
Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID))
|
||||
}
|
||||
if isAuthorized, ok := filters["is_authorized_to_enterprise"].(bool); ok {
|
||||
subQ := s.db.WithContext(ctx).Table("tb_enterprise_device_authorization").
|
||||
Select("device_id").
|
||||
Where("revoked_at IS NULL AND deleted_at IS NULL")
|
||||
if isAuthorized {
|
||||
query = query.Where("id IN (?)", subQ)
|
||||
} else {
|
||||
query = query.Where("id NOT IN (?)", subQ)
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
@@ -241,7 +258,7 @@ func (s *DeviceStore) GetByFilters(ctx context.Context, filters map[string]any)
|
||||
var devices []*model.Device
|
||||
query := s.db.WithContext(ctx).Model(&model.Device{})
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
query = s.applyDeviceFilters(query, filters)
|
||||
query = s.applyDeviceFilters(ctx, query, filters)
|
||||
if err := query.Find(&devices).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -180,6 +180,53 @@ func (s *EnterpriseCardAuthorizationStore) GetActiveAuthsByCardIDs(ctx context.C
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetConflictingAuthsByCardIDs 查询与指定企业冲突的有效授权(已被其他企业授权的卡)
|
||||
// 不受数据权限过滤,专用于授权前的冲突检测
|
||||
func (s *EnterpriseCardAuthorizationStore) GetConflictingAuthsByCardIDs(ctx context.Context, enterpriseID uint, cardIDs []uint) (map[uint]uint, error) {
|
||||
if len(cardIDs) == 0 {
|
||||
return make(map[uint]uint), nil
|
||||
}
|
||||
var auths []struct {
|
||||
CardID uint
|
||||
EnterpriseID uint
|
||||
}
|
||||
err := s.db.WithContext(ctx).Model(&model.EnterpriseCardAuthorization{}).
|
||||
Select("card_id, enterprise_id").
|
||||
Where("card_id IN ? AND enterprise_id != ? AND revoked_at IS NULL AND deleted_at IS NULL", cardIDs, enterpriseID).
|
||||
Scan(&auths).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[uint]uint, len(auths))
|
||||
for _, a := range auths {
|
||||
result[a.CardID] = a.EnterpriseID
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetActiveAuthEnterpriseByCardIDs 获取卡ID列表的有效授权企业映射(cardID -> enterpriseID),不受数据权限过滤
|
||||
func (s *EnterpriseCardAuthorizationStore) GetActiveAuthEnterpriseByCardIDs(ctx context.Context, cardIDs []uint) (map[uint]uint, error) {
|
||||
if len(cardIDs) == 0 {
|
||||
return make(map[uint]uint), nil
|
||||
}
|
||||
var auths []struct {
|
||||
CardID uint
|
||||
EnterpriseID uint
|
||||
}
|
||||
err := s.db.WithContext(ctx).Model(&model.EnterpriseCardAuthorization{}).
|
||||
Select("card_id, enterprise_id").
|
||||
Where("card_id IN ? AND revoked_at IS NULL AND deleted_at IS NULL", cardIDs).
|
||||
Scan(&auths).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[uint]uint, len(auths))
|
||||
for _, a := range auths {
|
||||
result[a.CardID] = a.EnterpriseID
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BatchUpdateStatus 批量更新授权状态(回收授权:设置 revoked_at)
|
||||
func (s *EnterpriseCardAuthorizationStore) BatchUpdateStatus(ctx context.Context, enterpriseID uint, cardIDs []uint, status int) error {
|
||||
if len(cardIDs) == 0 {
|
||||
|
||||
@@ -155,3 +155,27 @@ func (s *EnterpriseStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.En
|
||||
}
|
||||
return enterprises, nil
|
||||
}
|
||||
|
||||
// GetNameMapByIDs 批量查询企业名称映射(不受数据权限过滤)
|
||||
// 专用于列表响应中回填企业名称,企业 ID 来自已过滤的授权记录,无需二次权限限制
|
||||
func (s *EnterpriseStore) GetNameMapByIDs(ctx context.Context, ids []uint) (map[uint]string, error) {
|
||||
if len(ids) == 0 {
|
||||
return make(map[uint]string), nil
|
||||
}
|
||||
var rows []struct {
|
||||
ID uint
|
||||
EnterpriseName string
|
||||
}
|
||||
err := s.db.WithContext(ctx).Model(&model.Enterprise{}).
|
||||
Select("id, enterprise_name").
|
||||
Where("id IN ? AND deleted_at IS NULL", ids).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[uint]string, len(rows))
|
||||
for _, r := range rows {
|
||||
result[r.ID] = r.EnterpriseName
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -899,6 +899,26 @@ func (s *IotCardStore) applyStandaloneFilters(ctx context.Context, query *gorm.D
|
||||
if carrierName, ok := filters["carrier_name"].(string); ok && carrierName != "" {
|
||||
query = query.Where("carrier_name LIKE ?", "%"+carrierName+"%")
|
||||
}
|
||||
if networkStatus, ok := filters["network_status"].(int); ok {
|
||||
query = query.Where("network_status = ?", networkStatus)
|
||||
}
|
||||
if enterpriseID, ok := filters["authorized_enterprise_id"].(uint); ok && enterpriseID > 0 {
|
||||
// 只返回有效授权给该企业的卡
|
||||
query = query.Where("id IN (?)",
|
||||
s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
|
||||
Select("card_id").
|
||||
Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID))
|
||||
}
|
||||
if isAuthorized, ok := filters["is_authorized_to_enterprise"].(bool); ok {
|
||||
subQ := s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
|
||||
Select("card_id").
|
||||
Where("revoked_at IS NULL AND deleted_at IS NULL")
|
||||
if isAuthorized {
|
||||
query = query.Where("id IN (?)", subQ)
|
||||
} else {
|
||||
query = query.Where("id NOT IN (?)", subQ)
|
||||
}
|
||||
}
|
||||
// has_active_package 过滤:是否存在生效中的主套餐记录
|
||||
if hasActivePackage, ok := filters["has_active_package"].(bool); ok {
|
||||
subQuery := s.db.WithContext(ctx).Table("tb_package_usage pu").Select("1").
|
||||
@@ -1064,6 +1084,86 @@ func (s *IotCardStore) GetBySeriesBindingFilters(ctx context.Context, filters ma
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
// GetStandaloneByICCIDRangeForAuth 按号段范围查询单卡(企业授权用),应用代理数据权限过滤但不限制店铺
|
||||
func (s *IotCardStore) GetStandaloneByICCIDRangeForAuth(ctx context.Context, iccidStart, iccidEnd string) ([]*model.IotCard, error) {
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("is_standalone = true AND iccid >= ? AND iccid <= ?", iccidStart, iccidEnd)
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
var cards []*model.IotCard
|
||||
if err := query.Find(&cards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
// GetStandaloneByAuthFilters 按条件查询单卡(企业授权用),应用代理数据权限过滤
|
||||
func (s *IotCardStore) GetStandaloneByAuthFilters(ctx context.Context, filters map[string]any) ([]*model.IotCard, error) {
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("is_standalone = true")
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
|
||||
if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
|
||||
query = query.Where("iccid LIKE ?", "%"+iccid+"%")
|
||||
}
|
||||
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
|
||||
query = query.Where("batch_no = ?", batchNo)
|
||||
}
|
||||
if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
|
||||
query = query.Where("carrier_id = ?", carrierID)
|
||||
}
|
||||
if shopID, ok := filters["shop_id"].(uint); ok && shopID > 0 {
|
||||
query = query.Where("shop_id = ?", shopID)
|
||||
}
|
||||
if shopIDs, ok := filters["shop_ids"].([]uint); ok && len(shopIDs) > 0 {
|
||||
query = query.Where("shop_id IN ?", shopIDs)
|
||||
}
|
||||
|
||||
var cards []*model.IotCard
|
||||
if err := query.Find(&cards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
// GetAuthorizedStandaloneByICCIDRange 按号段范围查询已授权给指定企业的单卡(企业回收用)
|
||||
func (s *IotCardStore) GetAuthorizedStandaloneByICCIDRange(ctx context.Context, enterpriseID uint, iccidStart, iccidEnd string) ([]*model.IotCard, error) {
|
||||
subQ := s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
|
||||
Select("card_id").
|
||||
Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID)
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("is_standalone = true AND iccid >= ? AND iccid <= ? AND id IN (?)", iccidStart, iccidEnd, subQ)
|
||||
var cards []*model.IotCard
|
||||
if err := query.Find(&cards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
// GetAuthorizedStandaloneByFilters 按条件查询已授权给指定企业的单卡(企业回收用)
|
||||
func (s *IotCardStore) GetAuthorizedStandaloneByFilters(ctx context.Context, enterpriseID uint, filters map[string]any) ([]*model.IotCard, error) {
|
||||
subQ := s.db.WithContext(ctx).Table("tb_enterprise_card_authorization").
|
||||
Select("card_id").
|
||||
Where("enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", enterpriseID)
|
||||
query := s.db.WithContext(ctx).Model(&model.IotCard{}).
|
||||
Where("is_standalone = true AND id IN (?)", subQ)
|
||||
|
||||
if iccid, ok := filters["iccid"].(string); ok && iccid != "" {
|
||||
query = query.Where("iccid LIKE ?", "%"+iccid+"%")
|
||||
}
|
||||
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
|
||||
query = query.Where("batch_no = ?", batchNo)
|
||||
}
|
||||
if carrierID, ok := filters["carrier_id"].(uint); ok && carrierID > 0 {
|
||||
query = query.Where("carrier_id = ?", carrierID)
|
||||
}
|
||||
|
||||
var cards []*model.IotCard
|
||||
if err := query.Find(&cards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) BatchUpdateShopIDAndStatus(ctx context.Context, cardIDs []uint, shopID *uint, status int) error {
|
||||
if len(cardIDs) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -12,9 +12,10 @@ import (
|
||||
|
||||
// RefundListFilters 退款列表筛选条件
|
||||
type RefundListFilters struct {
|
||||
Status *int `json:"status"`
|
||||
OrderID *uint `json:"order_id"`
|
||||
ShopID *uint `json:"shop_id"`
|
||||
Status *int `json:"status"`
|
||||
OrderID *uint `json:"order_id"`
|
||||
ShopID *uint `json:"shop_id"`
|
||||
AssetIdentifier string `json:"asset_identifier"`
|
||||
}
|
||||
|
||||
// RefundStore 退款申请数据访问层
|
||||
@@ -81,6 +82,9 @@ func (s *RefundStore) List(ctx context.Context, opts *store.QueryOptions, filter
|
||||
if filters.ShopID != nil {
|
||||
query = query.Where("tb_refund_request.shop_id = ?", *filters.ShopID)
|
||||
}
|
||||
if filters.AssetIdentifier != "" {
|
||||
query = query.Where("tb_refund_request.asset_identifier = ?", filters.AssetIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user