Files
junhong_cmp_fiber/internal/store/postgres/device_store.go
break 3b7b856e48
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>
2026-06-17 16:23:03 +09:00

396 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package postgres
import (
"context"
"time"
"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/pkg/middleware"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type DeviceStore struct {
db *gorm.DB
redis *redis.Client
}
func NewDeviceStore(db *gorm.DB, redis *redis.Client) *DeviceStore {
return &DeviceStore{
db: db,
redis: redis,
}
}
func (s *DeviceStore) Create(ctx context.Context, device *model.Device) error {
return s.db.WithContext(ctx).Create(device).Error
}
func (s *DeviceStore) CreateBatch(ctx context.Context, devices []*model.Device) error {
if len(devices) == 0 {
return nil
}
return s.db.WithContext(ctx).CreateInBatches(devices, 100).Error
}
func (s *DeviceStore) GetByID(ctx context.Context, id uint) (*model.Device, error) {
var device model.Device
query := s.db.WithContext(ctx).Where("id = ?", id)
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.First(&device).Error; err != nil {
return nil, err
}
return &device, nil
}
func (s *DeviceStore) GetByDeviceNo(ctx context.Context, deviceNo string) (*model.Device, error) {
var device model.Device
query := s.db.WithContext(ctx).Where("virtual_no = ?", deviceNo)
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.First(&device).Error; err != nil {
return nil, err
}
return &device, nil
}
// GetByIdentifier 通过任意标识符查找设备
// 支持 virtual_no虚拟号、imei、sn 三个字段的自动匹配
func (s *DeviceStore) GetByIdentifier(ctx context.Context, identifier string) (*model.Device, error) {
var device model.Device
query := s.db.WithContext(ctx).Where("virtual_no = ? OR imei = ? OR sn = ?", identifier, identifier, identifier)
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.First(&device).Error; err != nil {
return nil, err
}
return &device, nil
}
func (s *DeviceStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.Device, error) {
var devices []*model.Device
if len(ids) == 0 {
return devices, nil
}
query := s.db.WithContext(ctx).Where("id IN ?", ids)
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
func (s *DeviceStore) Update(ctx context.Context, device *model.Device) error {
return s.db.WithContext(ctx).Save(device).Error
}
func (s *DeviceStore) Delete(ctx context.Context, id uint) error {
return s.db.WithContext(ctx).Delete(&model.Device{}, id).Error
}
func (s *DeviceStore) List(ctx context.Context, opts *store.QueryOptions, filters map[string]any) ([]*model.Device, int64, error) {
var devices []*model.Device
var total int64
query := s.db.WithContext(ctx).Model(&model.Device{})
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
query = s.applyDeviceFilters(ctx, query, filters)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if opts == nil {
opts = &store.QueryOptions{
Page: 1,
PageSize: constants.DefaultPageSize,
}
}
offset := (opts.Page - 1) * opts.PageSize
query = query.Offset(offset).Limit(opts.PageSize)
if opts.OrderBy != "" {
query = query.Order(opts.OrderBy)
} else {
query = query.Order("created_at DESC")
}
if err := query.Find(&devices).Error; err != nil {
return nil, 0, err
}
return devices, total, nil
}
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+"%")
}
if imei, ok := filters["imei"].(string); ok && imei != "" {
query = query.Where("imei LIKE ?", "%"+imei+"%")
}
if keyword, ok := filters["keyword"].(string); ok && keyword != "" {
query = query.Where("virtual_no LIKE ? OR imei LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
if virtualNoStart, ok := filters["virtual_no_start"].(string); ok && virtualNoStart != "" {
query = query.Where("virtual_no >= ?", virtualNoStart)
}
if virtualNoEnd, ok := filters["virtual_no_end"].(string); ok && virtualNoEnd != "" {
query = query.Where("virtual_no <= ?", virtualNoEnd)
}
if deviceName, ok := filters["device_name"].(string); ok && deviceName != "" {
query = query.Where("device_name LIKE ?", "%"+deviceName+"%")
}
if status, ok := filters["status"].(int); ok && status > 0 {
query = query.Where("status = ?", status)
}
if activationStatus, ok := filters["activation_status"].(int); ok {
query = s.applyActivationStatusFilter(query, activationStatus)
}
if shopIDs, ok := filters["shop_ids"].([]uint); ok {
if len(shopIDs) == 0 {
query = query.Where("1 = 0")
} else {
query = query.Where("shop_id IN ?", shopIDs)
}
} else if shopID, ok := filters["shop_id"].(*uint); ok {
if shopID == nil {
query = query.Where("shop_id IS NULL")
} else {
query = query.Where("shop_id = ?", *shopID)
}
}
if batchNo, ok := filters["batch_no"].(string); ok && batchNo != "" {
query = query.Where("batch_no = ?", batchNo)
}
if deviceType, ok := filters["device_type"].(string); ok && deviceType != "" {
query = query.Where("device_type = ?", deviceType)
}
if manufacturer, ok := filters["manufacturer"].(string); ok && manufacturer != "" {
query = query.Where("manufacturer LIKE ?", "%"+manufacturer+"%")
}
if createdAtStart, ok := filters["created_at_start"].(time.Time); ok && !createdAtStart.IsZero() {
query = query.Where("created_at >= ?", createdAtStart)
}
if createdAtEnd, ok := filters["created_at_end"].(time.Time); ok && !createdAtEnd.IsZero() {
query = query.Where("created_at <= ?", createdAtEnd)
}
if seriesID, ok := filters["series_id"].(uint); ok && seriesID > 0 {
query = query.Where("series_id = ?", seriesID)
}
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
}
// applyHasActivePackageFilter 按"是否有生效中主套餐"过滤设备。
// true: 设备在 tb_package_usage 中存在生效中(status=PackageUsageStatusActive)的主套餐记录;
// false: 不存在上述记录。
func (s *DeviceStore) applyHasActivePackageFilter(query *gorm.DB, hasActive bool) *gorm.DB {
subQuery := `SELECT 1 FROM tb_package_usage AS pu
WHERE pu.device_id = tb_device.id
AND pu.status = ?
AND pu.master_usage_id IS NULL
AND pu.deleted_at IS NULL`
if hasActive {
return query.Where("EXISTS ("+subQuery+")", constants.PackageUsageStatusActive)
}
return query.Where("NOT EXISTS ("+subQuery+")", constants.PackageUsageStatusActive)
}
func (s *DeviceStore) applyActivationStatusFilter(query *gorm.DB, activationStatus int) *gorm.DB {
activeCondition := `EXISTS (
SELECT 1
FROM tb_package_usage AS pu
JOIN tb_device_sim_binding AS b
ON b.device_id = pu.device_id
AND b.bind_status = ?
AND b.deleted_at IS NULL
JOIN tb_iot_card AS c
ON c.id = b.iot_card_id
AND c.real_name_status = ?
AND c.deleted_at IS NULL
WHERE pu.device_id = tb_device.id
AND pu.status = ?
AND pu.master_usage_id IS NULL
AND pu.deleted_at IS NULL
)`
args := []any{
constants.BindStatusBound,
constants.RealNameStatusVerified,
constants.PackageUsageStatusActive,
}
if activationStatus == constants.ActivationStatusActive {
return query.Where(activeCondition, args...)
}
if activationStatus == constants.ActivationStatusInactive {
return query.Where("NOT "+activeCondition, args...)
}
return query
}
// GetByFilters 根据筛选条件查询设备
// 用于批量设置套餐系列时选择完整筛选结果,不受列表分页限制。
func (s *DeviceStore) GetByFilters(ctx context.Context, filters map[string]any) ([]*model.Device, error) {
var devices []*model.Device
query := s.db.WithContext(ctx).Model(&model.Device{})
query = middleware.ApplyShopFilter(ctx, query)
query = s.applyDeviceFilters(ctx, query, filters)
if err := query.Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
func (s *DeviceStore) UpdateShopID(ctx context.Context, id uint, shopID *uint) error {
return s.db.WithContext(ctx).Model(&model.Device{}).Where("id = ?", id).Update("shop_id", shopID).Error
}
func (s *DeviceStore) BatchUpdateShopIDAndStatus(ctx context.Context, ids []uint, shopID *uint, status int) error {
if len(ids) == 0 {
return nil
}
updates := map[string]any{
"shop_id": shopID,
"status": status,
"updated_at": time.Now(),
}
return s.db.WithContext(ctx).Model(&model.Device{}).Where("id IN ?", ids).Updates(updates).Error
}
// GetActivationStatusMap 批量查询设备激活状态。
// 设备激活规则:存在生效中主套餐,且任意已绑定卡已实名。
func (s *DeviceStore) GetActivationStatusMap(ctx context.Context, ids []uint) (map[uint]int, error) {
result := make(map[uint]int, len(ids))
if len(ids) == 0 {
return result, nil
}
for _, id := range ids {
result[id] = constants.ActivationStatusInactive
}
var activeDeviceIDs []uint
err := s.db.WithContext(ctx).
Table("tb_package_usage AS pu").
Distinct("pu.device_id").
Joins("JOIN tb_device_sim_binding AS b ON b.device_id = pu.device_id AND b.bind_status = ? AND b.deleted_at IS NULL", constants.BindStatusBound).
Joins("JOIN tb_iot_card AS c ON c.id = b.iot_card_id AND c.real_name_status = ? AND c.deleted_at IS NULL", constants.RealNameStatusVerified).
Where("pu.device_id IN ?", ids).
Where("pu.status = ?", constants.PackageUsageStatusActive).
Where("pu.master_usage_id IS NULL").
Where("pu.deleted_at IS NULL").
Pluck("pu.device_id", &activeDeviceIDs).Error
if err != nil {
return nil, err
}
for _, id := range activeDeviceIDs {
result[id] = constants.ActivationStatusActive
}
return result, nil
}
func (s *DeviceStore) ExistsByDeviceNoBatch(ctx context.Context, deviceNos []string) (map[string]bool, error) {
result := make(map[string]bool)
if len(deviceNos) == 0 {
return result, nil
}
var existingDevices []struct {
VirtualNo string
}
if err := s.db.WithContext(ctx).Model(&model.Device{}).
Select("virtual_no").
Where("virtual_no IN ?", deviceNos).
Find(&existingDevices).Error; err != nil {
return nil, err
}
for _, d := range existingDevices {
result[d.VirtualNo] = true
}
return result, nil
}
func (s *DeviceStore) GetByDeviceNos(ctx context.Context, deviceNos []string) ([]*model.Device, error) {
var devices []*model.Device
if len(deviceNos) == 0 {
return devices, nil
}
query := s.db.WithContext(ctx).Where("virtual_no IN ?", deviceNos)
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
// BatchUpdateSeriesID 批量更新设备的套餐系列ID
func (s *DeviceStore) BatchUpdateSeriesID(ctx context.Context, deviceIDs []uint, seriesID *uint) error {
if len(deviceIDs) == 0 {
return nil
}
return s.db.WithContext(ctx).Model(&model.Device{}).
Where("id IN ?", deviceIDs).
Update("series_id", seriesID).Error
}
// ListBySeriesID 根据套餐系列ID查询设备列表
func (s *DeviceStore) ListBySeriesID(ctx context.Context, seriesID uint) ([]*model.Device, error) {
var devices []*model.Device
query := s.db.WithContext(ctx).Where("series_id = ?", seriesID)
// 应用数据权限过滤NULL shop_id 对代理用户不可见)
query = middleware.ApplyShopFilter(ctx, query)
if err := query.Find(&devices).Error; err != nil {
return nil, err
}
return devices, nil
}
func (s *DeviceStore) UpdateRechargeTrackingFields(ctx context.Context, deviceID uint, accumulatedJSON, triggeredJSON string) error {
return s.db.WithContext(ctx).Model(&model.Device{}).
Where("id = ?", deviceID).
Updates(map[string]interface{}{
"accumulated_recharge_by_series": accumulatedJSON,
"first_recharge_triggered_by_series": triggeredJSON,
}).Error
}
// UpdatePollingStatus 更新设备的轮询启用状态
func (s *DeviceStore) UpdatePollingStatus(ctx context.Context, deviceID uint, enablePolling bool) error {
return s.db.WithContext(ctx).Model(&model.Device{}).
Where("id = ?", deviceID).
Update("enable_polling", enablePolling).Error
}
// UpdateRealnamePolicy 更新设备的实名认证策略
func (s *DeviceStore) UpdateRealnamePolicy(ctx context.Context, deviceID uint, realnamePolicy string) error {
return s.db.WithContext(ctx).Model(&model.Device{}).
Where("id = ?", deviceID).
Update("realname_policy", realnamePolicy).Error
}