固化七月迭代审计治理进展以隔离线上热修

Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。

Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。

Confidence: medium

Scope-risk: broad

Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。

Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。

Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
This commit is contained in:
2026-08-03 09:47:22 +08:00
parent cf2ff0ac1c
commit b3499adfca
114 changed files with 16961 additions and 2782 deletions

View File

@@ -4,7 +4,9 @@ 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"
@@ -14,6 +16,7 @@ import (
"github.com/jackc/pgx/v5/pgconn"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Service struct {
@@ -24,6 +27,7 @@ type Service struct {
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
logger *zap.Logger
accessAudit accessauditapp.Writer
}
func New(
@@ -34,6 +38,7 @@ func New(
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore,
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore,
logger *zap.Logger,
accessAudit accessauditapp.Writer,
) *Service {
return &Service{
db: db,
@@ -43,6 +48,7 @@ func New(
enterpriseDeviceAuthStore: enterpriseDeviceAuthStore,
enterpriseCardAuthStore: enterpriseCardAuthStore,
logger: logger,
accessAudit: accessAudit,
}
}
@@ -52,12 +58,26 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 验证企业存在
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
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{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{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)
@@ -67,7 +87,7 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d
// 查询所有设备
var devices []model.Device
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", deviceNos).Find(&devices).Error; err != nil {
if err := enterpriseDeviceQuery(ctx, s.db).Where("virtual_no IN ?", deviceNos).Find(&devices).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败")
}
@@ -93,172 +113,221 @@ func (s *Service) AllocateDevices(ctx context.Context, enterpriseID uint, req *d
AuthorizedDevices: make([]dto.AuthorizedDeviceItem, 0),
}
devicesToAllocate := make([]*model.Device, 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: "请求中设备号重复",
})
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: "设备不存在",
})
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: "设备状态不正确,必须是已分销状态",
})
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 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,
})
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{VirtualNo: deviceNo, Reason: reason})
continue
}
devicesToAllocate = append(devicesToAllocate, device)
devices = append(devices, 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
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) {
@@ -296,6 +365,9 @@ func (s *Service) resolveDeviceNosForAllocate(ctx context.Context, req *dto.Allo
}
nos := make([]string, 0, len(devices))
for _, d := range devices {
if !canManageEnterpriseDevice(ctx, d) {
continue
}
nos = append(nos, d.VirtualNo)
}
return nos, nil
@@ -323,6 +395,9 @@ func (s *Service) resolveDeviceNosForRecall(ctx context.Context, enterpriseID ui
}
nos := make([]string, 0, len(devices))
for _, d := range devices {
if !canManageEnterpriseDevice(ctx, d) {
continue
}
nos = append(nos, d.VirtualNo)
}
return nos, nil
@@ -334,12 +409,27 @@ func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
if err := validateRecallDevicesRequest(req); err != nil {
return nil, err
}
// 验证企业存在
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
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{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{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)
@@ -349,7 +439,7 @@ func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto
// 查询设备
var devices []model.Device
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", deviceNos).Find(&devices).Error; err != nil {
if err := enterpriseDeviceQuery(ctx, s.db).Where("virtual_no IN ?", deviceNos).Find(&devices).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败")
}
@@ -370,66 +460,386 @@ func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto
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
})
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.SuccessCount = len(deviceAuthsToRevoke)
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) {
// 验证企业存在