package enterprise_device import ( "context" stderrors "errors" "time" "unicode/utf8" accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit" "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" "gorm.io/gorm/clause" ) type Service struct { db *gorm.DB enterpriseStore *postgres.EnterpriseStore deviceStore *postgres.DeviceStore deviceSimBindingStore *postgres.DeviceSimBindingStore enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore logger *zap.Logger accessAudit accessauditapp.Writer } func New( db *gorm.DB, enterpriseStore *postgres.EnterpriseStore, deviceStore *postgres.DeviceStore, deviceSimBindingStore *postgres.DeviceSimBindingStore, enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore, enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore, logger *zap.Logger, accessAudit accessauditapp.Writer, ) *Service { return &Service{ db: db, enterpriseStore: enterpriseStore, deviceStore: deviceStore, deviceSimBindingStore: deviceSimBindingStore, enterpriseDeviceAuthStore: enterpriseDeviceAuthStore, enterpriseCardAuthStore: enterpriseCardAuthStore, logger: logger, accessAudit: accessAudit, } } // 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, "未授权访问") } if err := validateAllocateDevicesRequest(req); err != nil { return nil, err } if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "企业设备授权审计接缝未配置") } if err := validateEnterpriseDeviceActor(ctx); err != nil { s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesAllocated, "向企业授权设备被拒绝", &model.Enterprise{Model: gorm.Model{ID: enterpriseID}}, nil, nil, err) return nil, err } if err := middleware.CanManageEnterprise(ctx, enterpriseID, s.enterpriseStore); err != nil { permissionErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesAllocated, "向企业授权设备被拒绝", &model.Enterprise{Model: gorm.Model{ID: enterpriseID}}, nil, nil, permissionErr) return nil, permissionErr } enterprise, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } ownerShop := loadEnterpriseDeviceOwnerShop(ctx, s.db, enterprise.OwnerShopID) // 根据选取模式解析候选设备号列表 deviceNos, err := s.resolveDeviceNosForAllocate(ctx, req) if err != nil { return nil, err } // 查询所有设备 var devices []model.Device if err := enterpriseDeviceQuery(ctx, s.db).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 := selectDevicesForAllocate( deviceNos, deviceMap, activeAuthEnterpriseMap, enterpriseID, userType, currentShopID, resp, ) if len(devicesToAllocate) > 0 { items, err := s.allocateDevices(ctx, enterprise, ownerShop, devicesToAllocate, req, currentUserID, userType, resp) if err != nil { s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesAllocated, "向企业授权设备失败", enterprise, ownerShop, devicesToAllocate, err) return nil, err } resp.AuthorizedDevices = append(resp.AuthorizedDevices, items...) } resp.SuccessCount = len(resp.AuthorizedDevices) resp.FailCount = len(resp.FailedItems) if resp.SuccessCount == 0 { s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesAllocated, "向企业授权设备被拒绝", enterprise, ownerShop, devicePointers(deviceMap, deviceIDs), errors.New(errors.CodeInvalidStatus, "没有设备满足授权条件")) } return resp, nil } // selectDevicesForAllocate 按既有设备状态、归属和有效授权规则筛选可授权设备。 func selectDevicesForAllocate( deviceNos []string, deviceMap map[string]*model.Device, activeAuthEnterpriseMap map[uint]uint, enterpriseID uint, userType int, currentShopID uint, resp *dto.AllocateDevicesResp, ) []*model.Device { devices := make([]*model.Device, 0, len(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, 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 && (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 } devices = append(devices, device) } return devices } // allocateDevices 在同一事务内创建设备、随设备卡授权和统一审计事实。 func (s *Service) allocateDevices( ctx context.Context, enterprise *model.Enterprise, ownerShop *model.Shop, devices []*model.Device, req *dto.AllocateDevicesReq, operatorID uint, userType int, resp *dto.AllocateDevicesResp, ) ([]dto.AuthorizedDeviceItem, error) { items := make([]dto.AuthorizedDeviceItem, 0, len(devices)) err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id IN ?", deviceIDs(devices)).Find(&[]model.Device{}).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "锁定待授权设备失败") } now := time.Now() deviceAuthByDevice := make(map[uint]*model.EnterpriseDeviceAuthorization, len(devices)) successDevices := make([]*model.Device, 0, len(devices)) for _, device := range devices { auth := &model.EnterpriseDeviceAuthorization{ EnterpriseID: enterprise.ID, DeviceID: device.ID, AuthorizedBy: operatorID, AuthorizedAt: now, AuthorizerType: userType, Remark: req.Remark, } if err := tx.Transaction(func(itemTx *gorm.DB) error { return itemTx.Create(auth).Error }); err != nil { if !isUniqueConstraintViolation(err, "uq_active_device_auth") { return errors.Wrap(errors.CodeInternalError, err, "创建设备授权记录失败") } reason, err := deviceAuthorizationConflictReason(tx, device.ID, enterprise.ID) if err != nil { return err } resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{VirtualNo: device.VirtualNo, Reason: reason}) continue } deviceAuthByDevice[device.ID] = auth successDevices = append(successDevices, device) } if len(successDevices) == 0 { return nil } successDeviceIDs := deviceIDs(successDevices) bindings, err := loadDeviceBindings(tx, successDeviceIDs, true) if err != nil { return err } cardAuths := make([]*model.EnterpriseCardAuthorization, 0, len(bindings)) for _, binding := range bindings { deviceAuthID := deviceAuthByDevice[binding.DeviceID].ID cardAuths = append(cardAuths, &model.EnterpriseCardAuthorization{ EnterpriseID: enterprise.ID, CardID: binding.IotCardID, DeviceAuthID: &deviceAuthID, AuthorizedBy: operatorID, AuthorizedAt: now, AuthorizerType: userType, Remark: req.Remark, }) } if len(cardAuths) > 0 { if err := tx.Create(cardAuths).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建卡授权记录失败") } } cards, err := loadAuditCards(tx, cardAuths) if err != nil { return err } result := constants.AuditResultSuccess if len(resp.FailedItems) > 0 { result = constants.AuditResultPartial } if err := s.accessAudit.WriteAccessChange(ctx, tx, enterpriseDeviceAllocateAudit( enterprise, ownerShop, successDevices, deviceAuthByDevice, bindings, cards, cardAuths, operatorID, result, )); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入企业设备授权审计失败") } cardCount := make(map[uint]int, len(successDevices)) for _, binding := range bindings { cardCount[binding.DeviceID]++ } for _, device := range successDevices { items = append(items, dto.AuthorizedDeviceItem{ DeviceID: device.ID, VirtualNo: device.VirtualNo, CardCount: cardCount[device.ID], }) } return nil }) return items, err } func deviceAuthorizationConflictReason(tx *gorm.DB, deviceID, enterpriseID uint) (string, error) { reason := "设备已授权给其他企业" var existing model.EnterpriseDeviceAuthorization err := tx.Select("enterprise_id").Where("device_id = ? AND revoked_at IS NULL", deviceID).First(&existing).Error if err == nil && existing.EnterpriseID == enterpriseID { return "设备已授权给此企业", nil } if err != nil && !stderrors.Is(err, gorm.ErrRecordNotFound) { return "", errors.Wrap(errors.CodeInternalError, err, "查询冲突授权记录失败") } return reason, nil } // enterpriseDeviceAllocateAudit 装配企业、设备、卡槽、卡及授权记录的资源关系。 func enterpriseDeviceAllocateAudit( enterprise *model.Enterprise, ownerShop *model.Shop, devices []*model.Device, deviceAuthByDevice map[uint]*model.EnterpriseDeviceAuthorization, bindings []*model.DeviceSimBinding, cards map[uint]*model.IotCard, cardAuths []*model.EnterpriseCardAuthorization, operatorID uint, result string, ) accessauditapp.ChangeAudit { change := accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseDevicesAllocated, Summary: "向企业授权设备", Result: result, OperatorID: operatorID, Enterprise: enterprise, Shop: ownerShop, BeforeData: map[string]any{"authorized_device_count": 0}, AfterData: map[string]any{"authorized_device_count": len(devices)}, } for _, device := range devices { auth := deviceAuthByDevice[device.ID] change.Devices = append(change.Devices, accessauditapp.DeviceChange{ Device: device, BeforeData: map[string]any{"enterprise_id": nil, "authorized": false}, AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备已授权给企业", }) change.DeviceAuthorizations = append(change.DeviceAuthorizations, accessauditapp.EnterpriseDeviceAuthorizationChange{ Authorization: auth, AfterData: map[string]any{"authorized": true}, }) } for _, binding := range bindings { change.DeviceBindings = append(change.DeviceBindings, accessauditapp.DeviceSimBindingChange{Binding: binding}) } for _, auth := range cardAuths { card := cards[auth.CardID] change.Cards = append(change.Cards, accessauditapp.IotCardChange{ Card: card, BeforeData: map[string]any{"enterprise_id": nil, "authorized": false}, AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "卡已随设备授权给企业", }) change.CardAuthorizations = append(change.CardAuthorizations, accessauditapp.EnterpriseCardAuthorizationChange{ Authorization: auth, AfterData: map[string]any{"authorized": true}, }) } return change } // isUniqueConstraintViolation 判断 PostgreSQL 唯一约束冲突并可限定约束名称。 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 { if !canManageEnterpriseDevice(ctx, d) { continue } 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 { if !canManageEnterpriseDevice(ctx, d) { continue } 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, "未授权访问") } if err := validateRecallDevicesRequest(req); err != nil { return nil, err } if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "企业设备授权审计接缝未配置") } if err := validateEnterpriseDeviceActor(ctx); err != nil { s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesRecalled, "回收企业设备授权被拒绝", &model.Enterprise{Model: gorm.Model{ID: enterpriseID}}, nil, nil, err) return nil, err } if err := middleware.CanManageEnterprise(ctx, enterpriseID, s.enterpriseStore); err != nil { permissionErr := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesRecalled, "回收企业设备授权被拒绝", &model.Enterprise{Model: gorm.Model{ID: enterpriseID}}, nil, nil, permissionErr) return nil, permissionErr } enterprise, err := s.enterpriseStore.GetByID(ctx, enterpriseID) if err != nil { return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } ownerShop := loadEnterpriseDeviceOwnerShop(ctx, s.db, enterprise.OwnerShopID) // 根据选取模式解析候选设备号列表 deviceNos, err := s.resolveDeviceNosForRecall(ctx, enterpriseID, req) if err != nil { return nil, err } // 查询设备 var devices []model.Device if err := enterpriseDeviceQuery(ctx, s.db).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), } deviceIDsToRecall := selectDeviceIDsForRecall(deviceNos, deviceMap, existingAuths, resp) if len(deviceIDsToRecall) > 0 { recalledIDs, err := s.recallDevices(ctx, enterprise, ownerShop, deviceIDsToRecall, deviceMap, currentUserID, len(resp.FailedItems) > 0) if err != nil { s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesRecalled, "回收企业设备授权失败", enterprise, ownerShop, devicePointers(deviceMap, deviceIDsToRecall), err) return nil, err } recalled := make(map[uint]struct{}, len(recalledIDs)) for _, deviceID := range recalledIDs { recalled[deviceID] = struct{}{} } devicesByID := devicesByID(deviceMap) for _, deviceID := range deviceIDsToRecall { if _, ok := recalled[deviceID]; ok { continue } resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: devicesByID[deviceID].VirtualNo, Reason: "设备未授权给此企业", }) } resp.SuccessCount = len(recalledIDs) } resp.FailCount = len(resp.FailedItems) if resp.SuccessCount == 0 { s.recordDeviceFailure(ctx, constants.AuditActionEnterpriseDevicesRecalled, "回收企业设备授权被拒绝", enterprise, ownerShop, devicePointers(deviceMap, deviceIDs), errors.New(errors.CodeInvalidStatus, "没有设备满足回收条件")) } return resp, nil } // selectDeviceIDsForRecall 按当前有效授权筛选回收目标,并保持越权与不存在同错。 func selectDeviceIDsForRecall( deviceNos []string, deviceMap map[string]*model.Device, existingAuths map[uint]bool, resp *dto.RecallDevicesResp, ) []uint { deviceIDs := make([]uint, 0, len(deviceNos)) seenDeviceNos := make(map[string]struct{}, len(deviceNos)) for _, deviceNo := range deviceNos { if _, seen := seenDeviceNos[deviceNo]; seen { continue } seenDeviceNos[deviceNo] = struct{}{} 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 } deviceIDs = append(deviceIDs, device.ID) } return deviceIDs } // recallDevices 在锁定有效授权后撤销实际命中项,并返回真实回收设备 ID。 func (s *Service) recallDevices( ctx context.Context, enterprise *model.Enterprise, ownerShop *model.Shop, requestedDeviceIDs []uint, deviceMap map[string]*model.Device, operatorID uint, partial bool, ) ([]uint, error) { recalledDeviceIDs := make([]uint, 0, len(requestedDeviceIDs)) err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var deviceAuths []*model.EnterpriseDeviceAuthorization if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("enterprise_id = ? AND device_id IN ? AND revoked_at IS NULL", enterprise.ID, requestedDeviceIDs). Find(&deviceAuths).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "查询有效设备授权失败") } if len(deviceAuths) == 0 { return nil } authIDs := make([]uint, 0, len(deviceAuths)) actualDeviceIDs := make([]uint, 0, len(deviceAuths)) for _, auth := range deviceAuths { authIDs = append(authIDs, auth.ID) actualDeviceIDs = append(actualDeviceIDs, auth.DeviceID) } bindings, err := loadDeviceBindings(tx, actualDeviceIDs, true) if err != nil { return err } var cardAuths []*model.EnterpriseCardAuthorization if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("device_auth_id IN ? AND revoked_at IS NULL", authIDs). Find(&cardAuths).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "查询有效卡授权失败") } cards, err := loadAuditCards(tx, cardAuths) if err != nil { return err } now := time.Now() if err := tx.Model(&model.EnterpriseDeviceAuthorization{}). Where("id IN ? AND revoked_at IS NULL", authIDs). Updates(map[string]any{"revoked_by": operatorID, "revoked_at": now}).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "撤销设备授权失败") } if len(cardAuths) > 0 { cardAuthIDs := make([]uint, 0, len(cardAuths)) for _, auth := range cardAuths { cardAuthIDs = append(cardAuthIDs, auth.ID) } if err := tx.Model(&model.EnterpriseCardAuthorization{}). Where("id IN ? AND revoked_at IS NULL", cardAuthIDs). Updates(map[string]any{"revoked_by": operatorID, "revoked_at": now}).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "撤销卡授权失败") } } devicesByID := devicesByID(deviceMap) result := constants.AuditResultSuccess if partial || len(deviceAuths) < len(requestedDeviceIDs) { result = constants.AuditResultPartial } change := enterpriseDeviceRecallAudit( enterprise, ownerShop, devicesByID, deviceAuths, bindings, cards, cardAuths, operatorID, now, result, ) if err := s.accessAudit.WriteAccessChange(ctx, tx, change); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入企业设备回收审计失败") } recalledDeviceIDs = actualDeviceIDs return nil }) return recalledDeviceIDs, err } // enterpriseDeviceRecallAudit 装配回收操作涉及的设备、卡槽、卡和授权记录变化。 func enterpriseDeviceRecallAudit( enterprise *model.Enterprise, ownerShop *model.Shop, devices map[uint]*model.Device, deviceAuths []*model.EnterpriseDeviceAuthorization, bindings []*model.DeviceSimBinding, cards map[uint]*model.IotCard, cardAuths []*model.EnterpriseCardAuthorization, operatorID uint, now time.Time, result string, ) accessauditapp.ChangeAudit { change := accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseDevicesRecalled, Summary: "回收企业设备授权", Result: result, OperatorID: operatorID, Enterprise: enterprise, Shop: ownerShop, BeforeData: map[string]any{"authorized_device_count": len(deviceAuths)}, AfterData: map[string]any{"authorized_device_count": 0}, } for _, auth := range deviceAuths { device := devices[auth.DeviceID] change.Devices = append(change.Devices, accessauditapp.DeviceChange{ Device: device, BeforeData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true}, AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": false}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备授权已回收", }) beforeRevokedBy, beforeRevokedAt := auth.RevokedBy, auth.RevokedAt auth.RevokedBy, auth.RevokedAt = &operatorID, &now change.DeviceAuthorizations = append(change.DeviceAuthorizations, accessauditapp.EnterpriseDeviceAuthorizationChange{ Authorization: auth, BeforeData: map[string]any{"revoked_by": beforeRevokedBy, "revoked_at": beforeRevokedAt}, AfterData: map[string]any{"revoked_by": operatorID, "revoked_at": now}, }) } for _, binding := range bindings { change.DeviceBindings = append(change.DeviceBindings, accessauditapp.DeviceSimBindingChange{Binding: binding}) } for _, auth := range cardAuths { card := cards[auth.CardID] change.Cards = append(change.Cards, accessauditapp.IotCardChange{ Card: card, BeforeData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": true}, AfterData: map[string]any{"enterprise_id": enterprise.ID, "authorization_id": auth.ID, "authorized": false}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "卡授权已随设备回收", }) beforeRevokedBy, beforeRevokedAt := auth.RevokedBy, auth.RevokedAt auth.RevokedBy, auth.RevokedAt = &operatorID, &now change.CardAuthorizations = append(change.CardAuthorizations, accessauditapp.EnterpriseCardAuthorizationChange{ Authorization: auth, BeforeData: map[string]any{"revoked_by": beforeRevokedBy, "revoked_at": beforeRevokedAt}, AfterData: map[string]any{"revoked_by": operatorID, "revoked_at": now}, }) } return change } func validateEnterpriseDeviceActor(ctx context.Context) error { switch middleware.GetUserTypeFromContext(ctx) { case constants.UserTypeSuperAdmin, constants.UserTypePlatform, constants.UserTypeAgent: return nil default: return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } } // validateAllocateDevicesRequest 校验 Service 边界的授权选取条件,防止空筛选扩散为全量操作。 func validateAllocateDevicesRequest(req *dto.AllocateDevicesReq) error { if req == nil || utf8.RuneCountInString(req.VirtualNo) > 100 || utf8.RuneCountInString(req.BatchNo) > 100 || utf8.RuneCountInString(req.Remark) > 500 { return errors.New(errors.CodeInvalidParam) } if req.SelectionType == "filter" { if req.VirtualNo == "" && req.BatchNo == "" && req.ShopID == nil { return errors.New(errors.CodeInvalidParam) } if req.ShopID != nil && *req.ShopID == 0 { return errors.New(errors.CodeInvalidParam) } return nil } if req.SelectionType != "list" || len(req.DeviceNos) == 0 || len(req.DeviceNos) > 100 { return errors.New(errors.CodeInvalidParam) } for _, deviceNo := range req.DeviceNos { if deviceNo == "" { return errors.New(errors.CodeInvalidParam) } } return nil } // validateRecallDevicesRequest 校验 Service 边界的回收选取条件,防止空筛选扩散为全量操作。 func validateRecallDevicesRequest(req *dto.RecallDevicesReq) error { if req == nil || utf8.RuneCountInString(req.VirtualNo) > 100 || utf8.RuneCountInString(req.BatchNo) > 100 { return errors.New(errors.CodeInvalidParam) } if req.SelectionType == "filter" { if req.VirtualNo == "" && req.BatchNo == "" { return errors.New(errors.CodeInvalidParam) } return nil } if req.SelectionType != "list" || len(req.DeviceNos) == 0 || len(req.DeviceNos) > 100 { return errors.New(errors.CodeInvalidParam) } for _, deviceNo := range req.DeviceNos { if deviceNo == "" { return errors.New(errors.CodeInvalidParam) } } return nil } func enterpriseDeviceQuery(ctx context.Context, db *gorm.DB) *gorm.DB { query := db.WithContext(ctx).Model(&model.Device{}) if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeAgent { return query } shopID := middleware.GetShopIDFromContext(ctx) if shopID == 0 { return query.Where("1 = 0") } return query.Where("shop_id = ?", shopID) } func canManageEnterpriseDevice(ctx context.Context, device *model.Device) bool { if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeAgent { return true } shopID := middleware.GetShopIDFromContext(ctx) return shopID > 0 && device.ShopID != nil && *device.ShopID == shopID } func loadEnterpriseDeviceOwnerShop(ctx context.Context, db *gorm.DB, ownerShopID *uint) *model.Shop { if ownerShopID == nil { return nil } var shop model.Shop if err := db.WithContext(ctx).Unscoped().First(&shop, *ownerShopID).Error; err != nil { return nil } return &shop } func loadDeviceBindings(tx *gorm.DB, deviceIDs []uint, lock bool) ([]*model.DeviceSimBinding, error) { bindings := make([]*model.DeviceSimBinding, 0) if len(deviceIDs) == 0 { return bindings, nil } query := tx.Where("device_id IN ? AND bind_status = 1", deviceIDs) if lock { query = query.Clauses(clause.Locking{Strength: "UPDATE"}) } if err := query.Find(&bindings).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败") } return bindings, nil } // loadAuditCards 使用非作用域查询保留软删除卡的稳定审计身份快照。 func loadAuditCards(tx *gorm.DB, auths []*model.EnterpriseCardAuthorization) (map[uint]*model.IotCard, error) { cardIDs := make([]uint, 0, len(auths)) for _, auth := range auths { cardIDs = append(cardIDs, auth.CardID) } cards := make(map[uint]*model.IotCard, len(cardIDs)) if len(cardIDs) == 0 { return cards, nil } var values []*model.IotCard if err := tx.Unscoped().Where("id IN ?", cardIDs).Find(&values).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询绑定卡审计快照失败") } for _, card := range values { cards[card.ID] = card } return cards, nil } func deviceIDs(devices []*model.Device) []uint { ids := make([]uint, 0, len(devices)) for _, device := range devices { ids = append(ids, device.ID) } return ids } func devicesByID(deviceMap map[string]*model.Device) map[uint]*model.Device { result := make(map[uint]*model.Device, len(deviceMap)) for _, device := range deviceMap { result[device.ID] = device } return result } func devicePointers(deviceMap map[string]*model.Device, ids []uint) []*model.Device { wanted := make(map[uint]struct{}, len(ids)) for _, id := range ids { wanted[id] = struct{}{} } devices := make([]*model.Device, 0, len(ids)) for _, device := range deviceMap { if _, ok := wanted[device.ID]; ok { devices = append(devices, device) } } return devices } // recordDeviceFailure 在业务事务结束后使用统一 Writer 记录失败或拒绝事实。 func (s *Service) recordDeviceFailure( ctx context.Context, actionCode, summary string, enterprise *model.Enterprise, ownerShop *model.Shop, devices []*model.Device, originalErr error, ) { changes := make([]accessauditapp.DeviceChange, 0, len(devices)) for _, device := range devices { changes = append(changes, accessauditapp.DeviceChange{Device: device}) } accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{ ActionCode: actionCode, Summary: summary, Result: enterpriseDeviceFailureResult(originalErr), OperatorID: middleware.GetUserIDFromContext(ctx), Enterprise: enterprise, Shop: ownerShop, Devices: changes, SubjectVisibility: constants.AuditSubjectInternalOnly, }, originalErr) } func enterpriseDeviceFailureResult(err error) string { var appErr *errors.AppError if stderrors.As(err, &appErr) { switch appErr.Code { case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeEnterpriseNotFound: return constants.AuditResultDenied case errors.CodeInvalidStatus: return constants.AuditResultDenied } } return constants.AuditResultFailed } // 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 }