package enterprise_device import ( "context" stderrors "errors" "time" "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/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/jackc/pgx/v5/pgconn" "go.uber.org/zap" "gorm.io/gorm" ) type Service struct { db *gorm.DB enterpriseStore *postgres.EnterpriseStore deviceStore *postgres.DeviceStore deviceSimBindingStore *postgres.DeviceSimBindingStore enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore logger *zap.Logger } func New( db *gorm.DB, enterpriseStore *postgres.EnterpriseStore, deviceStore *postgres.DeviceStore, deviceSimBindingStore *postgres.DeviceSimBindingStore, enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore, enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore, logger *zap.Logger, ) *Service { return &Service{ db: db, enterpriseStore: enterpriseStore, deviceStore: deviceStore, deviceSimBindingStore: deviceSimBindingStore, enterpriseDeviceAuthStore: enterpriseDeviceAuthStore, enterpriseCardAuthStore: enterpriseCardAuthStore, logger: logger, } } // 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 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } // 验证企业存在 _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { 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 ?", deviceNos).Find(&devices).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败") } deviceMap := make(map[string]*model.Device) deviceIDs := make([]uint, 0, len(devices)) for i := range devices { deviceMap[devices[i].VirtualNo] = &devices[i] deviceIDs = append(deviceIDs, devices[i].ID) } // 获取当前用户的店铺ID(用于验证设备所有权) currentShopID := middleware.GetShopIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) // 检查设备是否已存在有效授权(不区分企业) activeAuthEnterpriseMap, err := s.enterpriseDeviceAuthStore.GetActiveAuthEnterpriseByDeviceIDs(ctx, deviceIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询已有授权失败") } resp := &dto.AllocateDevicesResp{ FailedItems: make([]dto.FailedDeviceItem, 0), AuthorizedDevices: make([]dto.AuthorizedDeviceItem, 0), } devicesToAllocate := make([]*model.Device, 0) 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, Reason: "请求中设备号重复", }) continue } seenDeviceNos[deviceNo] = struct{}{} device, exists := deviceMap[deviceNo] if !exists { resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: "设备不存在", }) continue } // 验证设备状态(必须是"已分销"状态) if device.Status != 2 { resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: "设备状态不正确,必须是已分销状态", }) continue } // 验证设备所有权(除非是超级管理员或平台用户) if userType == constants.UserTypeAgent { if device.ShopID == nil || *device.ShopID != currentShopID { resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: "无权操作此设备", }) continue } } // 检查是否已授权(同企业 / 其他企业) if authEnterpriseID, exists := activeAuthEnterpriseMap[device.ID]; exists { reason := "设备已授权给其他企业" if authEnterpriseID == enterpriseID { reason = "设备已授权给此企业" } resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: reason, }) continue } devicesToAllocate = append(devicesToAllocate, device) } // 在事务中处理授权 if len(devicesToAllocate) > 0 { err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now() authorizerType := userType // 1. 创建设备授权记录(逐条处理,避免并发冲突导致整批失败) deviceAuthIDMap := make(map[uint]uint, len(devicesToAllocate)) successDevices := make([]*model.Device, 0, len(devicesToAllocate)) for _, device := range devicesToAllocate { deviceAuth := &model.EnterpriseDeviceAuthorization{ EnterpriseID: enterpriseID, DeviceID: device.ID, AuthorizedBy: currentUserID, AuthorizedAt: now, AuthorizerType: authorizerType, Remark: req.Remark, } if err := tx.Create(deviceAuth).Error; err != nil { if isUniqueConstraintViolation(err, "uq_active_device_auth") { reason := "设备已授权给其他企业" var existingAuth model.EnterpriseDeviceAuthorization queryErr := tx.Select("enterprise_id"). Where("device_id = ? AND revoked_at IS NULL", device.ID). First(&existingAuth).Error if queryErr == nil && existingAuth.EnterpriseID == enterpriseID { reason = "设备已授权给此企业" } if queryErr != nil && !stderrors.Is(queryErr, gorm.ErrRecordNotFound) { return errors.Wrap(errors.CodeInternalError, queryErr, "查询冲突授权记录失败") } resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: device.VirtualNo, Reason: reason, }) continue } return errors.Wrap(errors.CodeInternalError, err, "创建设备授权记录失败") } deviceAuthIDMap[device.ID] = deviceAuth.ID successDevices = append(successDevices, device) } // 2. 查询所有设备绑定的卡 deviceIDsToQuery := make([]uint, 0, len(successDevices)) for _, device := range successDevices { deviceIDsToQuery = append(deviceIDsToQuery, device.ID) } var bindings []model.DeviceSimBinding if len(deviceIDsToQuery) > 0 { if err := tx.Where("device_id IN ? AND bind_status = 1", deviceIDsToQuery).Find(&bindings).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败") } } // 3. 为每张绑定的卡创建授权记录 if len(bindings) > 0 { cardAuths := make([]*model.EnterpriseCardAuthorization, 0, len(bindings)) for _, binding := range bindings { deviceAuthID := deviceAuthIDMap[binding.DeviceID] cardAuths = append(cardAuths, &model.EnterpriseCardAuthorization{ EnterpriseID: enterpriseID, CardID: binding.IotCardID, DeviceAuthID: &deviceAuthID, AuthorizedBy: currentUserID, AuthorizedAt: now, AuthorizerType: authorizerType, Remark: req.Remark, }) } if err := tx.Create(cardAuths).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建卡授权记录失败") } } // 4. 统计每个设备的绑定卡数量 deviceCardCount := make(map[uint]int) for _, binding := range bindings { deviceCardCount[binding.DeviceID]++ } // 5. 构建响应 for _, device := range successDevices { resp.AuthorizedDevices = append(resp.AuthorizedDevices, dto.AuthorizedDeviceItem{ DeviceID: device.ID, VirtualNo: device.VirtualNo, CardCount: deviceCardCount[device.ID], }) } return nil }) if err != nil { return nil, err } } resp.SuccessCount = len(resp.AuthorizedDevices) resp.FailCount = len(resp.FailedItems) return resp, nil } func isUniqueConstraintViolation(err error, constraintName string) bool { var pgErr *pgconn.PgError if stderrors.As(err, &pgErr) { if pgErr.Code != "23505" { return false } if constraintName == "" { return true } return pgErr.ConstraintName == constraintName } return false } // 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 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } // 验证企业存在 _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { 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 ?", deviceNos).Find(&devices).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败") } deviceMap := make(map[string]*model.Device) deviceIDs := make([]uint, 0, len(devices)) for i := range devices { deviceMap[devices[i].VirtualNo] = &devices[i] deviceIDs = append(deviceIDs, devices[i].ID) } // 检查授权状态 existingAuths, err := s.enterpriseDeviceAuthStore.GetActiveAuthsByDeviceIDs(ctx, enterpriseID, deviceIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询授权状态失败") } resp := &dto.RecallDevicesResp{ FailedItems: make([]dto.FailedDeviceItem, 0), } deviceAuthsToRevoke := make([]uint, 0) for _, deviceNo := range deviceNos { device, exists := deviceMap[deviceNo] if !exists { resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: "设备不存在", }) continue } if !existingAuths[device.ID] { resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: "设备未授权给此企业", }) continue } // 获取授权记录ID auth, err := s.enterpriseDeviceAuthStore.GetByDeviceID(ctx, device.ID) if err != nil || auth.EnterpriseID != enterpriseID { resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, Reason: "授权记录不存在", }) continue } deviceAuthsToRevoke = append(deviceAuthsToRevoke, auth.ID) } // 在事务中处理撤销 if len(deviceAuthsToRevoke) > 0 { err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. 撤销设备授权 if err := s.enterpriseDeviceAuthStore.RevokeByIDs(ctx, deviceAuthsToRevoke, currentUserID); err != nil { return errors.Wrap(errors.CodeInternalError, err, "撤销设备授权失败") } // 2. 级联撤销卡授权 for _, authID := range deviceAuthsToRevoke { if err := s.enterpriseCardAuthStore.RevokeByDeviceAuthID(ctx, authID, currentUserID); err != nil { return errors.Wrap(errors.CodeInternalError, err, "撤销卡授权失败") } } return nil }) if err != nil { return nil, err } } resp.SuccessCount = len(deviceAuthsToRevoke) resp.FailCount = len(resp.FailedItems) return resp, nil } // ListDevices 查询企业授权设备列表(后台管理) func (s *Service) ListDevices(ctx context.Context, enterpriseID uint, req *dto.EnterpriseDeviceListReq) (*dto.EnterpriseDeviceListResp, error) { // 验证企业存在 _, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } // 查询授权记录 opts := postgres.DeviceAuthListOptions{ EnterpriseID: &enterpriseID, IncludeRevoked: false, Page: req.Page, PageSize: req.PageSize, } auths, total, err := s.enterpriseDeviceAuthStore.ListByEnterprise(ctx, opts) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询授权记录失败") } if len(auths) == 0 { return &dto.EnterpriseDeviceListResp{ List: make([]dto.EnterpriseDeviceItem, 0), Total: 0, }, nil } // 收集设备ID deviceIDs := make([]uint, 0, len(auths)) authMap := make(map[uint]*model.EnterpriseDeviceAuthorization) for _, auth := range auths { deviceIDs = append(deviceIDs, auth.DeviceID) authMap[auth.DeviceID] = auth } // 查询设备信息 var devices []model.Device query := s.db.WithContext(ctx).Where("id IN ?", deviceIDs) if req.VirtualNo != "" { query = query.Where("virtual_no LIKE ?", "%"+req.VirtualNo+"%") } if err := query.Find(&devices).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败") } // 统计每个设备的绑定卡数量 var bindings []model.DeviceSimBinding if err := s.db.WithContext(ctx). Where("device_id IN ? AND bind_status = 1", deviceIDs). Find(&bindings).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败") } cardCountMap := make(map[uint]int) for _, binding := range bindings { cardCountMap[binding.DeviceID]++ } // 构建响应 items := make([]dto.EnterpriseDeviceItem, 0, len(devices)) for _, device := range devices { auth := authMap[device.ID] items = append(items, dto.EnterpriseDeviceItem{ DeviceID: device.ID, VirtualNo: device.VirtualNo, DeviceName: device.DeviceName, DeviceModel: device.DeviceModel, CardCount: cardCountMap[device.ID], AuthorizedAt: auth.AuthorizedAt, }) } return &dto.EnterpriseDeviceListResp{ List: items, Total: total, }, nil }