diff --git a/internal/service/enterprise_device/service.go b/internal/service/enterprise_device/service.go index 7a7cd2a..60bf62d 100644 --- a/internal/service/enterprise_device/service.go +++ b/internal/service/enterprise_device/service.go @@ -2,6 +2,7 @@ package enterprise_device import ( "context" + stderrors "errors" "time" "github.com/break/junhong_cmp_fiber/internal/model" @@ -10,6 +11,7 @@ import ( "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" ) @@ -74,8 +76,8 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d currentShopID := middleware.GetShopIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) - // 检查已授权的设备 - existingAuths, err := s.enterpriseDeviceAuthStore.GetActiveAuthsByDeviceIDs(ctx, enterpriseID, deviceIDs) + // 检查设备是否已存在有效授权(不区分企业) + activeAuthEnterpriseMap, err := s.enterpriseDeviceAuthStore.GetActiveAuthEnterpriseByDeviceIDs(ctx, deviceIDs) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询已有授权失败") } @@ -86,7 +88,17 @@ 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 { + 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{ @@ -116,11 +128,15 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d } } - // 检查是否已授权 - if existingAuths[device.ID] { + // 检查是否已授权(同企业 / 其他企业) + if authEnterpriseID, exists := activeAuthEnterpriseMap[device.ID]; exists { + reason := "设备已授权给其他企业" + if authEnterpriseID == enterpriseID { + reason = "设备已授权给此企业" + } resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{ VirtualNo: deviceNo, - Reason: "设备已授权给此企业", + Reason: reason, }) continue } @@ -134,38 +150,58 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d now := time.Now() authorizerType := userType - // 1. 创建设备授权记录 - deviceAuths := make([]*model.EnterpriseDeviceAuthorization, 0, len(devicesToAllocate)) + // 1. 创建设备授权记录(逐条处理,避免并发冲突导致整批失败) + deviceAuthIDMap := make(map[uint]uint, len(devicesToAllocate)) + successDevices := make([]*model.Device, 0, len(devicesToAllocate)) for _, device := range devicesToAllocate { - deviceAuths = append(deviceAuths, &model.EnterpriseDeviceAuthorization{ + deviceAuth := &model.EnterpriseDeviceAuthorization{ EnterpriseID: enterpriseID, DeviceID: device.ID, AuthorizedBy: currentUserID, AuthorizedAt: now, AuthorizerType: authorizerType, Remark: req.Remark, - }) - } + } - if err := tx.Create(deviceAuths).Error; err != nil { - return errors.Wrap(errors.CodeInternalError, err, "创建设备授权记录失败") - } + if err := tx.Create(deviceAuth).Error; err != nil { + if isUniqueConstraintViolation(err, "uq_active_device_auth") { + reason := "设备已授权给其他企业" - // 构建设备ID到授权ID的映射 - deviceAuthIDMap := make(map[uint]uint) - for _, auth := range deviceAuths { - deviceAuthIDMap[auth.DeviceID] = auth.ID + 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(devicesToAllocate)) - for _, device := range devicesToAllocate { + deviceIDsToQuery := make([]uint, 0, len(successDevices)) + for _, device := range successDevices { deviceIDsToQuery = append(deviceIDsToQuery, device.ID) } var bindings []model.DeviceSimBinding - if err := tx.Where("device_id IN ? AND bind_status = 1", deviceIDsToQuery).Find(&bindings).Error; err != nil { - return errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败") + 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. 为每张绑定的卡创建授权记录 @@ -196,7 +232,7 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d } // 5. 构建响应 - for _, device := range devicesToAllocate { + for _, device := range successDevices { resp.AuthorizedDevices = append(resp.AuthorizedDevices, dto.AuthorizedDeviceItem{ DeviceID: device.ID, VirtualNo: device.VirtualNo, @@ -212,11 +248,25 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d } } - resp.SuccessCount = len(devicesToAllocate) + 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 +} + // RecallDevices 撤销设备授权 func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto.RecallDevicesReq) (*dto.RecallDevicesResp, error) { currentUserID := middleware.GetUserIDFromContext(ctx) diff --git a/internal/store/postgres/enterprise_device_authorization_store.go b/internal/store/postgres/enterprise_device_authorization_store.go index 4445598..fcf16a3 100644 --- a/internal/store/postgres/enterprise_device_authorization_store.go +++ b/internal/store/postgres/enterprise_device_authorization_store.go @@ -162,6 +162,29 @@ func (s *EnterpriseDeviceAuthorizationStore) GetActiveAuthsByDeviceIDs(ctx conte return result, nil } +// GetActiveAuthEnterpriseByDeviceIDs 获取设备ID列表的有效授权企业映射(deviceID -> enterpriseID) +func (s *EnterpriseDeviceAuthorizationStore) GetActiveAuthEnterpriseByDeviceIDs(ctx context.Context, deviceIDs []uint) (map[uint]uint, error) { + if len(deviceIDs) == 0 { + return make(map[uint]uint), nil + } + + var auths []model.EnterpriseDeviceAuthorization + query := s.db.WithContext(ctx). + Select("device_id, enterprise_id"). + Where("device_id IN ? AND revoked_at IS NULL", deviceIDs) + // 应用企业数据权限过滤 + query = middleware.ApplyEnterpriseFilter(ctx, query) + if err := query.Find(&auths).Error; err != nil { + return nil, err + } + + result := make(map[uint]uint, len(auths)) + for _, auth := range auths { + result[auth.DeviceID] = auth.EnterpriseID + } + return result, nil +} + func (s *EnterpriseDeviceAuthorizationStore) ListDeviceIDsByEnterprise(ctx context.Context, enterpriseID uint) ([]uint, error) { var deviceIDs []uint query := s.db.WithContext(ctx).