企业授权增强与资产列表扩展
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:
2026-06-17 16:23:03 +09:00
parent cf36f1447f
commit 3b7b856e48
37 changed files with 1260 additions and 84 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {