Files
junhong_cmp_fiber/internal/service/enterprise_device/service.go
huang 95104100c9
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
修复企业授权报错的问题
2026-04-27 10:30:10 +08:00

444 lines
14 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"
"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"
)
type Service struct {
db *gorm.DB
enterpriseStore *postgres.EnterpriseStore
deviceStore *postgres.DeviceStore
deviceSimBindingStore *postgres.DeviceSimBindingStore
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore
logger *zap.Logger
}
func New(
db *gorm.DB,
enterpriseStore *postgres.EnterpriseStore,
deviceStore *postgres.DeviceStore,
deviceSimBindingStore *postgres.DeviceSimBindingStore,
enterpriseDeviceAuthStore *postgres.EnterpriseDeviceAuthorizationStore,
enterpriseCardAuthStore *postgres.EnterpriseCardAuthorizationStore,
logger *zap.Logger,
) *Service {
return &Service{
db: db,
enterpriseStore: enterpriseStore,
deviceStore: deviceStore,
deviceSimBindingStore: deviceSimBindingStore,
enterpriseDeviceAuthStore: enterpriseDeviceAuthStore,
enterpriseCardAuthStore: enterpriseCardAuthStore,
logger: logger,
}
}
// AllocateDevices 授权设备给企业
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, "未授权访问")
}
// 验证企业存在
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
if err != nil {
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
// 查询所有设备
var devices []model.Device
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", req.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 := make([]*model.Device, 0)
seenDeviceNos := make(map[string]struct{}, len(req.DeviceNos))
for _, deviceNo := range req.DeviceNos {
if _, exists := seenDeviceNos[deviceNo]; exists {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
VirtualNo: deviceNo,
Reason: "请求中设备号重复",
})
continue
}
seenDeviceNos[deviceNo] = struct{}{}
device, exists := deviceMap[deviceNo]
if !exists {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
VirtualNo: deviceNo,
Reason: "设备不存在",
})
continue
}
// 验证设备状态(必须是"已分销"状态)
if device.Status != 2 {
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 authEnterpriseID, exists := activeAuthEnterpriseMap[device.ID]; exists {
reason := "设备已授权给其他企业"
if authEnterpriseID == enterpriseID {
reason = "设备已授权给此企业"
}
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
VirtualNo: deviceNo,
Reason: reason,
})
continue
}
devicesToAllocate = append(devicesToAllocate, 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
}
func isUniqueConstraintViolation(err error, constraintName string) bool {
var pgErr *pgconn.PgError
if stderrors.As(err, &pgErr) {
if pgErr.Code != "23505" {
return false
}
if constraintName == "" {
return true
}
return pgErr.ConstraintName == constraintName
}
return false
}
// RecallDevices 撤销设备授权
func (s *Service) RecallDevices(ctx context.Context, enterpriseID uint, req *dto.RecallDevicesReq) (*dto.RecallDevicesResp, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 验证企业存在
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
if err != nil {
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
// 查询设备
var devices []model.Device
if err := s.db.WithContext(ctx).Where("virtual_no IN ?", req.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),
}
deviceAuthsToRevoke := make([]uint, 0)
for _, deviceNo := range req.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
})
if err != nil {
return nil, err
}
}
resp.SuccessCount = len(deviceAuthsToRevoke)
resp.FailCount = len(resp.FailedItems)
return resp, nil
}
// 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
}