All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m32s
921 lines
34 KiB
Go
921 lines
34 KiB
Go
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
|
||
}
|