Files
junhong_cmp_fiber/internal/service/enterprise_device/service.go
break 88cc5e96ec
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m32s
暂存
2026-08-06 09:35:00 +08:00

921 lines
34 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 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
}