feat: 实现企业设备授权功能并归档 OpenSpec 变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m39s

- 新增企业设备授权模块(Model、DTO、Service、Handler、Store)
- 实现设备授权的创建、查询、更新、删除等完整业务逻辑
- 添加企业卡授权与设备授权的关联关系
- 新增 2 个数据库迁移脚本
- 同步 OpenSpec delta specs 到 main specs
- 归档 add-enterprise-device-authorization 变更
- 更新 API 文档和路由配置
- 新增完整的集成测试和单元测试覆盖
This commit is contained in:
2026-01-29 13:18:49 +08:00
parent e87513541b
commit b02175271a
118 changed files with 14306 additions and 472 deletions

View File

@@ -13,7 +13,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
return &Handlers{
Account: admin.NewAccountHandler(svc.Account),
Role: admin.NewRoleHandler(svc.Role),
Role: admin.NewRoleHandler(svc.Role, validate),
Permission: admin.NewPermissionHandler(svc.Permission),
PersonalCustomer: app.NewPersonalCustomerHandler(svc.PersonalCustomer, deps.Logger),
Shop: admin.NewShopHandler(svc.Shop),
@@ -25,6 +25,8 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
CommissionWithdrawalSetting: admin.NewCommissionWithdrawalSettingHandler(svc.CommissionWithdrawalSetting),
Enterprise: admin.NewEnterpriseHandler(svc.Enterprise),
EnterpriseCard: admin.NewEnterpriseCardHandler(svc.EnterpriseCard),
EnterpriseDevice: admin.NewEnterpriseDeviceHandler(svc.EnterpriseDevice),
EnterpriseDeviceH5: h5.NewEnterpriseDeviceHandler(svc.EnterpriseDevice),
Authorization: admin.NewAuthorizationHandler(svc.Authorization),
CustomerAccount: admin.NewCustomerAccountHandler(svc.CustomerAccount),
MyCommission: admin.NewMyCommissionHandler(svc.MyCommission),

View File

@@ -14,6 +14,7 @@ import (
deviceImportSvc "github.com/break/junhong_cmp_fiber/internal/service/device_import"
enterpriseSvc "github.com/break/junhong_cmp_fiber/internal/service/enterprise"
enterpriseCardSvc "github.com/break/junhong_cmp_fiber/internal/service/enterprise_card"
enterpriseDeviceSvc "github.com/break/junhong_cmp_fiber/internal/service/enterprise_device"
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
iotCardImportSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card_import"
myCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/my_commission"
@@ -47,6 +48,7 @@ type services struct {
CommissionCalculation *commissionCalculationSvc.Service
Enterprise *enterpriseSvc.Service
EnterpriseCard *enterpriseCardSvc.Service
EnterpriseDevice *enterpriseDeviceSvc.Service
Authorization *enterpriseCardSvc.AuthorizationService
CustomerAccount *customerAccountSvc.Service
MyCommission *myCommissionSvc.Service
@@ -99,6 +101,7 @@ func initServices(s *stores, deps *Dependencies) *services {
),
Enterprise: enterpriseSvc.New(deps.DB, s.Enterprise, s.Shop, s.Account),
EnterpriseCard: enterpriseCardSvc.New(deps.DB, s.Enterprise, s.EnterpriseCardAuthorization),
EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger),
Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger),
CustomerAccount: customerAccountSvc.New(deps.DB, s.Account, s.Shop, s.Enterprise),
MyCommission: myCommissionSvc.New(deps.DB, s.Shop, s.Wallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.WalletTransaction),

View File

@@ -20,6 +20,7 @@ type stores struct {
CommissionWithdrawalSetting *postgres.CommissionWithdrawalSettingStore
Enterprise *postgres.EnterpriseStore
EnterpriseCardAuthorization *postgres.EnterpriseCardAuthorizationStore
EnterpriseDeviceAuthorization *postgres.EnterpriseDeviceAuthorizationStore
IotCard *postgres.IotCardStore
IotCardImportTask *postgres.IotCardImportTaskStore
Device *postgres.DeviceStore
@@ -57,6 +58,7 @@ func initStores(deps *Dependencies) *stores {
CommissionWithdrawalSetting: postgres.NewCommissionWithdrawalSettingStore(deps.DB, deps.Redis),
Enterprise: postgres.NewEnterpriseStore(deps.DB, deps.Redis),
EnterpriseCardAuthorization: postgres.NewEnterpriseCardAuthorizationStore(deps.DB, deps.Redis),
EnterpriseDeviceAuthorization: postgres.NewEnterpriseDeviceAuthorizationStore(deps.DB, deps.Redis),
IotCard: postgres.NewIotCardStore(deps.DB, deps.Redis),
IotCardImportTask: postgres.NewIotCardImportTaskStore(deps.DB, deps.Redis),
Device: postgres.NewDeviceStore(deps.DB, deps.Redis),

View File

@@ -23,6 +23,8 @@ type Handlers struct {
CommissionWithdrawalSetting *admin.CommissionWithdrawalSettingHandler
Enterprise *admin.EnterpriseHandler
EnterpriseCard *admin.EnterpriseCardHandler
EnterpriseDevice *admin.EnterpriseDeviceHandler
EnterpriseDeviceH5 *h5.EnterpriseDeviceHandler
Authorization *admin.AuthorizationHandler
CustomerAccount *admin.CustomerAccountHandler
MyCommission *admin.MyCommissionHandler

View File

@@ -0,0 +1,80 @@
package admin
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
enterpriseDeviceService "github.com/break/junhong_cmp_fiber/internal/service/enterprise_device"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
type EnterpriseDeviceHandler struct {
service *enterpriseDeviceService.Service
}
func NewEnterpriseDeviceHandler(service *enterpriseDeviceService.Service) *EnterpriseDeviceHandler {
return &EnterpriseDeviceHandler{service: service}
}
func (h *EnterpriseDeviceHandler) AllocateDevices(c *fiber.Ctx) error {
enterpriseIDStr := c.Params("id")
enterpriseID, err := strconv.ParseUint(enterpriseIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "企业ID格式错误")
}
var req dto.AllocateDevicesReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.AllocateDevices(c.UserContext(), uint(enterpriseID), &req)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *EnterpriseDeviceHandler) RecallDevices(c *fiber.Ctx) error {
enterpriseIDStr := c.Params("id")
enterpriseID, err := strconv.ParseUint(enterpriseIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "企业ID格式错误")
}
var req dto.RecallDevicesReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.RecallDevices(c.UserContext(), uint(enterpriseID), &req)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *EnterpriseDeviceHandler) ListDevices(c *fiber.Ctx) error {
enterpriseIDStr := c.Params("id")
enterpriseID, err := strconv.ParseUint(enterpriseIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "企业ID格式错误")
}
var req dto.EnterpriseDeviceListReq
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.ListDevices(c.UserContext(), uint(enterpriseID), &req)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.List, result.Total, req.Page, req.PageSize)
}

View File

