Files
junhong_cmp_fiber/internal/service/enterprise_device/service.go
break 3b7b856e48
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m9s
企业授权增强与资产列表扩展
- 企业卡授权唯一约束:新增 DB 迁移(000154),卡级部分唯一索引防止同一张卡被多个企业同时持有,Service 层新增跨企业冲突检测
- 单卡列表新增 network_status 过滤参数
- 单卡/设备列表新增 asset_status、asset_status_name、generation 响应字段
- 单卡/设备列表新增企业维度过滤(authorized_enterprise_id、is_authorized_to_enterprise)及响应中企业授权信息(批量加载,无 N+1)
- 主钱包流水/退款列表新增 asset_identifier 精确过滤参数
- 企业卡授权/收回接口升级为三模式(list/range/filter),企业设备授权/收回升级为双模式(list/filter)
- 升级 sonic v1.14.2 → v1.15.2 以兼容 Go 1.26

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 16:23:03 +09:00

511 lines
16 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 授权设备给企业,支持 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, "未授权访问")
}
// 验证企业存在
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
if err != nil {
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
// 根据选取模式解析候选设备号列表
deviceNos, err := s.resolveDeviceNosForAllocate(ctx, req)
if err != nil {
return nil, err
}
// 查询所有设备
var devices []model.Device
if err := s.db.WithContext(ctx).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 := make([]*model.Device, 0)
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 {
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
}
// 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 {
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 {
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, "未授权访问")
}
// 验证企业存在
_, err := s.enterpriseStore.GetByID(ctx, enterpriseID)
if err != nil {
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
// 根据选取模式解析候选设备号列表
deviceNos, err := s.resolveDeviceNosForRecall(ctx, enterpriseID, req)
if err != nil {
return nil, err
}
// 查询设备
var devices []model.Device
if err := s.db.WithContext(ctx).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),
}
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
})
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
}