Files
junhong_cmp_fiber/internal/service/enterprise_device/service.go
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

621 lines
19 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"
"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"
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"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("device_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].DeviceNo] = &devices[i]
deviceIDs = append(deviceIDs, devices[i].ID)
}
// 获取当前用户的店铺ID用于验证设备所有权
currentShopID := middleware.GetShopIDFromContext(ctx)
userType := middleware.GetUserTypeFromContext(ctx)
// 检查已授权的设备
existingAuths, err := s.enterpriseDeviceAuthStore.GetActiveAuthsByDeviceIDs(ctx, enterpriseID, 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)
for _, deviceNo := range req.DeviceNos {
device, exists := deviceMap[deviceNo]
if !exists {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
DeviceNo: deviceNo,
Reason: "设备不存在",
})
continue
}
// 验证设备状态(必须是"已分销"状态)
if device.Status != 2 {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
DeviceNo: deviceNo,
Reason: "设备状态不正确,必须是已分销状态",
})
continue
}
// 验证设备所有权(除非是超级管理员或平台用户)
if userType == constants.UserTypeAgent {
if device.ShopID == nil || *device.ShopID != currentShopID {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
DeviceNo: deviceNo,
Reason: "无权操作此设备",
})
continue
}
}
// 检查是否已授权
if existingAuths[device.ID] {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
DeviceNo: deviceNo,
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. 创建设备授权记录
deviceAuths := make([]*model.EnterpriseDeviceAuthorization, 0, len(devicesToAllocate))
for _, device := range devicesToAllocate {
deviceAuths = append(deviceAuths, &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterpriseID,
DeviceID: device.ID,
AuthorizedBy: currentUserID,
AuthorizedAt: now,
AuthorizerType: authorizerType,
Remark: req.Remark,
})
}
if err := tx.Create(deviceAuths).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建设备授权记录失败")
}
// 构建设备ID到授权ID的映射
deviceAuthIDMap := make(map[uint]uint)
for _, auth := range deviceAuths {
deviceAuthIDMap[auth.DeviceID] = auth.ID
}
// 2. 查询所有设备绑定的卡
deviceIDsToQuery := make([]uint, 0, len(devicesToAllocate))
for _, device := range devicesToAllocate {
deviceIDsToQuery = append(deviceIDsToQuery, device.ID)
}
var bindings []model.DeviceSimBinding
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 devicesToAllocate {
resp.AuthorizedDevices = append(resp.AuthorizedDevices, dto.AuthorizedDeviceItem{
DeviceID: device.ID,
DeviceNo: device.DeviceNo,
CardCount: deviceCardCount[device.ID],
})
}
return nil
})
if err != nil {
return nil, err
}
}
resp.SuccessCount = len(devicesToAllocate)
resp.FailCount = len(resp.FailedItems)
return resp, nil
}
// 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("device_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].DeviceNo] = &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{
DeviceNo: deviceNo,
Reason: "设备不存在",
})
continue
}
if !existingAuths[device.ID] {
resp.FailedItems = append(resp.FailedItems, dto.FailedDeviceItem{
DeviceNo: 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{
DeviceNo: 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.DeviceNo != "" {
query = query.Where("device_no LIKE ?", "%"+req.DeviceNo+"%")
}
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,
DeviceNo: device.DeviceNo,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
CardCount: cardCountMap[device.ID],
AuthorizedAt: auth.AuthorizedAt,
})
}
return &dto.EnterpriseDeviceListResp{
List: items,
Total: total,
}, nil
}
// ListDevicesForEnterprise 查询企业授权设备列表H5企业用户
func (s *Service) ListDevicesForEnterprise(ctx context.Context, req *dto.EnterpriseDeviceListReq) (*dto.EnterpriseDeviceListResp, error) {
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
if enterpriseID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
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
}
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
}
skipCtx := pkggorm.SkipDataPermission(ctx)
var devices []model.Device
query := s.db.WithContext(skipCtx).Where("id IN ?", deviceIDs)
if req.DeviceNo != "" {
query = query.Where("device_no LIKE ?", "%"+req.DeviceNo+"%")
}
if err := query.Find(&devices).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败")
}
var bindings []model.DeviceSimBinding
if err := s.db.WithContext(skipCtx).
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,
DeviceNo: device.DeviceNo,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
CardCount: cardCountMap[device.ID],
AuthorizedAt: auth.AuthorizedAt,
})
}
return &dto.EnterpriseDeviceListResp{
List: items,
Total: total,
}, nil
}
// GetDeviceDetail 获取设备详情H5企业用户
func (s *Service) GetDeviceDetail(ctx context.Context, deviceID uint) (*dto.EnterpriseDeviceDetailResp, error) {
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
if enterpriseID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
auth, err := s.enterpriseDeviceAuthStore.GetByDeviceID(ctx, deviceID)
if err != nil || auth.EnterpriseID != enterpriseID || auth.RevokedAt != nil {
return nil, errors.New(errors.CodeDeviceNotAuthorized, "设备未授权给此企业")
}
skipCtx := pkggorm.SkipDataPermission(ctx)
var device model.Device
if err := s.db.WithContext(skipCtx).Where("id = ?", deviceID).First(&device).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备信息失败")
}
var bindings []model.DeviceSimBinding
if err := s.db.WithContext(skipCtx).
Where("device_id = ? AND bind_status = 1", deviceID).
Find(&bindings).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询设备绑定卡失败")
}
cardIDs := make([]uint, 0, len(bindings))
for _, binding := range bindings {
cardIDs = append(cardIDs, binding.IotCardID)
}
var cards []model.IotCard
cardInfos := make([]dto.DeviceCardInfo, 0)
if len(cardIDs) > 0 {
if err := s.db.WithContext(skipCtx).Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
}
carrierIDs := make([]uint, 0, len(cards))
for _, card := range cards {
carrierIDs = append(carrierIDs, card.CarrierID)
}
var carriers []model.Carrier
carrierMap := make(map[uint]string)
if len(carrierIDs) > 0 {
if err := s.db.WithContext(skipCtx).Where("id IN ?", carrierIDs).Find(&carriers).Error; err == nil {
for _, carrier := range carriers {
carrierMap[carrier.ID] = carrier.CarrierName
}
}
}
for _, card := range cards {
cardInfos = append(cardInfos, dto.DeviceCardInfo{
CardID: card.ID,
ICCID: card.ICCID,
MSISDN: card.MSISDN,
CarrierName: carrierMap[card.CarrierID],
NetworkStatus: card.NetworkStatus,
NetworkStatusName: getNetworkStatusName(card.NetworkStatus),
})
}
}
return &dto.EnterpriseDeviceDetailResp{
Device: dto.EnterpriseDeviceInfo{
DeviceID: device.ID,
DeviceNo: device.DeviceNo,
DeviceName: device.DeviceName,
DeviceModel: device.DeviceModel,
DeviceType: device.DeviceType,
AuthorizedAt: auth.AuthorizedAt,
},
Cards: cardInfos,
}, nil
}
func (s *Service) SuspendCard(ctx context.Context, deviceID, cardID uint, req *dto.DeviceCardOperationReq) (*dto.DeviceCardOperationResp, error) {
if err := s.validateCardOperation(ctx, deviceID, cardID); err != nil {
return nil, err
}
skipCtx := pkggorm.SkipDataPermission(ctx)
if err := s.db.WithContext(skipCtx).Model(&model.IotCard{}).
Where("id = ?", cardID).
Update("network_status", 0).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "停机操作失败")
}
return &dto.DeviceCardOperationResp{
Success: true,
Message: "停机成功",
}, nil
}
func (s *Service) ResumeCard(ctx context.Context, deviceID, cardID uint, req *dto.DeviceCardOperationReq) (*dto.DeviceCardOperationResp, error) {
if err := s.validateCardOperation(ctx, deviceID, cardID); err != nil {
return nil, err
}
skipCtx := pkggorm.SkipDataPermission(ctx)
if err := s.db.WithContext(skipCtx).Model(&model.IotCard{}).
Where("id = ?", cardID).
Update("network_status", 1).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "复机操作失败")
}
return &dto.DeviceCardOperationResp{
Success: true,
Message: "复机成功",
}, nil
}
func (s *Service) validateCardOperation(ctx context.Context, deviceID, cardID uint) error {
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
if enterpriseID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
auth, err := s.enterpriseDeviceAuthStore.GetByDeviceID(ctx, deviceID)
if err != nil || auth.EnterpriseID != enterpriseID || auth.RevokedAt != nil {
return errors.New(errors.CodeDeviceNotAuthorized, "设备未授权给此企业")
}
skipCtx := pkggorm.SkipDataPermission(ctx)
var binding model.DeviceSimBinding
if err := s.db.WithContext(skipCtx).
Where("device_id = ? AND iot_card_id = ? AND bind_status = 1", deviceID, cardID).
First(&binding).Error; err != nil {
return errors.New(errors.CodeForbidden, "卡不属于该设备")
}
var cardAuth model.EnterpriseCardAuthorization
if err := s.db.WithContext(skipCtx).
Where("enterprise_id = ? AND card_id = ? AND device_auth_id IS NOT NULL AND revoked_at IS NULL", enterpriseID, cardID).
First(&cardAuth).Error; err != nil {
return errors.New(errors.CodeForbidden, "无权操作此卡")
}
return nil
}
func getNetworkStatusName(status int) string {
if status == 1 {
return "开机"
}
return "停机"
}