@@ -3,6 +3,7 @@ package admin
import (
"strconv"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/pkg/errors"
@@ -14,12 +15,16 @@ import (
// RoleHandler 角色 Handler
type RoleHandler struct {
service *roleService.Service
service *roleService.Service
validator *validator.Validate
}
// NewRoleHandler 创建角色 Handler
func NewRoleHandler(service *roleService.Service) *RoleHandler {
return &RoleHandler{service: service}
func NewRoleHandler(service *roleService.Service, validator *validator.Validate) *RoleHandler {
return &RoleHandler{
service: service,
validator: validator,
}
}
// Create 创建角色
@@ -30,6 +35,10 @@ func (h *RoleHandler) Create(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
role, err := h.service.Create(c.UserContext(), &req)
if err != nil {
return err
@@ -67,6 +76,10 @@ func (h *RoleHandler) Update(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
role, err := h.service.Update(c.UserContext(), uint(id), &req)
if err != nil {
return err
@@ -119,6 +132,10 @@ func (h *RoleHandler) AssignPermissions(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
rps, err := h.service.AssignPermissions(c.UserContext(), uint(id), req.PermIDs)
if err != nil {
return err
@@ -176,6 +193,10 @@ func (h *RoleHandler) UpdateStatus(c *fiber.Ctx) error {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
if err := h.validator.Struct(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
if err := h.service.UpdateStatus(c.UserContext(), uint(id), req.Status); err != nil {
return err
}

View File

@@ -0,0 +1,107 @@
package h5
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
enterpriseDeviceService "github.com/break/junhong_cmp_fiber/internal/service/enterprise_device"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/response"
)
type EnterpriseDeviceHandler struct {
service *enterpriseDeviceService.Service
}
func NewEnterpriseDeviceHandler(service *enterpriseDeviceService.Service) *EnterpriseDeviceHandler {
return &EnterpriseDeviceHandler{service: service}
}
func (h *EnterpriseDeviceHandler) ListDevices(c *fiber.Ctx) error {
var req dto.H5EnterpriseDeviceListReq
if err := c.QueryParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
serviceReq := &dto.EnterpriseDeviceListReq{
Page: req.Page,
PageSize: req.PageSize,
DeviceNo: req.DeviceNo,
}
result, err := h.service.ListDevicesForEnterprise(c.UserContext(), serviceReq)
if err != nil {
return err
}
return response.SuccessWithPagination(c, result.List, result.Total, req.Page, req.PageSize)
}
func (h *EnterpriseDeviceHandler) GetDeviceDetail(c *fiber.Ctx) error {
deviceIDStr := c.Params("device_id")
deviceID, err := strconv.ParseUint(deviceIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "设备ID格式错误")
}
result, err := h.service.GetDeviceDetail(c.UserContext(), uint(deviceID))
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *EnterpriseDeviceHandler) SuspendCard(c *fiber.Ctx) error {
deviceIDStr := c.Params("device_id")
deviceID, err := strconv.ParseUint(deviceIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "设备ID格式错误")
}
cardIDStr := c.Params("card_id")
cardID, err := strconv.ParseUint(cardIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "卡ID格式错误")
}
var req dto.DeviceCardOperationReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.SuspendCard(c.UserContext(), uint(deviceID), uint(cardID), &req)
if err != nil {
return err
}
return response.Success(c, result)
}
func (h *EnterpriseDeviceHandler) ResumeCard(c *fiber.Ctx) error {
deviceIDStr := c.Params("device_id")
deviceID, err := strconv.ParseUint(deviceIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "设备ID格式错误")
}
cardIDStr := c.Params("card_id")
cardID, err := strconv.ParseUint(cardIDStr, 10, 64)
if err != nil {
return errors.New(errors.CodeInvalidParam, "卡ID格式错误")
}
var req dto.DeviceCardOperationReq
if err := c.BodyParser(&req); err != nil {
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
}
result, err := h.service.ResumeCard(c.UserContext(), uint(deviceID), uint(cardID), &req)
if err != nil {
return err
}
return response.Success(c, result)
}

View File

@@ -13,6 +13,7 @@ type StandaloneCard struct {
StatusName string `json:"status_name" description:"状态名称"`
}
// Deprecated: 已废弃,不再支持通过单卡授权接口授权设备卡,请使用设备授权接口
type DeviceBundle struct {
DeviceID uint `json:"device_id" description:"设备ID"`
DeviceNo string `json:"device_no" description:"设备号"`
@@ -20,12 +21,21 @@ type DeviceBundle struct {
BundleCards []DeviceBundleCard `json:"bundle_cards" description:"连带卡(同设备的其他卡)"`
}
// Deprecated: 已废弃,不再支持通过单卡授权接口授权设备卡,请使用设备授权接口
type DeviceBundleCard struct {
ICCID string `json:"iccid" description:"ICCID"`
IotCardID uint `json:"iot_card_id" description:"卡ID"`
MSISDN string `json:"msisdn" description:"手机号"`
}
// Deprecated: 已废弃,不再支持通过单卡授权接口授权设备卡,请使用设备授权接口
type AllocatedDevice struct {
DeviceID uint `json:"device_id" description:"设备ID"`
DeviceNo string `json:"device_no" description:"设备号"`
CardCount int `json:"card_count" description:"卡数量"`
ICCIDs []string `json:"iccids" description:"卡ICCID列表"`
}
type FailedItem struct {
ICCID string `json:"iccid" description:"ICCID"`
Reason string `json:"reason" description:"失败原因"`
@@ -41,29 +51,20 @@ type AllocatePreviewSummary struct {
type AllocateCardsPreviewResp struct {
StandaloneCards []StandaloneCard `json:"standalone_cards" description:"可直接授权的卡(未绑定设备)"`
DeviceBundles []DeviceBundle `json:"device_bundles" description:"需要整体授权的设备包"`
FailedItems []FailedItem `json:"failed_items" description:"失败的卡"`
Summary AllocatePreviewSummary `json:"summary" description:"汇总信息"`
}
type AllocateCardsReq struct {
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
ICCIDs []string `json:"iccids" validate:"required,min=1,max=1000,dive,required" required:"true" description:"需要授权的 ICCID 列表"`
ConfirmDeviceBundles bool `json:"confirm_device_bundles" description:"确认整体授权设备下所有卡"`
}
type AllocatedDevice struct {
DeviceID uint `json:"device_id" description:"设备ID"`
DeviceNo string `json:"device_no" description:"设备号"`
CardCount int `json:"card_count" description:"卡数量"`
ICCIDs []string `json:"iccids" description:"卡ICCID列表"`
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
ICCIDs []string `json:"iccids" validate:"required,min=1,max=1000,dive,required" required:"true" description:"需要授权的 ICCID 列表"`
Remark string `json:"remark" validate:"max=500" description:"授权备注"`
}
type AllocateCardsResp struct {
SuccessCount int `json:"success_count" description:"成功数量"`
FailCount int `json:"fail_count" description:"失败数量"`
FailedItems []FailedItem `json:"failed_items" description:"失败详情"`
AllocatedDevices []AllocatedDevice `json:"allocated_devices" description:"连带授权的设备列表"`
SuccessCount int `json:"success_count" description:"成功数量"`
FailCount int `json:"fail_count" description:"失败数量"`
FailedItems []FailedItem `json:"failed_items" description:"失败详情"`
}
type RecallCardsReq struct {

View File

@@ -0,0 +1,103 @@
package dto
import "time"
type AllocateDevicesReq struct {
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
DeviceNos []string `json:"device_nos" validate:"required,min=1,max=100" description:"设备号列表最多100个"`
Remark string `json:"remark" validate:"max=500" description:"授权备注"`
}
type AllocateDevicesResp struct {
SuccessCount int `json:"success_count" description:"成功数量"`
FailCount int `json:"fail_count" description:"失败数量"`
FailedItems []FailedDeviceItem `json:"failed_items" description:"失败项列表"`
AuthorizedDevices []AuthorizedDeviceItem `json:"authorized_devices" description:"已授权设备列表"`
}
type FailedDeviceItem struct {
DeviceNo string `json:"device_no" description:"设备号"`
Reason string `json:"reason" description:"失败原因"`
}
type AuthorizedDeviceItem struct {
DeviceID uint `json:"device_id" description:"设备ID"`
DeviceNo string `json:"device_no" description:"设备号"`
CardCount int `json:"card_count" description:"绑定卡数量"`
}
type RecallDevicesReq struct {
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
DeviceNos []string `json:"device_nos" validate:"required,min=1,max=100" description:"设备号列表最多100个"`
}
type RecallDevicesResp struct {
SuccessCount int `json:"success_count" description:"成功数量"`
FailCount int `json:"fail_count" description:"失败数量"`
FailedItems []FailedDeviceItem `json:"failed_items" description:"失败项列表"`
}
type EnterpriseDeviceListReq struct {
ID uint `json:"-" params:"id" path:"id" validate:"required" required:"true" description:"企业ID"`
Page int `json:"page" query:"page" validate:"required,min=1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"required,min=1,max=100" description:"每页数量"`
DeviceNo string `json:"device_no" query:"device_no" description:"设备号(模糊搜索)"`
}
type H5EnterpriseDeviceListReq struct {
Page int `json:"page" query:"page" validate:"required,min=1" description:"页码"`
PageSize int `json:"page_size" query:"page_size" validate:"required,min=1,max=100" description:"每页数量"`
DeviceNo string `json:"device_no" query:"device_no" description:"设备号(模糊搜索)"`
}
type EnterpriseDeviceListResp struct {
List []EnterpriseDeviceItem `json:"list" description:"设备列表"`
Total int64 `json:"total" description:"总数"`
}
type EnterpriseDeviceItem struct {
DeviceID uint `json:"device_id" description:"设备ID"`
DeviceNo string `json:"device_no" description:"设备号"`
DeviceName string `json:"device_name" description:"设备名称"`
DeviceModel string `json:"device_model" description:"设备型号"`
CardCount int `json:"card_count" description:"绑定卡数量"`
AuthorizedAt time.Time `json:"authorized_at" description:"授权时间"`
}
type EnterpriseDeviceDetailResp struct {
Device EnterpriseDeviceInfo `json:"device" description:"设备信息"`
Cards []DeviceCardInfo `json:"cards" description:"绑定卡列表"`
}
type EnterpriseDeviceInfo struct {
DeviceID uint `json:"device_id" description:"设备ID"`
DeviceNo string `json:"device_no" description:"设备号"`
DeviceName string `json:"device_name" description:"设备名称"`
DeviceModel string `json:"device_model" description:"设备型号"`
DeviceType string `json:"device_type" description:"设备类型"`
AuthorizedAt time.Time `json:"authorized_at" description:"授权时间"`
}
type DeviceCardInfo struct {
CardID uint `json:"card_id" description:"卡ID"`
ICCID string `json:"iccid" description:"ICCID"`
MSISDN string `json:"msisdn" description:"手机号"`
CarrierName string `json:"carrier_name" description:"运营商名称"`
NetworkStatus int `json:"network_status" description:"网络状态0=停机 1=开机"`
NetworkStatusName string `json:"network_status_name" description:"网络状态名称"`
}
type DeviceDetailReq struct {
DeviceID uint `json:"-" params:"device_id" path:"device_id" validate:"required" required:"true" description:"设备ID"`
}
type DeviceCardOperationReq struct {
DeviceID uint `json:"-" params:"device_id" path:"device_id" validate:"required" required:"true" description:"设备ID"`
CardID uint `json:"-" params:"card_id" path:"card_id" validate:"required" required:"true" description:"卡ID"`
Reason string `json:"reason" validate:"max=200" description:"操作原因"`
}
type DeviceCardOperationResp struct {
Success bool `json:"success" description:"操作是否成功"`
Message string `json:"message" description:"操作结果消息"`
}

View File

@@ -21,6 +21,7 @@ type EnterpriseCardAuthorization struct {
RevokedBy *uint `gorm:"column:revoked_by;comment:回收人账号ID" json:"revoked_by"`
RevokedAt *time.Time `gorm:"column:revoked_at;comment:回收时间" json:"revoked_at"`
Remark string `gorm:"column:remark;type:varchar(500);default:'';comment:授权备注" json:"remark"`
DeviceAuthID *uint `gorm:"column:device_auth_id;comment:关联的设备授权ID" json:"device_auth_id"`
}
func (EnterpriseCardAuthorization) TableName() string {

View File

@@ -0,0 +1,26 @@
package model
import (
"time"
"gorm.io/gorm"
)
type EnterpriseDeviceAuthorization struct {
ID uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"deleted_at,omitempty"`
EnterpriseID uint `gorm:"column:enterprise_id;not null;comment:被授权企业ID" json:"enterprise_id"`
DeviceID uint `gorm:"column:device_id;not null;comment:被授权设备ID" json:"device_id"`
AuthorizedBy uint `gorm:"column:authorized_by;not null;comment:授权人账号ID" json:"authorized_by"`
AuthorizedAt time.Time `gorm:"column:authorized_at;not null;default:CURRENT_TIMESTAMP;comment:授权时间" json:"authorized_at"`
AuthorizerType int `gorm:"column:authorizer_type;not null;comment:授权人类型2=平台用户 3=代理账号" json:"authorizer_type"`
RevokedBy *uint `gorm:"column:revoked_by;comment:回收人账号ID" json:"revoked_by"`
RevokedAt *time.Time `gorm:"column:revoked_at;comment:回收时间" json:"revoked_at"`
Remark string `gorm:"column:remark;type:varchar(500);default:'';comment:授权备注" json:"remark"`
}
func (EnterpriseDeviceAuthorization) TableName() string {
return "tb_enterprise_device_authorization"
}

View File

@@ -46,6 +46,9 @@ func RegisterAdminRoutes(router fiber.Router, handlers *bootstrap.Handlers, midd
if handlers.EnterpriseCard != nil {
registerEnterpriseCardRoutes(authGroup, handlers.EnterpriseCard, doc, basePath)
}
if handlers.EnterpriseDevice != nil {
registerEnterpriseDeviceRoutes(authGroup, handlers.EnterpriseDevice, doc, basePath)
}
if handlers.Authorization != nil {
registerAuthorizationRoutes(authGroup, handlers.Authorization, doc, basePath)
}

View File

@@ -0,0 +1,38 @@
package routes
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
func registerEnterpriseDeviceRoutes(router fiber.Router, handler *admin.EnterpriseDeviceHandler, doc *openapi.Generator, basePath string) {
enterprises := router.Group("/enterprises")
groupPath := basePath + "/enterprises"
Register(enterprises, doc, groupPath, "POST", "/:id/allocate-devices", handler.AllocateDevices, RouteSpec{
Summary: "授权设备给企业",
Tags: []string{"企业设备授权"},
Input: new(dto.AllocateDevicesReq),
Output: new(dto.AllocateDevicesResp),
Auth: true,
})
Register(enterprises, doc, groupPath, "POST", "/:id/recall-devices", handler.RecallDevices, RouteSpec{
Summary: "撤销设备授权",
Tags: []string{"企业设备授权"},
Input: new(dto.RecallDevicesReq),
Output: new(dto.RecallDevicesResp),
Auth: true,
})
Register(enterprises, doc, groupPath, "GET", "/:id/devices", handler.ListDevices, RouteSpec{
Summary: "企业设备列表",
Tags: []string{"企业设备授权"},
Input: new(dto.EnterpriseDeviceListReq),
Output: new(dto.EnterpriseDeviceListResp),
Auth: true,
})
}

View File

@@ -20,6 +20,9 @@ func RegisterH5Routes(router fiber.Router, handlers *bootstrap.Handlers, middlew
if handlers.H5Order != nil {
registerH5OrderRoutes(authGroup, handlers.H5Order, doc, basePath)
}
if handlers.EnterpriseDeviceH5 != nil {
registerH5EnterpriseDeviceRoutes(authGroup, handlers.EnterpriseDeviceH5, doc, basePath)
}
}
func registerH5AuthRoutes(router fiber.Router, handler interface{}, authMiddleware fiber.Handler, doc *openapi.Generator, basePath string) {

View File

@@ -0,0 +1,46 @@
package routes
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/handler/h5"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
func registerH5EnterpriseDeviceRoutes(router fiber.Router, handler *h5.EnterpriseDeviceHandler, doc *openapi.Generator, basePath string) {
devices := router.Group("/devices")
groupPath := basePath + "/devices"
Register(devices, doc, groupPath, "GET", "", handler.ListDevices, RouteSpec{
Summary: "企业设备列表H5",
Tags: []string{"H5-企业设备"},
Input: new(dto.H5EnterpriseDeviceListReq),
Output: new(dto.EnterpriseDeviceListResp),
Auth: true,
})
Register(devices, doc, groupPath, "GET", "/:device_id", handler.GetDeviceDetail, RouteSpec{
Summary: "获取设备详情H5",
Tags: []string{"H5-企业设备"},
Input: new(dto.DeviceDetailReq),
Output: new(dto.EnterpriseDeviceDetailResp),
Auth: true,
})
Register(devices, doc, groupPath, "POST", "/:device_id/cards/:card_id/suspend", handler.SuspendCard, RouteSpec{
Summary: "停机卡H5",
Tags: []string{"H5-企业设备"},
Input: new(dto.DeviceCardOperationReq),
Output: new(dto.DeviceCardOperationResp),
Auth: true,
})
Register(devices, doc, groupPath, "POST", "/:device_id/cards/:card_id/resume", handler.ResumeCard, RouteSpec{
Summary: "复机卡H5",
Tags: []string{"H5-企业设备"},
Input: new(dto.DeviceCardOperationReq),
Output: new(dto.DeviceCardOperationResp),
Auth: true,
})
}

View File

@@ -0,0 +1,161 @@
package enterprise_card
import (
"context"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"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/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func TestAuthorizationService_BatchAuthorize_BoundCardRejected(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
logger, _ := zap.NewDevelopment()
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
service := NewAuthorizationService(enterpriseStore, iotCardStore, authStore, logger)
shop := &model.Shop{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
ShopName: "测试店铺",
ShopCode: "TEST_SHOP_001",
Level: 1,
Status: 1,
}
require.NoError(t, tx.Create(shop).Error)
enterprise := &model.Enterprise{
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
EnterpriseName: "测试企业",
EnterpriseCode: "TEST_ENT_001",
OwnerShopID: &shop.ID,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{CarrierName: "测试运营商", CarrierType: "CMCC", Status: 1}
require.NoError(t, tx.Create(carrier).Error)
unboundCard := &model.IotCard{
ICCID: "UNBOUND_CARD_001",
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
ShopID: &shop.ID,
}
require.NoError(t, tx.Create(unboundCard).Error)
boundCard := &model.IotCard{
ICCID: "BOUND_CARD_001",
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
ShopID: &shop.ID,
}
require.NoError(t, tx.Create(boundCard).Error)
device := &model.Device{
DeviceNo: "TEST_DEVICE_001",
DeviceName: "测试设备",
Status: 2,
ShopID: &shop.ID,
}
require.NoError(t, tx.Create(device).Error)
now := time.Now()
binding := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: boundCard.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
require.NoError(t, tx.Create(binding).Error)
ctx := middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypePlatform,
ShopID: shop.ID,
})
t.Run("绑定设备的卡被拒绝授权", func(t *testing.T) {
req := BatchAuthorizeRequest{
EnterpriseID: enterprise.ID,
CardIDs: []uint{boundCard.ID},
AuthorizerID: 1,
AuthorizerType: constants.UserTypePlatform,
Remark: "测试授权",
}
err := service.BatchAuthorize(ctx, req)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok, "应返回 AppError 类型")
assert.Equal(t, errors.CodeCannotAuthorizeBoundCard, appErr.Code)
assert.Contains(t, appErr.Message, "已绑定设备")
})
t.Run("未绑定设备的卡可以授权", func(t *testing.T) {
req := BatchAuthorizeRequest{
EnterpriseID: enterprise.ID,
CardIDs: []uint{unboundCard.ID},
AuthorizerID: 1,
AuthorizerType: constants.UserTypePlatform,
Remark: "测试授权",
}
err := service.BatchAuthorize(ctx, req)
require.NoError(t, err)
auths, err := authStore.ListByCards(ctx, []uint{unboundCard.ID}, false)
require.NoError(t, err)
assert.Len(t, auths, 1)
assert.Equal(t, enterprise.ID, auths[0].EnterpriseID)
})
t.Run("混合卡列表中有绑定卡时整体拒绝", func(t *testing.T) {
unboundCard2 := &model.IotCard{
ICCID: "UNBOUND_CARD_002",
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
ShopID: &shop.ID,
}
require.NoError(t, tx.Create(unboundCard2).Error)
req := BatchAuthorizeRequest{
EnterpriseID: enterprise.ID,
CardIDs: []uint{unboundCard2.ID, boundCard.ID},
AuthorizerID: 1,
AuthorizerType: constants.UserTypePlatform,
Remark: "测试授权",
}
err := service.BatchAuthorize(ctx, req)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok, "应返回 AppError 类型")
assert.Equal(t, errors.CodeCannotAuthorizeBoundCard, appErr.Code)
auths, err := authStore.ListByCards(ctx, []uint{unboundCard2.ID}, false)
require.NoError(t, err)
assert.Len(t, auths, 0, "混合列表中的未绑定卡也不应被授权")
})
}

View File

@@ -65,34 +65,16 @@ func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, r
s.db.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = 1", cardIDs).Find(&bindings)
}
cardToDevice := make(map[uint]uint)
deviceCards := make(map[uint][]uint)
cardToDevice := make(map[uint]bool)
for _, binding := range bindings {
cardToDevice[binding.IotCardID] = binding.DeviceID
deviceCards[binding.DeviceID] = append(deviceCards[binding.DeviceID], binding.IotCardID)
}
deviceIDs := make([]uint, 0, len(deviceCards))
for deviceID := range deviceCards {
deviceIDs = append(deviceIDs, deviceID)
}
var devices []model.Device
deviceMap := make(map[uint]*model.Device)
if len(deviceIDs) > 0 {
s.db.WithContext(ctx).Where("id IN ?", deviceIDs).Find(&devices)
for i := range devices {
deviceMap[devices[i].ID] = &devices[i]
}
cardToDevice[binding.IotCardID] = true
}
resp := &dto.AllocateCardsPreviewResp{
StandaloneCards: make([]dto.StandaloneCard, 0),
DeviceBundles: make([]dto.DeviceBundle, 0),
FailedItems: make([]dto.FailedItem, 0),
}
processedDevices := make(map[uint]bool)
for _, iccid := range req.ICCIDs {
card, exists := cardMap[iccid]
if !exists {
@@ -103,67 +85,28 @@ func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, r
continue
}
deviceID, hasDevice := cardToDevice[card.ID]
if !hasDevice {
resp.StandaloneCards = append(resp.StandaloneCards, dto.StandaloneCard{
ICCID: card.ICCID,
IotCardID: card.ID,
MSISDN: card.MSISDN,
CarrierID: card.CarrierID,
StatusName: getCardStatusName(card.Status),
if cardToDevice[card.ID] {
resp.FailedItems = append(resp.FailedItems, dto.FailedItem{
ICCID: iccid,
Reason: "该卡已绑定设备,请使用设备授权功能",
})
} else {
if processedDevices[deviceID] {
continue
}
processedDevices[deviceID] = true
device := deviceMap[deviceID]
if device == nil {
continue
}
bundleCardIDs := deviceCards[deviceID]
bundle := dto.DeviceBundle{
DeviceID: deviceID,
DeviceNo: device.DeviceNo,
BundleCards: make([]dto.DeviceBundleCard, 0),
}
for _, bundleCardID := range bundleCardIDs {
bundleCard := cardIDMap[bundleCardID]
if bundleCard == nil {
continue
}
if bundleCard.ID == card.ID {
bundle.TriggerCard = dto.DeviceBundleCard{
ICCID: bundleCard.ICCID,
IotCardID: bundleCard.ID,
MSISDN: bundleCard.MSISDN,
}
} else {
bundle.BundleCards = append(bundle.BundleCards, dto.DeviceBundleCard{
ICCID: bundleCard.ICCID,
IotCardID: bundleCard.ID,
MSISDN: bundleCard.MSISDN,
})
}
}
resp.DeviceBundles = append(resp.DeviceBundles, bundle)
continue
}
}
deviceCardCount := 0
for _, bundle := range resp.DeviceBundles {
deviceCardCount += 1 + len(bundle.BundleCards)
resp.StandaloneCards = append(resp.StandaloneCards, dto.StandaloneCard{
ICCID: card.ICCID,
IotCardID: card.ID,
MSISDN: card.MSISDN,
CarrierID: card.CarrierID,
StatusName: getCardStatusName(card.Status),
})
}
resp.Summary = dto.AllocatePreviewSummary{
StandaloneCardCount: len(resp.StandaloneCards),
DeviceCount: len(resp.DeviceBundles),
DeviceCardCount: deviceCardCount,
TotalCardCount: len(resp.StandaloneCards) + deviceCardCount,
DeviceCount: 0,
DeviceCardCount: 0,
TotalCardCount: len(resp.StandaloneCards),
FailedCount: len(resp.FailedItems),
}
@@ -186,36 +129,15 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
return nil, err
}
if len(preview.DeviceBundles) > 0 && !req.ConfirmDeviceBundles {
return nil, errors.New(errors.CodeInvalidParam, "存在设备包,请确认整体授权设备下所有卡")
}
resp := &dto.AllocateCardsResp{
FailedItems: preview.FailedItems,
FailCount: len(preview.FailedItems),
AllocatedDevices: make([]dto.AllocatedDevice, 0),
FailedItems: preview.FailedItems,
FailCount: len(preview.FailedItems),
}
cardIDsToAllocate := make([]uint, 0)
for _, card := range preview.StandaloneCards {
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
}
for _, bundle := range preview.DeviceBundles {
cardIDsToAllocate = append(cardIDsToAllocate, bundle.TriggerCard.IotCardID)
for _, card := range bundle.BundleCards {
cardIDsToAllocate = append(cardIDsToAllocate, card.IotCardID)
}
iccids := []string{bundle.TriggerCard.ICCID}
for _, card := range bundle.BundleCards {
iccids = append(iccids, card.ICCID)
}
resp.AllocatedDevices = append(resp.AllocatedDevices, dto.AllocatedDevice{
DeviceID: bundle.DeviceID,
DeviceNo: bundle.DeviceNo,
CardCount: 1 + len(bundle.BundleCards),
ICCIDs: iccids,
})
}
existingAuths, err := s.enterpriseCardAuthStore.GetActiveAuthsByCardIDs(ctx, enterpriseID, cardIDsToAllocate)
if err != nil {
@@ -235,6 +157,7 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
AuthorizedBy: currentUserID,
AuthorizedAt: now,
AuthorizerType: userType,
Remark: req.Remark,
})
}

View File

@@ -0,0 +1,621 @@
package enterprise_device
import (
"context"
"fmt"
"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, fmt.Errorf("查询设备信息失败: %w", 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, fmt.Errorf("查询已有授权失败: %w", 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 fmt.Errorf("创建设备授权记录失败: %w", 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 fmt.Errorf("查询设备绑定卡失败: %w", 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 fmt.Errorf("创建卡授权记录失败: %w", 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, fmt.Errorf("查询设备信息失败: %w", 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, fmt.Errorf("查询授权状态失败: %w", 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 fmt.Errorf("撤销设备授权失败: %w", err)
}
// 2. 级联撤销卡授权
for _, authID := range deviceAuthsToRevoke {
if err := s.enterpriseCardAuthStore.RevokeByDeviceAuthID(ctx, authID, currentUserID); err != nil {
return fmt.Errorf("撤销卡授权失败: %w", 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, fmt.Errorf("查询授权记录失败: %w", 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, fmt.Errorf("查询设备信息失败: %w", 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, fmt.Errorf("查询设备绑定卡失败: %w", 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, fmt.Errorf("查询授权记录失败: %w", 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, fmt.Errorf("查询设备信息失败: %w", 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, fmt.Errorf("查询设备绑定卡失败: %w", 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, fmt.Errorf("查询设备信息失败: %w", 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, fmt.Errorf("查询设备绑定卡失败: %w", 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, fmt.Errorf("查询卡信息失败: %w", 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, fmt.Errorf("停机操作失败: %w", 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, fmt.Errorf("复机操作失败: %w", 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 "停机"
}

View File

@@ -0,0 +1,916 @@
package enterprise_device
import (
"context"
"fmt"
"testing"
"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/middleware"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
func uniqueServiceTestPrefix() string {
return fmt.Sprintf("SVC%d", time.Now().UnixNano()%1000000000)
}
func createTestContext(userID uint, userType int, shopID uint, enterpriseID uint) context.Context {
ctx := context.Background()
return middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: userID,
UserType: userType,
ShopID: shopID,
EnterpriseID: enterpriseID,
})
}
type testEnv struct {
service *Service
enterprise *model.Enterprise
shop *model.Shop
devices []*model.Device
cards []*model.IotCard
bindings []*model.DeviceSimBinding
carrier *model.Carrier
}
func setupTestEnv(t *testing.T, prefix string) *testEnv {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
shop := &model.Shop{
ShopName: prefix + "_测试店铺",
ShopCode: prefix,
Level: 1,
Status: 1,
}
require.NoError(t, tx.Create(shop).Error)
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
OwnerShopID: &shop.ID,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
devices := make([]*model.Device, 3)
for i := 0; i < 3; i++ {
devices[i] = &model.Device{
DeviceNo: fmt.Sprintf("%s_D%03d", prefix, i+1),
DeviceName: fmt.Sprintf("测试设备%d", i+1),
Status: 2,
ShopID: &shop.ID,
}
require.NoError(t, tx.Create(devices[i]).Error)
}
cards := make([]*model.IotCard, 4)
for i := 0; i < 4; i++ {
cards[i] = &model.IotCard{
ICCID: fmt.Sprintf("%s%04d", prefix, i+1),
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
ShopID: &shop.ID,
}
require.NoError(t, tx.Create(cards[i]).Error)
}
now := time.Now()
bindings := []*model.DeviceSimBinding{
{DeviceID: devices[0].ID, IotCardID: cards[0].ID, SlotPosition: 1, BindStatus: 1, BindTime: &now},
{DeviceID: devices[0].ID, IotCardID: cards[1].ID, SlotPosition: 2, BindStatus: 1, BindTime: &now},
{DeviceID: devices[1].ID, IotCardID: cards[2].ID, SlotPosition: 1, BindStatus: 1, BindTime: &now},
}
for _, b := range bindings {
require.NoError(t, tx.Create(b).Error)
}
return &testEnv{
service: svc,
enterprise: enterprise,
shop: shop,
devices: devices,
cards: cards,
bindings: bindings,
carrier: carrier,
}
}
func TestService_AllocateDevices(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
tests := []struct {
name string
ctx context.Context
req *dto.AllocateDevicesReq
wantSuccess int
wantFail int
wantErr bool
}{
{
name: "平台用户成功授权设备",
ctx: createTestContext(1, constants.UserTypePlatform, 0, 0),
req: &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
Remark: "测试授权",
},
wantSuccess: 1,
wantFail: 0,
wantErr: false,
},
{
name: "代理用户成功授权自己店铺的设备",
ctx: createTestContext(2, constants.UserTypeAgent, env.shop.ID, 0),
req: &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[1].DeviceNo},
},
wantSuccess: 1,
wantFail: 0,
wantErr: false,
},
{
name: "设备不存在时记录失败",
ctx: createTestContext(1, constants.UserTypePlatform, 0, 0),
req: &dto.AllocateDevicesReq{
DeviceNos: []string{"NOT_EXIST_DEVICE"},
},
wantSuccess: 0,
wantFail: 1,
wantErr: false,
},
{
name: "未授权用户返回错误",
ctx: context.Background(),
req: &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[2].DeviceNo},
},
wantSuccess: 0,
wantFail: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := env.service.AllocateDevices(tt.ctx, env.enterprise.ID, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantSuccess, resp.SuccessCount)
assert.Equal(t, tt.wantFail, resp.FailCount)
})
}
}
func TestService_AllocateDevices_DeviceStatusValidation(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
prefix := uniqueServiceTestPrefix()
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
inStockDevice := &model.Device{
DeviceNo: prefix + "_INSTOCK",
DeviceName: "在库设备",
Status: 1,
}
require.NoError(t, tx.Create(inStockDevice).Error)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
t.Run("设备状态不是已分销时失败", func(t *testing.T) {
req := &dto.AllocateDevicesReq{
DeviceNos: []string{inStockDevice.DeviceNo},
}
resp, err := svc.AllocateDevices(ctx, enterprise.ID, req)
require.NoError(t, err)
assert.Equal(t, 0, resp.SuccessCount)
assert.Equal(t, 1, resp.FailCount)
assert.Contains(t, resp.FailedItems[0].Reason, "状态不正确")
})
}
func TestService_AllocateDevices_AgentPermission(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
prefix := uniqueServiceTestPrefix()
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
shop1 := &model.Shop{ShopName: prefix + "_店铺1", ShopCode: prefix + "1", Level: 1, Status: 1}
require.NoError(t, tx.Create(shop1).Error)
shop2 := &model.Shop{ShopName: prefix + "_店铺2", ShopCode: prefix + "2", Level: 1, Status: 1}
require.NoError(t, tx.Create(shop2).Error)
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
device := &model.Device{
DeviceNo: prefix + "_D001",
DeviceName: "测试设备",
Status: 2,
ShopID: &shop1.ID,
}
require.NoError(t, tx.Create(device).Error)
t.Run("代理用户无法授权其他店铺的设备", func(t *testing.T) {
ctx := createTestContext(1, constants.UserTypeAgent, shop2.ID, 0)
req := &dto.AllocateDevicesReq{
DeviceNos: []string{device.DeviceNo},
}
resp, err := svc.AllocateDevices(ctx, enterprise.ID, req)
require.NoError(t, err)
assert.Equal(t, 0, resp.SuccessCount)
assert.Equal(t, 1, resp.FailCount)
assert.Contains(t, resp.FailedItems[0].Reason, "无权操作")
})
}
func TestService_AllocateDevices_DuplicateAuthorization(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
req := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
resp, err := env.service.AllocateDevices(ctx, env.enterprise.ID, req)
require.NoError(t, err)
assert.Equal(t, 1, resp.SuccessCount)
t.Run("重复授权同一设备时失败", func(t *testing.T) {
resp2, err := env.service.AllocateDevices(ctx, env.enterprise.ID, req)
require.NoError(t, err)
assert.Equal(t, 0, resp2.SuccessCount)
assert.Equal(t, 1, resp2.FailCount)
assert.Contains(t, resp2.FailedItems[0].Reason, "已授权")
})
}
func TestService_AllocateDevices_CascadeCardAuthorization(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
t.Run("授权设备时级联授权绑定的卡", func(t *testing.T) {
req := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
resp, err := env.service.AllocateDevices(ctx, env.enterprise.ID, req)
require.NoError(t, err)
assert.Equal(t, 1, resp.SuccessCount)
assert.Len(t, resp.AuthorizedDevices, 1)
assert.Equal(t, 2, resp.AuthorizedDevices[0].CardCount)
})
}
func TestService_RecallDevices(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo, env.devices[1].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
tests := []struct {
name string
req *dto.RecallDevicesReq
wantSuccess int
wantFail int
wantErr bool
}{
{
name: "成功撤销授权",
req: &dto.RecallDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
},
wantSuccess: 1,
wantFail: 0,
wantErr: false,
},
{
name: "设备不存在时失败",
req: &dto.RecallDevicesReq{
DeviceNos: []string{"NOT_EXIST"},
},
wantSuccess: 0,
wantFail: 1,
wantErr: false,
},
{
name: "设备未授权时失败",
req: &dto.RecallDevicesReq{
DeviceNos: []string{env.devices[2].DeviceNo},
},
wantSuccess: 0,
wantFail: 1,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := env.service.RecallDevices(ctx, env.enterprise.ID, tt.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantSuccess, resp.SuccessCount)
assert.Equal(t, tt.wantFail, resp.FailCount)
})
}
}
func TestService_RecallDevices_Unauthorized(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
t.Run("未授权用户返回错误", func(t *testing.T) {
req := &dto.RecallDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.RecallDevices(context.Background(), env.enterprise.ID, req)
require.Error(t, err)
})
}
func TestService_ListDevices(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo, env.devices[1].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
tests := []struct {
name string
req *dto.EnterpriseDeviceListReq
wantTotal int64
wantLen int
}{
{
name: "获取所有授权设备",
req: &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 10},
wantTotal: 2,
wantLen: 2,
},
{
name: "分页查询",
req: &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 1},
wantTotal: 2,
wantLen: 1,
},
{
name: "按设备号搜索",
req: &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 10, DeviceNo: env.devices[0].DeviceNo},
wantTotal: 2,
wantLen: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := env.service.ListDevices(ctx, env.enterprise.ID, tt.req)
require.NoError(t, err)
assert.Equal(t, tt.wantTotal, resp.Total)
assert.Len(t, resp.List, tt.wantLen)
})
}
}
func TestService_ListDevices_EnterpriseNotFound(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
t.Run("企业不存在返回错误", func(t *testing.T) {
req := &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 10}
_, err := env.service.ListDevices(ctx, 99999, req)
require.Error(t, err)
})
}
func TestService_ListDevicesForEnterprise(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
t.Run("企业用户获取自己的授权设备", func(t *testing.T) {
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, env.enterprise.ID)
req := &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 10}
resp, err := env.service.ListDevicesForEnterprise(enterpriseCtx, req)
require.NoError(t, err)
assert.Equal(t, int64(1), resp.Total)
})
t.Run("未设置企业ID返回错误", func(t *testing.T) {
req := &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 10}
_, err := env.service.ListDevicesForEnterprise(context.Background(), req)
require.Error(t, err)
})
}
func TestService_GetDeviceDetail(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, env.enterprise.ID)
t.Run("成功获取设备详情", func(t *testing.T) {
resp, err := env.service.GetDeviceDetail(enterpriseCtx, env.devices[0].ID)
require.NoError(t, err)
assert.Equal(t, env.devices[0].ID, resp.Device.DeviceID)
assert.Equal(t, env.devices[0].DeviceNo, resp.Device.DeviceNo)
assert.Len(t, resp.Cards, 2)
})
t.Run("设备未授权时返回错误", func(t *testing.T) {
_, err := env.service.GetDeviceDetail(enterpriseCtx, env.devices[1].ID)
require.Error(t, err)
})
t.Run("未设置企业ID返回错误", func(t *testing.T) {
_, err := env.service.GetDeviceDetail(context.Background(), env.devices[0].ID)
require.Error(t, err)
})
}
func TestService_SuspendCard(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, env.enterprise.ID)
t.Run("成功停机", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试停机"}
resp, err := env.service.SuspendCard(enterpriseCtx, env.devices[0].ID, env.cards[0].ID, req)
require.NoError(t, err)
assert.True(t, resp.Success)
})
t.Run("卡不属于设备时返回错误", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试停机"}
_, err := env.service.SuspendCard(enterpriseCtx, env.devices[0].ID, env.cards[3].ID, req)
require.Error(t, err)
})
t.Run("设备未授权时返回错误", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试停机"}
_, err := env.service.SuspendCard(enterpriseCtx, env.devices[1].ID, env.cards[2].ID, req)
require.Error(t, err)
})
t.Run("未设置企业ID返回错误", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试停机"}
_, err := env.service.SuspendCard(context.Background(), env.devices[0].ID, env.cards[0].ID, req)
require.Error(t, err)
})
}
func TestService_ResumeCard(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, env.enterprise.ID)
t.Run("成功复机", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试复机"}
resp, err := env.service.ResumeCard(enterpriseCtx, env.devices[0].ID, env.cards[0].ID, req)
require.NoError(t, err)
assert.True(t, resp.Success)
})
t.Run("卡不属于设备时返回错误", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试复机"}
_, err := env.service.ResumeCard(enterpriseCtx, env.devices[0].ID, env.cards[3].ID, req)
require.Error(t, err)
})
t.Run("设备未授权时返回错误", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试复机"}
_, err := env.service.ResumeCard(enterpriseCtx, env.devices[1].ID, env.cards[2].ID, req)
require.Error(t, err)
})
t.Run("未设置企业ID返回错误", func(t *testing.T) {
req := &dto.DeviceCardOperationReq{Reason: "测试复机"}
_, err := env.service.ResumeCard(context.Background(), env.devices[0].ID, env.cards[0].ID, req)
require.Error(t, err)
})
}
func TestService_ListDevices_EmptyResult(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
t.Run("企业无授权设备时返回空列表", func(t *testing.T) {
req := &dto.EnterpriseDeviceListReq{Page: 1, PageSize: 10}
resp, err := env.service.ListDevices(ctx, env.enterprise.ID, req)
require.NoError(t, err)
assert.Equal(t, int64(0), resp.Total)
assert.Empty(t, resp.List)
})
}
func TestService_GetDeviceDetail_WithCarrierInfo(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, env.enterprise.ID)
t.Run("获取设备详情包含运营商信息", func(t *testing.T) {
resp, err := env.service.GetDeviceDetail(enterpriseCtx, env.devices[0].ID)
require.NoError(t, err)
assert.Len(t, resp.Cards, 2)
for _, card := range resp.Cards {
assert.NotEmpty(t, card.CarrierName)
}
})
}
func TestService_GetDeviceDetail_NetworkStatus(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
_, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, env.enterprise.ID)
t.Run("网络状态名称正确", func(t *testing.T) {
resp, err := env.service.GetDeviceDetail(enterpriseCtx, env.devices[0].ID)
require.NoError(t, err)
for _, card := range resp.Cards {
if card.NetworkStatus == 1 {
assert.Equal(t, "开机", card.NetworkStatusName)
} else {
assert.Equal(t, "停机", card.NetworkStatusName)
}
}
})
}
func TestService_GetDeviceDetail_DeviceWithoutCards(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
prefix := uniqueServiceTestPrefix()
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
device := &model.Device{
DeviceNo: prefix + "_D001",
DeviceName: "无卡设备",
Status: 2,
}
require.NoError(t, tx.Create(device).Error)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{device.DeviceNo},
}
_, err := svc.AllocateDevices(ctx, enterprise.ID, allocateReq)
require.NoError(t, err)
t.Run("设备无绑定卡时返回空卡列表", func(t *testing.T) {
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, enterprise.ID)
resp, err := svc.GetDeviceDetail(enterpriseCtx, device.ID)
require.NoError(t, err)
assert.Equal(t, device.ID, resp.Device.DeviceID)
assert.Empty(t, resp.Cards)
})
}
func TestService_RecallDevices_CascadeRevoke(t *testing.T) {
prefix := uniqueServiceTestPrefix()
env := setupTestEnv(t, prefix)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
resp, err := env.service.AllocateDevices(ctx, env.enterprise.ID, allocateReq)
require.NoError(t, err)
assert.Equal(t, 2, resp.AuthorizedDevices[0].CardCount)
t.Run("撤销设备授权时级联撤销卡授权", func(t *testing.T) {
recallReq := &dto.RecallDevicesReq{
DeviceNos: []string{env.devices[0].DeviceNo},
}
recallResp, err := env.service.RecallDevices(ctx, env.enterprise.ID, recallReq)
require.NoError(t, err)
assert.Equal(t, 1, recallResp.SuccessCount)
})
}
func TestService_GetDeviceDetail_WithNetworkStatusOn(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
prefix := uniqueServiceTestPrefix()
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
device := &model.Device{
DeviceNo: prefix + "_D001",
DeviceName: "测试设备",
Status: 2,
}
require.NoError(t, tx.Create(device).Error)
card := &model.IotCard{
ICCID: prefix + "0001",
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
NetworkStatus: 1,
}
require.NoError(t, tx.Create(card).Error)
now := time.Now()
binding := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
require.NoError(t, tx.Create(binding).Error)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
allocateReq := &dto.AllocateDevicesReq{
DeviceNos: []string{device.DeviceNo},
}
_, err := svc.AllocateDevices(ctx, enterprise.ID, allocateReq)
require.NoError(t, err)
t.Run("开机状态卡显示正确", func(t *testing.T) {
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, enterprise.ID)
resp, err := svc.GetDeviceDetail(enterpriseCtx, device.ID)
require.NoError(t, err)
assert.Len(t, resp.Cards, 1)
assert.Equal(t, 1, resp.Cards[0].NetworkStatus)
assert.Equal(t, "开机", resp.Cards[0].NetworkStatusName)
})
}
func TestService_EnterpriseNotFound(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
ctx := createTestContext(1, constants.UserTypePlatform, 0, 0)
t.Run("AllocateDevices企业不存在", func(t *testing.T) {
req := &dto.AllocateDevicesReq{DeviceNos: []string{"D001"}}
_, err := svc.AllocateDevices(ctx, 99999, req)
require.Error(t, err)
})
t.Run("RecallDevices企业不存在", func(t *testing.T) {
req := &dto.RecallDevicesReq{DeviceNos: []string{"D001"}}
_, err := svc.RecallDevices(ctx, 99999, req)
require.Error(t, err)
})
}
func TestService_ValidateCardOperation_RevokedDeviceAuth(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
prefix := uniqueServiceTestPrefix()
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
deviceSimBindingStore := postgres.NewDeviceSimBindingStore(tx, rdb)
enterpriseDeviceAuthStore := postgres.NewEnterpriseDeviceAuthorizationStore(tx, rdb)
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
logger := zap.NewNop()
svc := New(tx, enterpriseStore, deviceStore, deviceSimBindingStore, enterpriseDeviceAuthStore, enterpriseCardAuthStore, logger)
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
device := &model.Device{
DeviceNo: prefix + "_D001",
DeviceName: "测试设备",
Status: 2,
}
require.NoError(t, tx.Create(device).Error)
card := &model.IotCard{
ICCID: prefix + "0001",
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
}
require.NoError(t, tx.Create(card).Error)
now := time.Now()
binding := &model.DeviceSimBinding{
DeviceID: device.ID,
IotCardID: card.ID,
SlotPosition: 1,
BindStatus: 1,
BindTime: &now,
}
require.NoError(t, tx.Create(binding).Error)
deviceAuth := &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterprise.ID,
DeviceID: device.ID,
AuthorizedBy: 1,
AuthorizedAt: now,
AuthorizerType: 2,
RevokedBy: ptrUintED(1),
RevokedAt: &now,
}
require.NoError(t, tx.Create(deviceAuth).Error)
t.Run("已撤销的设备授权无法操作卡", func(t *testing.T) {
enterpriseCtx := createTestContext(1, constants.UserTypeEnterprise, 0, enterprise.ID)
req := &dto.DeviceCardOperationReq{Reason: "测试"}
_, err := svc.SuspendCard(enterpriseCtx, device.ID, card.ID, req)
require.Error(t, err)
})
}
func ptrUintED(v uint) *uint {
return &v
}

View File

@@ -394,3 +394,13 @@ func (s *EnterpriseCardAuthorizationStore) GetByID(ctx context.Context, id uint)
}
return &auth, nil
}
func (s *EnterpriseCardAuthorizationStore) RevokeByDeviceAuthID(ctx context.Context, deviceAuthID uint, revokedBy uint) error {
now := time.Now()
return s.db.WithContext(ctx).Model(&model.EnterpriseCardAuthorization{}).
Where("device_auth_id = ? AND revoked_at IS NULL", deviceAuthID).
Updates(map[string]interface{}{
"revoked_by": revokedBy,
"revoked_at": now,
}).Error
}

View File

@@ -0,0 +1,309 @@
package postgres
import (
"context"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func uniqueCardAuthTestPrefix() string {
return fmt.Sprintf("ECA%d", time.Now().UnixNano()%1000000000)
}
func TestEnterpriseCardAuthorizationStore_RevokeByDeviceAuthID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseCardAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueCardAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
cards := []*model.IotCard{
{ICCID: prefix + "0001", CardType: "normal", CarrierID: carrier.ID, Status: 2},
{ICCID: prefix + "0002", CardType: "normal", CarrierID: carrier.ID, Status: 2},
{ICCID: prefix + "0003", CardType: "normal", CarrierID: carrier.ID, Status: 2},
}
for _, c := range cards {
require.NoError(t, tx.Create(c).Error)
}
deviceAuthID := uint(12345)
now := time.Now()
auths := []*model.EnterpriseCardAuthorization{
{EnterpriseID: enterprise.ID, CardID: cards[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, DeviceAuthID: &deviceAuthID},
{EnterpriseID: enterprise.ID, CardID: cards[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, DeviceAuthID: &deviceAuthID},
{EnterpriseID: enterprise.ID, CardID: cards[2].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, DeviceAuthID: nil},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("成功撤销指定设备授权ID关联的卡授权", func(t *testing.T) {
revokerID := uint(2)
err := store.RevokeByDeviceAuthID(ctx, deviceAuthID, revokerID)
require.NoError(t, err)
result, err := store.ListByEnterprise(ctx, enterprise.ID, false)
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, cards[2].ID, result[0].CardID)
revokedResult, err := store.ListByEnterprise(ctx, enterprise.ID, true)
require.NoError(t, err)
assert.Len(t, revokedResult, 3)
for _, auth := range revokedResult {
if auth.DeviceAuthID != nil && *auth.DeviceAuthID == deviceAuthID {
assert.NotNil(t, auth.RevokedAt)
assert.NotNil(t, auth.RevokedBy)
assert.Equal(t, revokerID, *auth.RevokedBy)
}
}
})
t.Run("设备授权ID不存在时不报错", func(t *testing.T) {
err := store.RevokeByDeviceAuthID(ctx, 99999, uint(1))
require.NoError(t, err)
})
}
func TestEnterpriseCardAuthorizationStore_Create(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseCardAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueCardAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
card := &model.IotCard{
ICCID: prefix + "0001",
CardType: "normal",
CarrierID: carrier.ID,
Status: 2,
}
require.NoError(t, tx.Create(card).Error)
t.Run("成功创建卡授权记录", func(t *testing.T) {
auth := &model.EnterpriseCardAuthorization{
EnterpriseID: enterprise.ID,
CardID: card.ID,
AuthorizedBy: 1,
AuthorizedAt: time.Now(),
AuthorizerType: 2,
Remark: "测试授权",
}
err := store.Create(ctx, auth)
require.NoError(t, err)
assert.NotZero(t, auth.ID)
})
}
func TestEnterpriseCardAuthorizationStore_BatchCreate(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseCardAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueCardAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
cards := []*model.IotCard{
{ICCID: prefix + "0001", CardType: "normal", CarrierID: carrier.ID, Status: 2},
{ICCID: prefix + "0002", CardType: "normal", CarrierID: carrier.ID, Status: 2},
}
for _, c := range cards {
require.NoError(t, tx.Create(c).Error)
}
t.Run("成功批量创建卡授权记录", func(t *testing.T) {
now := time.Now()
auths := []*model.EnterpriseCardAuthorization{
{EnterpriseID: enterprise.ID, CardID: cards[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, CardID: cards[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
}
err := store.BatchCreate(ctx, auths)
require.NoError(t, err)
for _, auth := range auths {
assert.NotZero(t, auth.ID)
}
})
t.Run("空列表不报错", func(t *testing.T) {
err := store.BatchCreate(ctx, []*model.EnterpriseCardAuthorization{})
require.NoError(t, err)
})
}
func TestEnterpriseCardAuthorizationStore_ListByEnterprise(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseCardAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueCardAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
cards := []*model.IotCard{
{ICCID: prefix + "0001", CardType: "normal", CarrierID: carrier.ID, Status: 2},
{ICCID: prefix + "0002", CardType: "normal", CarrierID: carrier.ID, Status: 2},
}
for _, c := range cards {
require.NoError(t, tx.Create(c).Error)
}
now := time.Now()
auths := []*model.EnterpriseCardAuthorization{
{EnterpriseID: enterprise.ID, CardID: cards[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, CardID: cards[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, RevokedBy: ptrUintCA(1), RevokedAt: &now},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("获取未撤销的授权记录", func(t *testing.T) {
result, err := store.ListByEnterprise(ctx, enterprise.ID, false)
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, cards[0].ID, result[0].CardID)
})
t.Run("获取所有授权记录包括已撤销", func(t *testing.T) {
result, err := store.ListByEnterprise(ctx, enterprise.ID, true)
require.NoError(t, err)
assert.Len(t, result, 2)
})
}
func ptrUintCA(v uint) *uint {
return &v
}
func TestEnterpriseCardAuthorizationStore_GetActiveAuthsByCardIDs(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseCardAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueCardAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
carrier := &model.Carrier{
CarrierName: "测试运营商",
CarrierType: "CMCC",
Status: 1,
}
require.NoError(t, tx.Create(carrier).Error)
cards := []*model.IotCard{
{ICCID: prefix + "0001", CardType: "normal", CarrierID: carrier.ID, Status: 2},
{ICCID: prefix + "0002", CardType: "normal", CarrierID: carrier.ID, Status: 2},
{ICCID: prefix + "0003", CardType: "normal", CarrierID: carrier.ID, Status: 2},
}
for _, c := range cards {
require.NoError(t, tx.Create(c).Error)
}
now := time.Now()
auths := []*model.EnterpriseCardAuthorization{
{EnterpriseID: enterprise.ID, CardID: cards[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, CardID: cards[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, RevokedBy: ptrUintCA(1), RevokedAt: &now},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("获取有效授权的卡ID映射", func(t *testing.T) {
cardIDs := []uint{cards[0].ID, cards[1].ID, cards[2].ID}
result, err := store.GetActiveAuthsByCardIDs(ctx, enterprise.ID, cardIDs)
require.NoError(t, err)
assert.True(t, result[cards[0].ID])
assert.False(t, result[cards[1].ID])
assert.False(t, result[cards[2].ID])
})
t.Run("空卡ID列表返回空映射", func(t *testing.T) {
result, err := store.GetActiveAuthsByCardIDs(ctx, enterprise.ID, []uint{})
require.NoError(t, err)
assert.Empty(t, result)
})
}

View File

@@ -0,0 +1,160 @@
package postgres
import (
"context"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type EnterpriseDeviceAuthorizationStore struct {
db *gorm.DB
redis *redis.Client
}
func NewEnterpriseDeviceAuthorizationStore(db *gorm.DB, redis *redis.Client) *EnterpriseDeviceAuthorizationStore {
return &EnterpriseDeviceAuthorizationStore{
db: db,
redis: redis,
}
}
func (s *EnterpriseDeviceAuthorizationStore) Create(ctx context.Context, auth *model.EnterpriseDeviceAuthorization) error {
return s.db.WithContext(ctx).Create(auth).Error
}
func (s *EnterpriseDeviceAuthorizationStore) BatchCreate(ctx context.Context, auths []*model.EnterpriseDeviceAuthorization) error {
if len(auths) == 0 {
return nil
}
batchSize := 100
for i := 0; i < len(auths); i += batchSize {
end := i + batchSize
if end > len(auths) {
end = len(auths)
}
batch := auths[i:end]
if err := s.db.WithContext(ctx).Create(batch).Error; err != nil {
return err
}
}
return nil
}
func (s *EnterpriseDeviceAuthorizationStore) GetByID(ctx context.Context, id uint) (*model.EnterpriseDeviceAuthorization, error) {
var auth model.EnterpriseDeviceAuthorization
err := s.db.WithContext(ctx).Where("id = ?", id).First(&auth).Error
if err != nil {
return nil, err
}
return &auth, nil
}
func (s *EnterpriseDeviceAuthorizationStore) GetByDeviceID(ctx context.Context, deviceID uint) (*model.EnterpriseDeviceAuthorization, error) {
var auth model.EnterpriseDeviceAuthorization
err := s.db.WithContext(ctx).
Where("device_id = ? AND revoked_at IS NULL", deviceID).
First(&auth).Error
if err != nil {
return nil, err
}
return &auth, nil
}
func (s *EnterpriseDeviceAuthorizationStore) GetByEnterpriseID(ctx context.Context, enterpriseID uint, includeRevoked bool) ([]*model.EnterpriseDeviceAuthorization, error) {
var auths []*model.EnterpriseDeviceAuthorization
query := s.db.WithContext(ctx).Where("enterprise_id = ?", enterpriseID)
if !includeRevoked {
query = query.Where("revoked_at IS NULL")
}
err := query.Find(&auths).Error
return auths, err
}
type DeviceAuthListOptions struct {
EnterpriseID *uint
DeviceIDs []uint
AuthorizerID *uint
IncludeRevoked bool
Page int
PageSize int
}
func (s *EnterpriseDeviceAuthorizationStore) ListByEnterprise(ctx context.Context, opts DeviceAuthListOptions) ([]*model.EnterpriseDeviceAuthorization, int64, error) {
var auths []*model.EnterpriseDeviceAuthorization
var total int64
query := s.db.WithContext(ctx).Model(&model.EnterpriseDeviceAuthorization{})
if opts.EnterpriseID != nil {
query = query.Where("enterprise_id = ?", *opts.EnterpriseID)
}
if len(opts.DeviceIDs) > 0 {
query = query.Where("device_id IN ?", opts.DeviceIDs)
}
if opts.AuthorizerID != nil {
query = query.Where("authorized_by = ?", *opts.AuthorizerID)
}
if !opts.IncludeRevoked {
query = query.Where("revoked_at IS NULL")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if opts.Page > 0 && opts.PageSize > 0 {
offset := (opts.Page - 1) * opts.PageSize
query = query.Offset(offset).Limit(opts.PageSize)
}
err := query.Order("authorized_at DESC").Find(&auths).Error
return auths, total, err
}
func (s *EnterpriseDeviceAuthorizationStore) RevokeByIDs(ctx context.Context, ids []uint, revokedBy uint) error {
now := time.Now()
return s.db.WithContext(ctx).
Model(&model.EnterpriseDeviceAuthorization{}).
Where("id IN ? AND revoked_at IS NULL", ids).
Updates(map[string]interface{}{
"revoked_by": revokedBy,
"revoked_at": now,
}).Error
}
func (s *EnterpriseDeviceAuthorizationStore) GetActiveAuthsByDeviceIDs(ctx context.Context, enterpriseID uint, deviceIDs []uint) (map[uint]bool, error) {
if len(deviceIDs) == 0 {
return make(map[uint]bool), nil
}
var auths []model.EnterpriseDeviceAuthorization
err := s.db.WithContext(ctx).
Select("device_id").
Where("enterprise_id = ? AND device_id IN ? AND revoked_at IS NULL", enterpriseID, deviceIDs).
Find(&auths).Error
if err != nil {
return nil, err
}
result := make(map[uint]bool, len(auths))
for _, auth := range auths {
result[auth.DeviceID] = true
}
return result, nil
}
func (s *EnterpriseDeviceAuthorizationStore) ListDeviceIDsByEnterprise(ctx context.Context, enterpriseID uint) ([]uint, error) {
var deviceIDs []uint
err := s.db.WithContext(ctx).
Model(&model.EnterpriseDeviceAuthorization{}).
Where("enterprise_id = ? AND revoked_at IS NULL", enterpriseID).
Pluck("device_id", &deviceIDs).Error
return deviceIDs, err
}

View File

@@ -0,0 +1,517 @@
package postgres
import (
"context"
"fmt"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/tests/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func uniqueDeviceAuthTestPrefix() string {
return fmt.Sprintf("EDA%d", time.Now().UnixNano()%1000000000)
}
func TestEnterpriseDeviceAuthorizationStore_Create(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
device := &model.Device{
DeviceNo: prefix + "_001",
DeviceName: "测试设备1",
Status: 2,
}
require.NoError(t, tx.Create(device).Error)
t.Run("成功创建授权记录", func(t *testing.T) {
auth := &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterprise.ID,
DeviceID: device.ID,
AuthorizedBy: 1,
AuthorizedAt: time.Now(),
AuthorizerType: 2,
Remark: "测试授权",
}
err := store.Create(ctx, auth)
require.NoError(t, err)
assert.NotZero(t, auth.ID)
assert.Equal(t, enterprise.ID, auth.EnterpriseID)
assert.Equal(t, device.ID, auth.DeviceID)
})
}
func TestEnterpriseDeviceAuthorizationStore_BatchCreate(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
devices := []*model.Device{
{DeviceNo: prefix + "_001", DeviceName: "测试设备1", Status: 2},
{DeviceNo: prefix + "_002", DeviceName: "测试设备2", Status: 2},
{DeviceNo: prefix + "_003", DeviceName: "测试设备3", Status: 2},
}
for _, d := range devices {
require.NoError(t, tx.Create(d).Error)
}
t.Run("成功批量创建授权记录", func(t *testing.T) {
now := time.Now()
auths := []*model.EnterpriseDeviceAuthorization{
{EnterpriseID: enterprise.ID, DeviceID: devices[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, DeviceID: devices[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, DeviceID: devices[2].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
}
err := store.BatchCreate(ctx, auths)
require.NoError(t, err)
for _, auth := range auths {
assert.NotZero(t, auth.ID)
}
})
t.Run("空列表不报错", func(t *testing.T) {
err := store.BatchCreate(ctx, []*model.EnterpriseDeviceAuthorization{})
require.NoError(t, err)
})
}
func TestEnterpriseDeviceAuthorizationStore_GetByID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
device := &model.Device{
DeviceNo: prefix + "_001",
DeviceName: "测试设备1",
Status: 2,
}
require.NoError(t, tx.Create(device).Error)
auth := &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterprise.ID,
DeviceID: device.ID,
AuthorizedBy: 1,
AuthorizedAt: time.Now(),
AuthorizerType: 2,
Remark: "测试备注",
}
require.NoError(t, store.Create(ctx, auth))
t.Run("成功获取授权记录", func(t *testing.T) {
result, err := store.GetByID(ctx, auth.ID)
require.NoError(t, err)
assert.Equal(t, auth.ID, result.ID)
assert.Equal(t, enterprise.ID, result.EnterpriseID)
assert.Equal(t, device.ID, result.DeviceID)
assert.Equal(t, "测试备注", result.Remark)
})
t.Run("记录不存在返回错误", func(t *testing.T) {
_, err := store.GetByID(ctx, 99999)
require.Error(t, err)
})
}
func TestEnterpriseDeviceAuthorizationStore_GetByDeviceID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
device := &model.Device{
DeviceNo: prefix + "_001",
DeviceName: "测试设备1",
Status: 2,
}
require.NoError(t, tx.Create(device).Error)
auth := &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterprise.ID,
DeviceID: device.ID,
AuthorizedBy: 1,
AuthorizedAt: time.Now(),
AuthorizerType: 2,
}
require.NoError(t, store.Create(ctx, auth))
t.Run("成功通过设备ID获取授权记录", func(t *testing.T) {
result, err := store.GetByDeviceID(ctx, device.ID)
require.NoError(t, err)
assert.Equal(t, auth.ID, result.ID)
assert.Equal(t, enterprise.ID, result.EnterpriseID)
})
t.Run("设备未授权返回错误", func(t *testing.T) {
_, err := store.GetByDeviceID(ctx, 99999)
require.Error(t, err)
})
t.Run("已撤销的授权不返回", func(t *testing.T) {
device2 := &model.Device{
DeviceNo: prefix + "_002",
DeviceName: "测试设备2",
Status: 2,
}
require.NoError(t, tx.Create(device2).Error)
now := time.Now()
revokedAuth := &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterprise.ID,
DeviceID: device2.ID,
AuthorizedBy: 1,
AuthorizedAt: now,
AuthorizerType: 2,
RevokedBy: ptrUint(1),
RevokedAt: &now,
}
require.NoError(t, store.Create(ctx, revokedAuth))
_, err := store.GetByDeviceID(ctx, device2.ID)
require.Error(t, err)
})
}
func TestEnterpriseDeviceAuthorizationStore_GetByEnterpriseID(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
devices := []*model.Device{
{DeviceNo: prefix + "_001", DeviceName: "测试设备1", Status: 2},
{DeviceNo: prefix + "_002", DeviceName: "测试设备2", Status: 2},
}
for _, d := range devices {
require.NoError(t, tx.Create(d).Error)
}
now := time.Now()
auths := []*model.EnterpriseDeviceAuthorization{
{EnterpriseID: enterprise.ID, DeviceID: devices[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, DeviceID: devices[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, RevokedBy: ptrUint(1), RevokedAt: &now},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("获取未撤销的授权记录", func(t *testing.T) {
result, err := store.GetByEnterpriseID(ctx, enterprise.ID, false)
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, devices[0].ID, result[0].DeviceID)
})
t.Run("获取所有授权记录包括已撤销", func(t *testing.T) {
result, err := store.GetByEnterpriseID(ctx, enterprise.ID, true)
require.NoError(t, err)
assert.Len(t, result, 2)
})
}
func TestEnterpriseDeviceAuthorizationStore_ListByEnterprise(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
devices := make([]*model.Device, 5)
for i := 0; i < 5; i++ {
devices[i] = &model.Device{
DeviceNo: fmt.Sprintf("%s_%03d", prefix, i+1),
DeviceName: fmt.Sprintf("测试设备%d", i+1),
Status: 2,
}
require.NoError(t, tx.Create(devices[i]).Error)
}
now := time.Now()
for i, d := range devices {
auth := &model.EnterpriseDeviceAuthorization{
EnterpriseID: enterprise.ID,
DeviceID: d.ID,
AuthorizedBy: uint(i + 1),
AuthorizedAt: now.Add(time.Duration(i) * time.Minute),
AuthorizerType: 2,
}
require.NoError(t, store.Create(ctx, auth))
}
t.Run("分页查询", func(t *testing.T) {
opts := DeviceAuthListOptions{
EnterpriseID: &enterprise.ID,
Page: 1,
PageSize: 2,
}
result, total, err := store.ListByEnterprise(ctx, opts)
require.NoError(t, err)
assert.Equal(t, int64(5), total)
assert.Len(t, result, 2)
})
t.Run("按授权人过滤", func(t *testing.T) {
authorizerID := uint(1)
opts := DeviceAuthListOptions{
EnterpriseID: &enterprise.ID,
AuthorizerID: &authorizerID,
Page: 1,
PageSize: 10,
}
result, total, err := store.ListByEnterprise(ctx, opts)
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, result, 1)
})
t.Run("按设备ID过滤", func(t *testing.T) {
opts := DeviceAuthListOptions{
EnterpriseID: &enterprise.ID,
DeviceIDs: []uint{devices[0].ID, devices[1].ID},
Page: 1,
PageSize: 10,
}
result, total, err := store.ListByEnterprise(ctx, opts)
require.NoError(t, err)
assert.Equal(t, int64(2), total)
assert.Len(t, result, 2)
})
}
func TestEnterpriseDeviceAuthorizationStore_RevokeByIDs(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
devices := []*model.Device{
{DeviceNo: prefix + "_001", DeviceName: "测试设备1", Status: 2},
{DeviceNo: prefix + "_002", DeviceName: "测试设备2", Status: 2},
}
for _, d := range devices {
require.NoError(t, tx.Create(d).Error)
}
now := time.Now()
auths := []*model.EnterpriseDeviceAuthorization{
{EnterpriseID: enterprise.ID, DeviceID: devices[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, DeviceID: devices[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("成功撤销授权", func(t *testing.T) {
revokerID := uint(2)
err := store.RevokeByIDs(ctx, []uint{auths[0].ID}, revokerID)
require.NoError(t, err)
result, err := store.GetByID(ctx, auths[0].ID)
require.NoError(t, err)
assert.NotNil(t, result.RevokedAt)
assert.NotNil(t, result.RevokedBy)
assert.Equal(t, revokerID, *result.RevokedBy)
})
t.Run("已撤销的记录不再被重复撤销", func(t *testing.T) {
err := store.RevokeByIDs(ctx, []uint{auths[0].ID}, uint(3))
require.NoError(t, err)
result, err := store.GetByID(ctx, auths[0].ID)
require.NoError(t, err)
assert.Equal(t, uint(2), *result.RevokedBy)
})
}
func TestEnterpriseDeviceAuthorizationStore_GetActiveAuthsByDeviceIDs(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
devices := []*model.Device{
{DeviceNo: prefix + "_001", DeviceName: "测试设备1", Status: 2},
{DeviceNo: prefix + "_002", DeviceName: "测试设备2", Status: 2},
{DeviceNo: prefix + "_003", DeviceName: "测试设备3", Status: 2},
}
for _, d := range devices {
require.NoError(t, tx.Create(d).Error)
}
now := time.Now()
auths := []*model.EnterpriseDeviceAuthorization{
{EnterpriseID: enterprise.ID, DeviceID: devices[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, DeviceID: devices[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2, RevokedBy: ptrUint(1), RevokedAt: &now},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("获取有效授权的设备ID映射", func(t *testing.T) {
deviceIDs := []uint{devices[0].ID, devices[1].ID, devices[2].ID}
result, err := store.GetActiveAuthsByDeviceIDs(ctx, enterprise.ID, deviceIDs)
require.NoError(t, err)
assert.True(t, result[devices[0].ID])
assert.False(t, result[devices[1].ID])
assert.False(t, result[devices[2].ID])
})
t.Run("空设备ID列表返回空映射", func(t *testing.T) {
result, err := store.GetActiveAuthsByDeviceIDs(ctx, enterprise.ID, []uint{})
require.NoError(t, err)
assert.Empty(t, result)
})
}
func TestEnterpriseDeviceAuthorizationStore_ListDeviceIDsByEnterprise(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
store := NewEnterpriseDeviceAuthorizationStore(tx, rdb)
ctx := context.Background()
prefix := uniqueDeviceAuthTestPrefix()
enterprise := &model.Enterprise{
EnterpriseName: prefix + "_测试企业",
EnterpriseCode: prefix,
Status: 1,
}
require.NoError(t, tx.Create(enterprise).Error)
devices := []*model.Device{
{DeviceNo: prefix + "_001", DeviceName: "测试设备1", Status: 2},
{DeviceNo: prefix + "_002", DeviceName: "测试设备2", Status: 2},
}
for _, d := range devices {
require.NoError(t, tx.Create(d).Error)
}
now := time.Now()
auths := []*model.EnterpriseDeviceAuthorization{
{EnterpriseID: enterprise.ID, DeviceID: devices[0].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
{EnterpriseID: enterprise.ID, DeviceID: devices[1].ID, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: 2},
}
for _, auth := range auths {
require.NoError(t, store.Create(ctx, auth))
}
t.Run("获取企业授权设备ID列表", func(t *testing.T) {
result, err := store.ListDeviceIDsByEnterprise(ctx, enterprise.ID)
require.NoError(t, err)
assert.Len(t, result, 2)
assert.Contains(t, result, devices[0].ID)
assert.Contains(t, result, devices[1].ID)
})
t.Run("无授权记录返回空列表", func(t *testing.T) {
result, err := store.ListDeviceIDsByEnterprise(ctx, 99999)
require.NoError(t, err)
assert.Empty(t, result)
})
}
func ptrUint(v uint) *uint {
return &v
}