操作日志
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m10s

This commit is contained in:
2026-04-27 12:16:38 +08:00
parent 95104100c9
commit cb75b5668b
33 changed files with 3860 additions and 112 deletions

View File

@@ -119,8 +119,15 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
PollingManualTrigger: admin.NewPollingManualTriggerHandler(svc.PollingManualTrigger),
Asset: func() *admin.AssetHandler {
pollingQueueMgr := pollingPkg.NewPollingQueueManager(deps.Redis, constants.PollingShardCount, deps.Logger)
assetPollingSvc := pollingSvcPkg.NewAssetPollingService(deviceStore, deviceSimBindingStore, svc.IotCard, pollingQueueMgr, deps.Logger)
h := admin.NewAssetHandler(svc.Asset, svc.Device, svc.IotCard, svc.StopResumeService, assetPollingSvc)
assetPollingSvc := pollingSvcPkg.NewAssetPollingService(
deviceStore,
deviceSimBindingStore,
svc.IotCard,
pollingQueueMgr,
deps.Logger,
svc.AssetAudit,
)
h := admin.NewAssetHandler(svc.Asset, svc.AssetAudit, svc.Device, svc.IotCard, svc.StopResumeService, assetPollingSvc)
h.SetLifecycleService(svc.AssetLifecycle)
return h
}(),

View File

@@ -8,6 +8,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/polling"
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
carrierSvc "github.com/break/junhong_cmp_fiber/internal/service/carrier"
@@ -56,6 +57,7 @@ import (
type services struct {
Account *accountSvc.Service
AccountAudit *accountAuditSvc.Service
AssetAudit *assetAuditSvc.Service
Role *roleSvc.Service
Permission *permissionSvc.Service
PersonalCustomer *personalCustomerSvc.Service
@@ -110,10 +112,22 @@ type services struct {
func initServices(s *stores, deps *Dependencies) *services {
purchaseValidation := purchaseValidationSvc.New(deps.DB, s.IotCard, s.Device, s.Package, s.ShopPackageAllocation)
accountAudit := accountAuditSvc.NewService(s.AccountOperationLog)
assetAudit := assetAuditSvc.NewService(s.AssetOperationLog)
account := accountSvc.New(s.Account, s.Role, s.AccountRole, s.ShopRole, s.Shop, s.Enterprise, accountAudit)
// 创建 IotCard service 并设置回调
iotCard := iotCardSvc.New(deps.DB, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, deps.Logger)
iotCard := iotCardSvc.New(
deps.DB,
s.IotCard,
s.Shop,
s.AssetAllocationRecord,
s.ShopPackageAllocation,
s.ShopSeriesAllocation,
s.PackageSeries,
deps.GatewayClient,
deps.Logger,
assetAudit,
)
// 使用 PollingLifecycleService 替代 APICallback通过分片队列准确操作修复 api_callback.go 遗漏 protect 队列的 Bug3
pollingConfigStore := postgres.NewPollingConfigStore(deps.DB)
pollingConfigMgr := polling.NewPollingConfigManager(pollingConfigStore, deps.Redis, deps.Logger)
@@ -142,17 +156,40 @@ func initServices(s *stores, deps *Dependencies) *services {
deps.Logger,
)
stopResumeService := iotCardSvc.NewStopResumeService(deps.Redis, s.IotCard, s.PackageUsage, s.DeviceSimBinding, deps.GatewayClient, deps.Logger)
stopResumeService := iotCardSvc.NewStopResumeService(
deps.Redis,
s.IotCard,
s.PackageUsage,
s.DeviceSimBinding,
deps.GatewayClient,
deps.Logger,
assetAudit,
)
iotCard.SetRealnameActivator(packageActivation)
iotCard.SetStopResumeService(stopResumeService)
iotCard.SetDeviceSimBindingStore(s.DeviceSimBinding)
iotCard.SetRedisClient(deps.Redis)
device := deviceSvc.New(deps.DB, deps.Redis, s.Device, s.DeviceSimBinding, s.IotCard, s.Shop, s.AssetAllocationRecord, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.PackageSeries, deps.GatewayClient, s.AssetIdentifier)
device := deviceSvc.New(
deps.DB,
deps.Redis,
s.Device,
s.DeviceSimBinding,
s.IotCard,
s.Shop,
s.AssetAllocationRecord,
s.ShopPackageAllocation,
s.ShopSeriesAllocation,
s.PackageSeries,
deps.GatewayClient,
s.AssetIdentifier,
assetAudit,
)
operationPassword := operationPasswordSvc.New(deps.Redis)
return &services{
Account: account,
AccountAudit: accountAudit,
AssetAudit: assetAudit,
Role: roleSvc.New(s.Role, s.Permission, s.RolePermission, s.AccountRole, s.ShopRole),
Permission: permissionSvc.New(s.Permission, s.AccountRole, s.RolePermission, account, deps.Redis),
PersonalCustomer: personalCustomerSvc.NewService(s.PersonalCustomer, s.PersonalCustomerPhone, deps.Logger),
@@ -198,10 +235,10 @@ func initServices(s *stores, deps *Dependencies) *services {
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),
IotCard: iotCard,
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient),
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient, assetAudit),
ExportTask: exportTaskSvc.New(deps.DB, s.ExportTask, deps.QueueClient, deps.StorageService),
Device: device,
DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient),
DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient, assetAudit),
AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account),
Carrier: carrierSvc.New(s.Carrier),
PackageSeries: packageSeriesSvc.New(s.PackageSeries, s.ShopSeriesAllocation, s.Package),
@@ -223,7 +260,7 @@ func initServices(s *stores, deps *Dependencies) *services {
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, deps.Logger),
Asset: assetSvc.New(deps.DB, s.Device, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.DeviceSimBinding, s.Shop, deps.Redis, iotCard, deps.GatewayClient, s.AssetIdentifier, s.Order, s.OrderItem, s.ExchangeOrder),
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device),
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, assetAudit),
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
StopResumeService: stopResumeService,
WechatConfig: wechatConfig,

View File

@@ -7,6 +7,7 @@ import (
type stores struct {
Account *postgres.AccountStore
AccountOperationLog *postgres.AccountOperationLogStore
AssetOperationLog *postgres.AssetOperationLogStore
Shop *postgres.ShopStore
Role *postgres.RoleStore
Permission *postgres.PermissionStore
@@ -75,6 +76,7 @@ func initStores(deps *Dependencies) *stores {
return &stores{
Account: postgres.NewAccountStore(deps.DB, deps.Redis),
AccountOperationLog: postgres.NewAccountOperationLogStore(deps.DB),
AssetOperationLog: postgres.NewAssetOperationLogStore(deps.DB),
Shop: postgres.NewShopStore(deps.DB, deps.Redis),
Role: postgres.NewRoleStore(deps.DB),
Permission: postgres.NewPermissionStore(deps.DB),

View File

@@ -1,6 +1,7 @@
package bootstrap
import (
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
@@ -21,6 +22,7 @@ type workerServices struct {
}
func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *queue.WorkerServices {
assetAudit := assetAuditSvc.NewService(stores.AssetOperationLog)
commissionStatsService := commission_stats.New(stores.ShopSeriesCommissionStats)
commissionCalculationService := commission_calculation.New(
@@ -108,7 +110,15 @@ func initWorkerServices(stores *queue.WorkerStores, deps *WorkerDependencies) *q
)
// 创建停复机服务并注入回调:流量耗尽自动停机、套餐激活/重置/支付后自动复机
stopResumeService := iotCardSvc.NewStopResumeService(deps.Redis, stores.IotCard, stores.PackageUsage, stores.DeviceSimBinding, deps.GatewayClient, deps.Logger)
stopResumeService := iotCardSvc.NewStopResumeService(
deps.Redis,
stores.IotCard,
stores.PackageUsage,
stores.DeviceSimBinding,
deps.GatewayClient,
deps.Logger,
assetAudit,
)
usageService.SetStopResumeCallback(stopResumeService)
activationService.SetResumeCallback(stopResumeService)
orderService.SetResumeCallback(stopResumeService)

View File

@@ -6,6 +6,7 @@ import (
)
type workerStores struct {
AssetOperationLog *postgres.AssetOperationLogStore
IotCardImportTask *postgres.IotCardImportTaskStore
IotCard *postgres.IotCardStore
DeviceImportTask *postgres.DeviceImportTaskStore
@@ -38,6 +39,7 @@ type workerStores struct {
func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
stores := &workerStores{
AssetOperationLog: postgres.NewAssetOperationLogStore(deps.DB),
IotCardImportTask: postgres.NewIotCardImportTaskStore(deps.DB, deps.Redis),
IotCard: postgres.NewIotCardStore(deps.DB, deps.Redis),
DeviceImportTask: postgres.NewDeviceImportTaskStore(deps.DB, deps.Redis),
@@ -69,6 +71,7 @@ func initWorkerStores(deps *WorkerDependencies) *queue.WorkerStores {
}
return &queue.WorkerStores{
AssetOperationLog: stores.AssetOperationLog,
IotCardImportTask: stores.IotCardImportTask,
IotCard: stores.IotCard,
DeviceImportTask: stores.DeviceImportTask,

View File

@@ -2,11 +2,13 @@ package admin
import (
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
dto "github.com/break/junhong_cmp_fiber/internal/model/dto"
assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
deviceService "github.com/break/junhong_cmp_fiber/internal/service/device"
iotCardService "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
@@ -20,6 +22,7 @@ import (
// 提供统一的资产解析、实时状态、套餐查询、停复机、停用、轮询管控等接口
type AssetHandler struct {
assetService *assetService.Service
assetAuditService *assetAuditSvc.Service
deviceService *deviceService.Service
iotCardService *iotCardService.Service
iotCardStopResume *iotCardService.StopResumeService
@@ -30,6 +33,7 @@ type AssetHandler struct {
// NewAssetHandler 创建资产管理处理器
func NewAssetHandler(
assetSvc *assetService.Service,
assetAuditService *assetAuditSvc.Service,
deviceSvc *deviceService.Service,
iotCardSvc *iotCardService.Service,
iotCardStopResume *iotCardService.StopResumeService,
@@ -37,6 +41,7 @@ func NewAssetHandler(
) *AssetHandler {
return &AssetHandler{
assetService: assetSvc,
assetAuditService: assetAuditService,
deviceService: deviceSvc,
iotCardService: iotCardSvc,
iotCardStopResume: iotCardStopResume,
@@ -258,6 +263,48 @@ func (h *AssetHandler) Orders(c *fiber.Ctx) error {
return response.Success(c, result)
}
// OperationLogs 查询资产操作审计日志
// GET /api/admin/assets/:identifier/operation-logs
func (h *AssetHandler) OperationLogs(c *fiber.Ctx) error {
if h.assetAuditService == nil {
return errors.New(errors.CodeInternalError, "资产审计服务未配置")
}
asset, err := h.resolveByIdentifier(c)
if err != nil {
return err
}
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 20)
if page <= 0 || pageSize <= 0 || pageSize > 100 {
return errors.New(errors.CodeInvalidParam, "分页参数无效")
}
operationType := strings.TrimSpace(c.Query("operation_type"))
resultStatus := strings.TrimSpace(c.Query("result_status"))
if resultStatus != "" &&
resultStatus != constants.AssetAuditResultSuccess &&
resultStatus != constants.AssetAuditResultFailed &&
resultStatus != constants.AssetAuditResultDenied {
return errors.New(errors.CodeInvalidParam, "无效的结果状态")
}
result, err := h.assetAuditService.ListByAsset(c.UserContext(), assetAuditSvc.ListByAssetParams{
AssetType: asset.AssetType,
AssetID: asset.AssetID,
Page: page,
PageSize: pageSize,
OperationType: operationType,
ResultStatus: resultStatus,
})
if err != nil {
return err
}
return response.Success(c, result)
}
// UpdatePollingStatus 更新资产轮询状态
// PATCH /api/admin/assets/:identifier/polling-status
func (h *AssetHandler) UpdatePollingStatus(c *fiber.Ctx) error {

View File

@@ -0,0 +1,43 @@
package model
import "time"
// AssetOperationLog 资产操作审计日志模型。
// 记录资产域(卡/设备)敏感写操作,不建立外键约束,不使用 GORM 关联标签。
type AssetOperationLog struct {
ID uint `gorm:"column:id;primaryKey;comment:主键ID" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;not null;comment:创建时间" json:"created_at"`
OperatorID *uint `gorm:"column:operator_id;type:bigint;index:idx_asset_log_operator_created,priority:1;comment:操作人ID系统触发为空" json:"operator_id,omitempty"`
OperatorType string `gorm:"column:operator_type;type:varchar(32);not null;comment:操作人类型(system/admin_user/agent_user/enterprise_user/personal_customer)" json:"operator_type"`
OperatorName string `gorm:"column:operator_name;type:varchar(255);not null;default:'';comment:操作人名称快照" json:"operator_name"`
AssetType string `gorm:"column:asset_type;type:varchar(32);not null;index:idx_asset_log_asset_created,priority:1;comment:资产类型(iot_card/device)" json:"asset_type"`
AssetID uint `gorm:"column:asset_id;type:bigint;not null;index:idx_asset_log_asset_created,priority:2;comment:资产ID" json:"asset_id"`
AssetIdentifier string `gorm:"column:asset_identifier;type:varchar(128);not null;default:'';index:idx_asset_log_identifier_created,priority:1;comment:资产标识(ICCID/虚拟号)" json:"asset_identifier"`
OperationType string `gorm:"column:operation_type;type:varchar(64);not null;index:idx_asset_log_operation_created,priority:1;comment:操作类型" json:"operation_type"`
OperationDesc string `gorm:"column:operation_desc;type:text;not null;comment:操作描述(中文)" json:"operation_desc"`
BeforeData JSONB `gorm:"column:before_data;type:jsonb;comment:变更前数据" json:"before_data,omitempty"`
AfterData JSONB `gorm:"column:after_data;type:jsonb;comment:变更后数据" json:"after_data,omitempty"`
RequestID *string `gorm:"column:request_id;type:varchar(255);comment:请求ID" json:"request_id,omitempty"`
IPAddress *string `gorm:"column:ip_address;type:varchar(64);comment:请求IP" json:"ip_address,omitempty"`
UserAgent *string `gorm:"column:user_agent;type:text;comment:用户代理" json:"user_agent,omitempty"`
RequestPath *string `gorm:"column:request_path;type:varchar(255);comment:请求路径" json:"request_path,omitempty"`
RequestMethod *string `gorm:"column:request_method;type:varchar(16);comment:请求方法" json:"request_method,omitempty"`
ResultStatus string `gorm:"column:result_status;type:varchar(16);not null;index:idx_asset_log_result_created,priority:1;comment:执行结果(success/failed/denied)" json:"result_status"`
ErrorCode *string `gorm:"column:error_code;type:varchar(64);comment:错误码" json:"error_code,omitempty"`
ErrorMsg *string `gorm:"column:error_msg;type:text;comment:错误摘要" json:"error_msg,omitempty"`
BatchTotal int `gorm:"column:batch_total;type:int;not null;default:0;comment:批量总数" json:"batch_total"`
SuccessCount int `gorm:"column:success_count;type:int;not null;default:0;comment:批量成功数" json:"success_count"`
FailCount int `gorm:"column:fail_count;type:int;not null;default:0;comment:批量失败数" json:"fail_count"`
}
// TableName 指定资产操作日志表名。
func (AssetOperationLog) TableName() string {
return "tb_asset_operation_log"
}

View File

@@ -0,0 +1,54 @@
package dto
import "time"
// AssetOperationLogListRequest 资产操作日志查询请求
type AssetOperationLogListRequest struct {
Identifier string `path:"identifier" description:"资产标识符(虚拟号/ICCID/IMEI/SN/MSISDN" required:"true"`
Page int `query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码默认1"`
PageSize int `query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页条数默认20最大100"`
OperationType string `query:"operation_type" description:"操作类型(如 device_stop、card_allocate"`
ResultStatus string `query:"result_status" description:"执行结果 (success:成功, failed:失败, denied:拒绝)"`
}
// AssetOperationLogItem 资产操作日志条目
type AssetOperationLogItem struct {
ID uint `json:"id" description:"日志ID"`
CreatedAt time.Time `json:"created_at" description:"操作时间"`
OperatorID *uint `json:"operator_id,omitempty" description:"操作人ID系统触发为空"`
OperatorType string `json:"operator_type" description:"操作人类型(system/admin_user/agent_user/enterprise_user/personal_customer)"`
OperatorName string `json:"operator_name" description:"操作人名称快照"`
AssetType string `json:"asset_type" description:"资产类型(iot_card/device)"`
AssetID uint `json:"asset_id" description:"资产ID"`
AssetIdentifier string `json:"asset_identifier" description:"资产标识(ICCID/虚拟号)"`
OperationType string `json:"operation_type" description:"操作类型"`
OperationDesc string `json:"operation_desc" description:"操作描述(中文)"`
BeforeData map[string]any `json:"before_data,omitempty" description:"变更前数据"`
AfterData map[string]any `json:"after_data,omitempty" description:"变更后数据"`
RequestID *string `json:"request_id,omitempty" description:"请求ID"`
IPAddress *string `json:"ip_address,omitempty" description:"请求IP"`
UserAgent *string `json:"user_agent,omitempty" description:"用户代理"`
RequestPath *string `json:"request_path,omitempty" description:"请求路径"`
RequestMethod *string `json:"request_method,omitempty" description:"请求方法"`
ResultStatus string `json:"result_status" description:"执行结果 (success:成功, failed:失败, denied:拒绝)"`
ErrorCode *string `json:"error_code,omitempty" description:"错误码"`
ErrorMsg *string `json:"error_msg,omitempty" description:"错误摘要"`
BatchTotal int `json:"batch_total" description:"批量总数"`
SuccessCount int `json:"success_count" description:"批量成功数"`
FailCount int `json:"fail_count" description:"批量失败数"`
}
// AssetOperationLogListResponse 资产操作日志分页响应
type AssetOperationLogListResponse struct {
Total int64 `json:"total" description:"总条数"`
Page int `json:"page" description:"当前页码"`
PageSize int `json:"page_size" description:"每页条数"`
Items []*AssetOperationLogItem `json:"items" description:"日志列表"`
}

View File

@@ -110,6 +110,15 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Auth: true,
})
Register(assets, doc, groupPath, "GET", "/:identifier/operation-logs", handler.OperationLogs, RouteSpec{
Summary: "资产操作审计日志",
Description: "通过资产标识符查询审计日志,支持分页和操作类型/结果状态筛选。",
Tags: []string{"资产管理"},
Input: new(dto.AssetOperationLogListRequest),
Output: new(dto.AssetOperationLogListResponse),
Auth: true,
})
Register(assets, doc, groupPath, "PATCH", "/:identifier/polling-status", handler.UpdatePollingStatus, RouteSpec{
Summary: "更新资产轮询状态",
Description: "启用或禁用指定资产IoT卡或设备的定期状态轮询。",

View File

@@ -5,6 +5,7 @@ import (
stderrors "errors"
"github.com/break/junhong_cmp_fiber/internal/model"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"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"
@@ -15,44 +16,145 @@ var deactivatableAssetStatuses = []int{constants.AssetStatusInStock, constants.A
// LifecycleService 资产生命周期服务
type LifecycleService struct {
db *gorm.DB
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
db *gorm.DB
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
assetAuditService assetAuditSvc.OperationLogger
}
// NewLifecycleService 创建资产生命周期服务
func NewLifecycleService(db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore) *LifecycleService {
func NewLifecycleService(
db *gorm.DB,
iotCardStore *postgres.IotCardStore,
deviceStore *postgres.DeviceStore,
assetAuditService assetAuditSvc.OperationLogger,
) *LifecycleService {
return &LifecycleService{
db: db,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
db: db,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
assetAuditService: assetAuditService,
}
}
func (s *LifecycleService) logLifecycleAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAuditService == nil {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.OperationType == "" {
p.OperationType = constants.AssetAuditOpAssetDeactivate
}
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
// DeactivateIotCard 手动停用 IoT 卡
func (s *LifecycleService) DeactivateIotCard(ctx context.Context, id uint) error {
card, err := s.iotCardStore.GetByID(ctx, id)
if err != nil {
if stderrors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(errors.CodeIotCardNotFound)
appErr := errors.New(errors.CodeIotCardNotFound)
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: id,
OperationDesc: "统一入口停用IoT卡失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
})
return appErr
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: id,
OperationDesc: "统一入口停用IoT卡失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
})
return appErr
}
beforeData := map[string]any{
"asset_status": card.AssetStatus,
"iccid": card.ICCID,
"virtual_no": card.VirtualNo,
}
if !canDeactivateAsset(card.AssetStatus) {
return errors.New(errors.CodeForbidden, "当前状态不允许停用")
appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
OperationDesc: "统一入口停用IoT卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
})
return appErr
}
result := s.db.WithContext(ctx).Model(&model.IotCard{}).
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
Update("asset_status", constants.AssetStatusDeactivated)
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败")
appErr := errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
OperationDesc: "统一入口停用IoT卡失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
AfterData: map[string]any{
"asset_status": constants.AssetStatusDeactivated,
"iccid": card.ICCID,
"virtual_no": card.VirtualNo,
},
})
return appErr
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
appErr := errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
OperationDesc: "统一入口停用IoT卡失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
})
return appErr
}
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
OperationDesc: "统一入口停用IoT卡",
ResultStatus: constants.AssetAuditResultSuccess,
BeforeData: beforeData,
AfterData: map[string]any{
"asset_status": constants.AssetStatusDeactivated,
"iccid": card.ICCID,
"virtual_no": card.VirtualNo,
},
})
return nil
}
@@ -61,25 +163,103 @@ func (s *LifecycleService) DeactivateDevice(ctx context.Context, id uint) error
device, err := s.deviceStore.GetByID(ctx, id)
if err != nil {
if stderrors.Is(err, gorm.ErrRecordNotFound) {
return errors.New(errors.CodeNotFound, "设备不存在")
appErr := errors.New(errors.CodeNotFound, "设备不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: id,
OperationDesc: "统一入口停用设备失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
})
return appErr
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: id,
OperationDesc: "统一入口停用设备失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
})
return appErr
}
beforeData := map[string]any{
"asset_status": device.AssetStatus,
"virtual_no": device.VirtualNo,
}
if !canDeactivateAsset(device.AssetStatus) {
return errors.New(errors.CodeForbidden, "当前状态不允许停用")
appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.VirtualNo,
OperationDesc: "统一入口停用设备被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
})
return appErr
}
result := s.db.WithContext(ctx).Model(&model.Device{}).
Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).
Update("asset_status", constants.AssetStatusDeactivated)
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败")
appErr := errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.VirtualNo,
OperationDesc: "统一入口停用设备失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
AfterData: map[string]any{
"asset_status": constants.AssetStatusDeactivated,
"virtual_no": device.VirtualNo,
},
})
return appErr
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
appErr := errors.New(errors.CodeConflict, "状态已变更,请刷新后重试")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(appErr)
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.VirtualNo,
OperationDesc: "统一入口停用设备失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
})
return appErr
}
s.logLifecycleAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.VirtualNo,
OperationDesc: "统一入口停用设备",
ResultStatus: constants.AssetAuditResultSuccess,
BeforeData: beforeData,
AfterData: map[string]any{
"asset_status": constants.AssetStatusDeactivated,
"virtual_no": device.VirtualNo,
},
})
return nil
}

View File

@@ -0,0 +1,269 @@
package asset_audit
import (
"context"
stderrors "errors"
"fmt"
"strconv"
"strings"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/bytedance/sonic"
)
// OperatorInfo 审计操作者信息。
type OperatorInfo struct {
ID *uint
Type string
Name string
}
// BuildLogParams 构建资产审计日志参数。
type BuildLogParams struct {
Operator OperatorInfo
AssetType string
AssetID uint
AssetIdentifier string
OperationType string
OperationDesc string
BeforeData map[string]any
AfterData map[string]any
ResultStatus string
ErrorCode string
ErrorMsg string
BatchTotal int
SuccessCount int
FailCount int
}
// BuildLog 统一构建资产审计日志。
func BuildLog(ctx context.Context, p BuildLogParams) *model.AssetOperationLog {
resultStatus := strings.TrimSpace(p.ResultStatus)
if resultStatus == "" {
resultStatus = constants.AssetAuditResultSuccess
}
operationDesc := strings.TrimSpace(p.OperationDesc)
if operationDesc == "" {
operationDesc = p.OperationType
}
log := &model.AssetOperationLog{
OperatorID: p.Operator.ID,
OperatorType: strings.TrimSpace(p.Operator.Type),
OperatorName: strings.TrimSpace(p.Operator.Name),
AssetType: NormalizeAssetType(p.AssetType),
AssetID: p.AssetID,
AssetIdentifier: strings.TrimSpace(p.AssetIdentifier),
OperationType: strings.TrimSpace(p.OperationType),
OperationDesc: operationDesc,
BeforeData: sanitizeAndTrimJSONB(p.BeforeData),
AfterData: sanitizeAndTrimJSONB(p.AfterData),
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
RequestPath: middleware.GetRequestPathFromContext(ctx),
RequestMethod: middleware.GetRequestMethodFromContext(ctx),
ResultStatus: resultStatus,
BatchTotal: nonNegative(p.BatchTotal),
SuccessCount: nonNegative(p.SuccessCount),
FailCount: nonNegative(p.FailCount),
}
if log.OperatorType == "" {
log.OperatorType = constants.AssetAuditOperatorTypeSystem
}
if log.OperatorName == "" {
log.OperatorName = "系统任务"
}
if code := strings.TrimSpace(p.ErrorCode); code != "" {
log.ErrorCode = strPtr(code)
}
if msg := strings.TrimSpace(p.ErrorMsg); msg != "" {
msg = limitString(msg, 1000)
log.ErrorMsg = strPtr(msg)
}
return log
}
// OperatorFromContext 从 context 解析操作者信息。
func OperatorFromContext(ctx context.Context) OperatorInfo {
uid := middleware.GetUserIDFromContext(ctx)
ut := middleware.GetUserTypeFromContext(ctx)
if uid == 0 {
return SystemOperator("系统任务")
}
op := OperatorInfo{ID: uintPtr(uid)}
switch ut {
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
op.Type = constants.AssetAuditOperatorTypeAdmin
op.Name = fmt.Sprintf("后台用户#%d", uid)
case constants.UserTypeAgent:
op.Type = constants.AssetAuditOperatorTypeAgent
op.Name = fmt.Sprintf("代理用户#%d", uid)
case constants.UserTypeEnterprise:
op.Type = constants.AssetAuditOperatorTypeEnterprise
op.Name = fmt.Sprintf("企业用户#%d", uid)
case constants.UserTypePersonalCustomer:
op.Type = constants.AssetAuditOperatorTypePersonal
op.Name = fmt.Sprintf("个人客户#%d", uid)
default:
op.Type = constants.AssetAuditOperatorTypeAdmin
op.Name = fmt.Sprintf("用户#%d", uid)
}
return op
}
// SystemOperator 构建系统操作者信息。
func SystemOperator(name string) OperatorInfo {
n := strings.TrimSpace(name)
if n == "" {
n = "系统任务"
}
return OperatorInfo{
ID: nil,
Type: constants.AssetAuditOperatorTypeSystem,
Name: n,
}
}
// BuildErrorInfo 从统一错误提取审计错误码与错误摘要。
func BuildErrorInfo(err error) (string, string) {
if err == nil {
return "", ""
}
var appErr *apperrors.AppError
if strings.TrimSpace(err.Error()) == "" {
return "", "未知错误"
}
if stderrors.As(err, &appErr) && appErr != nil {
return strconv.Itoa(appErr.Code), appErr.Message
}
return "", err.Error()
}
// NormalizeAssetType 统一资产类型值。
func NormalizeAssetType(assetType string) string {
switch strings.TrimSpace(assetType) {
case "card":
return constants.AssetTypeIotCard
case "device":
return constants.AssetTypeDevice
default:
return strings.TrimSpace(assetType)
}
}
func sanitizeAndTrimJSONB(data map[string]any) model.JSONB {
if len(data) == 0 {
return nil
}
sanitized := sanitizeMap(data)
raw, err := sonic.Marshal(sanitized)
if err != nil {
return model.JSONB{"_marshal_error": "序列化失败"}
}
if len(raw) <= constants.AssetAuditDataMaxBytes {
return model.JSONB(sanitized)
}
preview := string(raw)
if len(preview) > constants.AssetAuditPreviewMaxBytes {
preview = preview[:constants.AssetAuditPreviewMaxBytes]
}
return model.JSONB{
"_truncated": true,
"_original_bytes": len(raw),
"_preview": preview,
"_message": "数据超过大小限制,已裁剪",
}
}
func sanitizeMap(source map[string]any) map[string]any {
out := make(map[string]any, len(source))
for k, v := range source {
if isSensitiveKey(k) {
out[k] = "******"
continue
}
out[k] = sanitizeValue(v)
}
return out
}
func sanitizeValue(v any) any {
switch vv := v.(type) {
case map[string]any:
return sanitizeMap(vv)
case model.JSONB:
return sanitizeMap(map[string]any(vv))
case []map[string]any:
arr := make([]any, 0, len(vv))
for _, item := range vv {
arr = append(arr, sanitizeMap(item))
}
return arr
case []any:
arr := make([]any, 0, len(vv))
for _, item := range vv {
arr = append(arr, sanitizeValue(item))
}
return arr
case string:
return limitString(vv, 2000)
default:
return v
}
}
func isSensitiveKey(key string) bool {
lower := strings.ToLower(strings.TrimSpace(key))
for _, kw := range []string{"password", "passwd", "pwd", "secret", "token", "wifi_password", "wifipwd"} {
if strings.Contains(lower, kw) {
return true
}
}
return false
}
func nonNegative(v int) int {
if v < 0 {
return 0
}
return v
}
func limitString(value string, max int) string {
if max <= 0 || len(value) <= max {
return value
}
return value[:max]
}
func strPtr(v string) *string {
if strings.TrimSpace(v) == "" {
return nil
}
return &v
}
func uintPtr(v uint) *uint {
return &v
}

View File

@@ -0,0 +1,149 @@
// Package asset_audit 提供资产操作审计日志服务。
package asset_audit
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/logger"
"go.uber.org/zap"
)
// AssetOperationLogStore 资产操作日志存储接口。
type AssetOperationLogStore interface {
Create(ctx context.Context, log *model.AssetOperationLog) error
ListByAssetPaged(
ctx context.Context,
assetType string,
assetID uint,
page int,
pageSize int,
operationType string,
resultStatus string,
) ([]*model.AssetOperationLog, int64, error)
}
// OperationLogger 资产审计记录接口。
type OperationLogger interface {
LogOperation(ctx context.Context, log *model.AssetOperationLog)
}
// Service 资产审计服务。
type Service struct {
store AssetOperationLogStore
}
// ListByAssetParams 按资产查询日志参数。
type ListByAssetParams struct {
AssetType string
AssetID uint
Page int
PageSize int
OperationType string
ResultStatus string
}
// NewService 创建资产审计服务实例。
func NewService(store AssetOperationLogStore) *Service {
return &Service{store: store}
}
// LogOperation 记录资产操作日志(异步写入,不阻塞主流程)。
func (s *Service) LogOperation(ctx context.Context, log *model.AssetOperationLog) {
if s == nil || s.store == nil || log == nil {
return
}
go func() {
if err := s.store.Create(context.Background(), log); err != nil {
logger.GetAppLogger().Error("写入资产操作日志失败",
zap.String("asset_type", log.AssetType),
zap.Uint("asset_id", log.AssetID),
zap.String("operation_type", log.OperationType),
zap.String("result_status", log.ResultStatus),
zap.Error(err))
}
}()
}
// ListByAsset 按资产分页查询操作日志。
func (s *Service) ListByAsset(ctx context.Context, params ListByAssetParams) (*dto.AssetOperationLogListResponse, error) {
if s == nil || s.store == nil {
return &dto.AssetOperationLogListResponse{
Total: 0,
Page: 1,
PageSize: constants.DefaultPageSize,
Items: []*dto.AssetOperationLogItem{},
}, nil
}
page := params.Page
pageSize := params.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = constants.DefaultPageSize
}
if pageSize > 100 {
pageSize = 100
}
logs, total, err := s.store.ListByAssetPaged(
ctx,
NormalizeAssetType(params.AssetType),
params.AssetID,
page,
pageSize,
params.OperationType,
params.ResultStatus,
)
if err != nil {
return nil, err
}
items := make([]*dto.AssetOperationLogItem, 0, len(logs))
for _, item := range logs {
items = append(items, toAssetOperationLogItem(item))
}
return &dto.AssetOperationLogListResponse{
Total: total,
Page: page,
PageSize: pageSize,
Items: items,
}, nil
}
func toAssetOperationLogItem(log *model.AssetOperationLog) *dto.AssetOperationLogItem {
if log == nil {
return nil
}
return &dto.AssetOperationLogItem{
ID: log.ID,
CreatedAt: log.CreatedAt,
OperatorID: log.OperatorID,
OperatorType: log.OperatorType,
OperatorName: log.OperatorName,
AssetType: log.AssetType,
AssetID: log.AssetID,
AssetIdentifier: log.AssetIdentifier,
OperationType: log.OperationType,
OperationDesc: log.OperationDesc,
BeforeData: map[string]any(log.BeforeData),
AfterData: map[string]any(log.AfterData),
RequestID: log.RequestID,
IPAddress: log.IPAddress,
UserAgent: log.UserAgent,
RequestPath: log.RequestPath,
RequestMethod: log.RequestMethod,
ResultStatus: log.ResultStatus,
ErrorCode: log.ErrorCode,
ErrorMsg: log.ErrorMsg,
BatchTotal: log.BatchTotal,
SuccessCount: log.SuccessCount,
FailCount: log.FailCount,
}
}

View File

@@ -0,0 +1,88 @@
package device
import (
"context"
"strings"
"github.com/break/junhong_cmp_fiber/internal/model"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AssetAuditService 资产审计服务接口。
type AssetAuditService interface {
LogOperation(ctx context.Context, log *model.AssetOperationLog)
}
func (s *Service) logDeviceAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAuditService == nil {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.AssetType == "" {
p.AssetType = constants.AssetTypeDevice
}
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
func (s *Service) logDeviceOperation(
ctx context.Context,
operationType string,
operationDesc string,
resultStatus string,
device *model.Device,
beforeData map[string]any,
afterData map[string]any,
batchTotal int,
successCount int,
failCount int,
err error,
) {
if strings.TrimSpace(operationDesc) == "" {
operationDesc = operationType
}
if strings.TrimSpace(resultStatus) == "" {
if err != nil {
resultStatus = constants.AssetAuditResultFailed
} else {
resultStatus = constants.AssetAuditResultSuccess
}
}
params := assetAuditSvc.BuildLogParams{
OperationType: operationType,
OperationDesc: operationDesc,
ResultStatus: resultStatus,
BeforeData: beforeData,
AfterData: afterData,
BatchTotal: batchTotal,
SuccessCount: successCount,
FailCount: failCount,
}
if device != nil {
params.AssetID = device.ID
params.AssetIdentifier = device.VirtualNo
}
if err != nil {
params.ErrorCode, params.ErrorMsg = assetAuditSvc.BuildErrorInfo(err)
}
s.logDeviceAudit(ctx, params)
}
func deviceSnapshot(device *model.Device) map[string]any {
if device == nil {
return nil
}
return map[string]any{
"id": device.ID,
"virtual_no": device.VirtualNo,
"shop_id": device.ShopID,
"status": device.Status,
"series_id": device.SeriesID,
"enable_polling": device.EnablePolling,
"realname_policy": device.RealnamePolicy,
}
}

View File

@@ -77,37 +77,195 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
device, err := s.deviceStore.GetByID(ctx, deviceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
appErr := errors.New(errors.CodeNotFound, "设备不存在")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"device_id": deviceID,
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
appErr,
)
return nil, appErr
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"device_id": deviceID,
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
err,
)
return nil, err
}
if req.SlotPosition > device.MaxSimSlots {
return nil, errors.New(errors.CodeInvalidParam, "插槽位置超出设备最大插槽数")
appErr := errors.New(errors.CodeInvalidParam, "插槽位置超出设备最大插槽数")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡被拒绝",
constants.AssetAuditResultDenied,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
appErr,
)
return nil, appErr
}
existingBinding, err := s.deviceSimBindingStore.GetByDeviceAndSlot(ctx, device.ID, req.SlotPosition)
if err != nil && err != gorm.ErrRecordNotFound {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
err,
)
return nil, err
}
if existingBinding != nil {
return nil, errors.New(errors.CodeConflict, "该插槽已有绑定的卡")
appErr := errors.New(errors.CodeConflict, "该插槽已有绑定的卡")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡被拒绝",
constants.AssetAuditResultDenied,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
appErr,
)
return nil, appErr
}
card, err := s.iotCardStore.GetByID(ctx, req.IotCardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeIotCardNotFound)
appErr := errors.New(errors.CodeIotCardNotFound)
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
appErr,
)
return nil, appErr
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
err,
)
return nil, err
}
activeBinding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
if err != nil && err != gorm.ErrRecordNotFound {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
err,
)
return nil, err
}
if activeBinding != nil {
return nil, errors.New(errors.CodeIotCardBoundToDevice, "该卡已绑定到其他设备")
appErr := errors.New(errors.CodeIotCardBoundToDevice, "该卡已绑定到其他设备")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡被拒绝",
constants.AssetAuditResultDenied,
device,
map[string]any{
"device": deviceSnapshot(device),
"card": map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"status": card.Status,
},
},
map[string]any{
"iot_card_id": req.IotCardID,
"slot_position": req.SlotPosition,
},
0,
0,
0,
appErr,
)
return nil, appErr
}
binding := &model.DeviceSimBinding{
@@ -118,6 +276,28 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
}
if err := s.deviceSimBindingStore.Create(ctx, binding); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{
"device": deviceSnapshot(device),
"card": map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"status": card.Status,
},
},
map[string]any{
"slot_position": req.SlotPosition,
},
0,
0,
0,
err,
)
return nil, err
}
@@ -130,6 +310,32 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
)
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceBindCard,
"设备绑卡",
constants.AssetAuditResultSuccess,
device,
map[string]any{
"device": deviceSnapshot(device),
"card": map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"status": card.Status,
},
},
map[string]any{
"binding_id": binding.ID,
"slot_position": req.SlotPosition,
"iot_card_id": card.ID,
"iccid": card.ICCID,
},
1,
1,
0,
nil,
)
return &dto.BindCardToDeviceResponse{
BindingID: binding.ID,
Message: "绑定成功",
@@ -140,20 +346,97 @@ func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*
device, err := s.deviceStore.GetByID(ctx, deviceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "设备不存在")
appErr := errors.New(errors.CodeNotFound, "设备不存在")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceUnbindCard,
"设备解绑卡失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"device_id": deviceID,
"iot_card_id": cardID,
},
0,
0,
0,
appErr,
)
return nil, appErr
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceUnbindCard,
"设备解绑卡失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"device_id": deviceID,
"iot_card_id": cardID,
},
0,
0,
0,
err,
)
return nil, err
}
binding, err := s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "该卡未绑定到此设备")
appErr := errors.New(errors.CodeNotFound, "该卡未绑定到此设备")
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceUnbindCard,
"设备解绑卡被拒绝",
constants.AssetAuditResultDenied,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"iot_card_id": cardID},
0,
0,
0,
appErr,
)
return nil, appErr
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceUnbindCard,
"设备解绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"iot_card_id": cardID},
0,
0,
0,
err,
)
return nil, err
}
if err := s.deviceSimBindingStore.Unbind(ctx, binding.ID); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceUnbindCard,
"设备解绑卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{
"device": deviceSnapshot(device),
"binding_id": binding.ID,
"iot_card_id": binding.IotCardID,
},
nil,
0,
0,
0,
err,
)
return nil, err
}
@@ -166,6 +449,27 @@ func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*
)
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceUnbindCard,
"设备解绑卡",
constants.AssetAuditResultSuccess,
device,
map[string]any{
"device": deviceSnapshot(device),
"binding_id": binding.ID,
"iot_card_id": binding.IotCardID,
},
map[string]any{
"iot_card_id": cardID,
"unbind": true,
},
1,
1,
0,
nil,
)
return &dto.UnbindCardFromDeviceResponse{
Message: "解绑成功",
}, nil

View File

@@ -4,26 +4,28 @@ import (
"context"
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// getDeviceIMEI 通过标识符获取设备并验证 IMEI 存在
// getGatewayDevice 通过标识符获取设备并验证 IMEI 存在
// 提供统一的设备查找 + IMEI 校验逻辑,供所有 Gateway 代理方法复用
func (s *Service) getDeviceIMEI(ctx context.Context, identifier string) (string, error) {
func (s *Service) getGatewayDevice(ctx context.Context, identifier string) (*model.Device, string, error) {
device, err := s.deviceStore.GetByIdentifier(ctx, identifier)
if err != nil {
return "", errors.New(errors.CodeNotFound, "设备不存在或无权限访问")
return nil, "", errors.New(errors.CodeNotFound, "设备不存在或无权限访问")
}
if device.IMEI == "" {
return "", errors.New(errors.CodeInvalidParam, "该设备未配置 IMEI无法调用网关接口")
return nil, "", errors.New(errors.CodeInvalidParam, "该设备未配置 IMEI无法调用网关接口")
}
return device.IMEI, nil
return device, device.IMEI, nil
}
// GatewayGetDeviceInfo 通过标识符查询设备网关信息
func (s *Service) GatewayGetDeviceInfo(ctx context.Context, identifier string) (*gateway.DeviceInfoResp, error) {
imei, err := s.getDeviceIMEI(ctx, identifier)
_, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
return nil, err
}
@@ -34,7 +36,7 @@ func (s *Service) GatewayGetDeviceInfo(ctx context.Context, identifier string) (
// GatewayGetSlotInfo 通过标识符查询设备卡槽信息
func (s *Service) GatewayGetSlotInfo(ctx context.Context, identifier string) (*gateway.SlotInfoResp, error) {
imei, err := s.getDeviceIMEI(ctx, identifier)
_, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
return nil, err
}
@@ -45,73 +47,356 @@ func (s *Service) GatewayGetSlotInfo(ctx context.Context, identifier string) (*g
// GatewaySetSpeedLimit 通过标识符设置设备限速
func (s *Service) GatewaySetSpeedLimit(ctx context.Context, identifier string, req *dto.SetSpeedLimitRequest) error {
imei, err := s.getDeviceIMEI(ctx, identifier)
device, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSpeedLimit,
"设备限速失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"identifier": identifier,
"speed_limit": req.SpeedLimit,
},
0,
0,
0,
err,
)
return err
}
return s.gatewayClient.SetSpeedLimit(ctx, &gateway.SpeedLimitReq{
if err = s.gatewayClient.SetSpeedLimit(ctx, &gateway.SpeedLimitReq{
DeviceID: imei,
SpeedLimit: req.SpeedLimit,
})
}); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSpeedLimit,
"设备限速失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"speed_limit": req.SpeedLimit},
0,
0,
0,
err,
)
return err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSpeedLimit,
"设备限速",
constants.AssetAuditResultSuccess,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"speed_limit": req.SpeedLimit},
0,
0,
0,
nil,
)
return nil
}
// GatewaySetWiFi 通过标识符设置设备 WiFi
func (s *Service) GatewaySetWiFi(ctx context.Context, identifier string, req *dto.SetWiFiRequest) error {
imei, err := s.getDeviceIMEI(ctx, identifier)
device, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSetWiFi,
"设备设置WiFi失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"identifier": identifier,
"card_no": req.CardNo,
"ssid": req.SSID,
"enabled": req.Enabled,
"password": req.Password,
},
0,
0,
0,
err,
)
return err
}
return s.gatewayClient.SetWiFi(ctx, &gateway.WiFiReq{
if err = s.gatewayClient.SetWiFi(ctx, &gateway.WiFiReq{
CardNo: req.CardNo,
DeviceID: imei,
SSID: req.SSID,
Password: req.Password,
Enabled: req.Enabled,
})
}); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSetWiFi,
"设备设置WiFi失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"card_no": req.CardNo,
"ssid": req.SSID,
"enabled": req.Enabled,
"password": req.Password,
},
0,
0,
0,
err,
)
return err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSetWiFi,
"设备设置WiFi",
constants.AssetAuditResultSuccess,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{
"card_no": req.CardNo,
"ssid": req.SSID,
"enabled": req.Enabled,
"password": req.Password,
},
0,
0,
0,
nil,
)
return nil
}
// GatewaySwitchCard 通过标识符切换设备使用的卡
func (s *Service) GatewaySwitchCard(ctx context.Context, identifier string, req *dto.SwitchCardRequest) error {
imei, err := s.getDeviceIMEI(ctx, identifier)
device, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSwitchCard,
"设备切卡失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"identifier": identifier,
"target_iccid": req.TargetICCID,
},
0,
0,
0,
err,
)
return err
}
return s.gatewayClient.SwitchCard(ctx, &gateway.SwitchCardReq{
if err = s.gatewayClient.SwitchCard(ctx, &gateway.SwitchCardReq{
CardNo: imei,
ICCID: req.TargetICCID,
})
}); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSwitchCard,
"设备切卡失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"target_iccid": req.TargetICCID},
0,
0,
0,
err,
)
return err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSwitchCard,
"设备切卡",
constants.AssetAuditResultSuccess,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"target_iccid": req.TargetICCID},
0,
0,
0,
nil,
)
return nil
}
// GatewayRebootDevice 通过标识符重启设备
func (s *Service) GatewayRebootDevice(ctx context.Context, identifier string) error {
imei, err := s.getDeviceIMEI(ctx, identifier)
device, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceReboot,
"设备重启失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{"identifier": identifier},
0,
0,
0,
err,
)
return err
}
return s.gatewayClient.RebootDevice(ctx, &gateway.DeviceOperationReq{
if err = s.gatewayClient.RebootDevice(ctx, &gateway.DeviceOperationReq{
DeviceID: imei,
})
}); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceReboot,
"设备重启失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
nil,
0,
0,
0,
err,
)
return err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceReboot,
"设备重启",
constants.AssetAuditResultSuccess,
device,
map[string]any{"device": deviceSnapshot(device)},
nil,
0,
0,
0,
nil,
)
return nil
}
// GatewayResetDevice 通过标识符恢复设备出厂设置
func (s *Service) GatewayResetDevice(ctx context.Context, identifier string) error {
imei, err := s.getDeviceIMEI(ctx, identifier)
device, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceReset,
"设备恢复出厂失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{"identifier": identifier},
0,
0,
0,
err,
)
return err
}
return s.gatewayClient.ResetDevice(ctx, &gateway.DeviceOperationReq{
if err = s.gatewayClient.ResetDevice(ctx, &gateway.DeviceOperationReq{
DeviceID: imei,
})
}); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceReset,
"设备恢复出厂失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
nil,
0,
0,
0,
err,
)
return err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceReset,
"设备恢复出厂",
constants.AssetAuditResultSuccess,
device,
map[string]any{"device": deviceSnapshot(device)},
nil,
0,
0,
0,
nil,
)
return nil
}
// GatewaySwitchMode 通过标识符设置设备切卡模式
func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req *dto.SetSwitchModeRequest) error {
imei, err := s.getDeviceIMEI(ctx, identifier)
device, imei, err := s.getGatewayDevice(ctx, identifier)
if err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSwitchMode,
"设备切卡模式切换失败",
constants.AssetAuditResultFailed,
nil,
nil,
map[string]any{
"identifier": identifier,
"switch_mode": req.SwitchMode,
},
0,
0,
0,
err,
)
return err
}
return s.gatewayClient.SwitchMode(ctx, &gateway.SwitchModeReq{
if err = s.gatewayClient.SwitchMode(ctx, &gateway.SwitchModeReq{
CardNo: imei,
SwitchMode: req.SwitchMode,
})
}); err != nil {
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSwitchMode,
"设备切卡模式切换失败",
constants.AssetAuditResultFailed,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"switch_mode": req.SwitchMode},
0,
0,
0,
err,
)
return err
}
s.logDeviceOperation(
ctx,
constants.AssetAuditOpDeviceSwitchMode,
"设备切卡模式切换",
constants.AssetAuditResultSuccess,
device,
map[string]any{"device": deviceSnapshot(device)},
map[string]any{"switch_mode": req.SwitchMode},
0,
0,
0,
nil,
)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
package device_import
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AssetAuditService 资产审计服务接口。
type AssetAuditService interface {
LogOperation(ctx context.Context, log *model.AssetOperationLog)
}
func (s *Service) logDeviceImportAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAudit == nil {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.OperationType == "" {
p.OperationType = constants.AssetAuditOpDeviceImportTaskCreate
}
if p.AssetType == "" {
p.AssetType = constants.AssetTypeDevice
}
s.assetAudit.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
func newDeviceImportAuditParams(
taskID uint,
taskNo string,
req *dto.ImportDeviceRequest,
resultStatus string,
err error,
) assetAuditSvc.BuildLogParams {
afterData := map[string]any{}
if req != nil {
afterData["batch_no"] = req.BatchNo
afterData["file_key"] = req.FileKey
afterData["realname_policy"] = req.RealnamePolicy
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
return assetAuditSvc.BuildLogParams{
AssetID: taskID,
AssetIdentifier: taskNo,
OperationDesc: "创建设备导入任务",
ResultStatus: resultStatus,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: afterData,
}
}

View File

@@ -20,24 +20,33 @@ type Service struct {
db *gorm.DB
importTaskStore *postgres.DeviceImportTaskStore
queueClient *queue.Client
assetAudit AssetAuditService
}
type DeviceImportPayload struct {
TaskID uint `json:"task_id"`
}
func New(db *gorm.DB, importTaskStore *postgres.DeviceImportTaskStore, queueClient *queue.Client) *Service {
func New(
db *gorm.DB,
importTaskStore *postgres.DeviceImportTaskStore,
queueClient *queue.Client,
assetAudit AssetAuditService,
) *Service {
return &Service{
db: db,
importTaskStore: importTaskStore,
queueClient: queueClient,
assetAudit: assetAudit,
}
}
func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceRequest) (*dto.ImportDeviceResponse, error) {
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr))
return nil, appErr
}
taskNo := s.importTaskStore.GenerateTaskNo(ctx)
@@ -55,16 +64,22 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceReq
task.Updater = userID
if err := s.importTaskStore.Create(ctx, task); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建导入任务失败")
appErr := errors.Wrap(errors.CodeInternalError, err, "创建导入任务失败")
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(0, taskNo, req, constants.AssetAuditResultFailed, appErr))
return nil, appErr
}
payload := DeviceImportPayload{TaskID: task.ID}
err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeDeviceImport, payload)
if err != nil {
s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败: "+err.Error())
return nil, errors.Wrap(errors.CodeInternalError, err, "任务入队失败")
appErr := errors.Wrap(errors.CodeInternalError, err, "任务入队失败")
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultFailed, appErr))
return nil, appErr
}
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultSuccess, nil))
return &dto.ImportDeviceResponse{
TaskID: task.ID,
TaskNo: taskNo,

View File

@@ -0,0 +1,58 @@
package iot_card
import (
"context"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AssetAuditService 资产审计服务接口。
type AssetAuditService interface {
LogOperation(ctx context.Context, log *model.AssetOperationLog)
}
func (s *Service) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAuditService == nil {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.AssetType == "" {
p.AssetType = constants.AssetTypeIotCard
}
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
func (s *StopResumeService) logCardAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAuditService == nil {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.AssetType == "" {
p.AssetType = constants.AssetTypeIotCard
}
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
func cardSnapshot(card *model.IotCard) map[string]any {
if card == nil {
return nil
}
return map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"shop_id": card.ShopID,
"status": card.Status,
"series_id": card.SeriesID,
"enable_polling": card.EnablePolling,
"realname_policy": card.RealnamePolicy,
"real_name_status": card.RealNameStatus,
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -64,6 +65,7 @@ type Service struct {
deviceSimBindingStore *postgres.DeviceSimBindingStore
redis *redis.Client
assetIdentifierStore *postgres.AssetIdentifierStore
assetAuditService AssetAuditService
}
func New(
@@ -76,6 +78,7 @@ func New(
packageSeriesStore *postgres.PackageSeriesStore,
gatewayClient *gateway.Client,
logger *zap.Logger,
assetAuditService AssetAuditService,
) *Service {
return &Service{
db: db,
@@ -87,6 +90,7 @@ func New(
packageSeriesStore: packageSeriesStore,
gatewayClient: gatewayClient,
logger: logger,
assetAuditService: assetAuditService,
}
}
@@ -341,11 +345,37 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandaloneCardsRequest, operatorID uint, operatorShopID *uint) (*dto.AllocateStandaloneCardsResponse, error) {
if err := s.validateDirectSubordinate(ctx, operatorShopID, req.ToShopID); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardAllocate,
OperationDesc: "单卡分配被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: map[string]any{
"to_shop_id": req.ToShopID,
"selection_type": req.SelectionType,
"requested_count": len(req.ICCIDs),
},
})
return nil, err
}
cards, err := s.getCardsForAllocation(ctx, req, operatorShopID)
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardAllocate,
OperationDesc: "单卡分配执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: map[string]any{
"to_shop_id": req.ToShopID,
"selection_type": req.SelectionType,
"requested_count": len(req.ICCIDs),
},
})
return nil, err
}
@@ -401,6 +431,19 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
}
if len(cardIDs) == 0 {
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardAllocate,
OperationDesc: "单卡分配被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorMsg: "无可分配卡",
BatchTotal: len(cards),
FailCount: len(failedItems),
AfterData: map[string]any{
"to_shop_id": req.ToShopID,
"failed_items": failedItems,
"selection_type": req.SelectionType,
},
})
return &dto.AllocateStandaloneCardsResponse{
TotalCount: len(cards),
SuccessCount: 0,
@@ -426,6 +469,21 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
})
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardAllocate,
OperationDesc: "单卡分配执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(cards),
SuccessCount: len(cardIDs),
FailCount: len(failedItems),
AfterData: map[string]any{
"to_shop_id": req.ToShopID,
"failed_items": failedItems,
},
})
return nil, err
}
@@ -436,6 +494,32 @@ func (s *Service) AllocateCards(ctx context.Context, req *dto.AllocateStandalone
}
}
beforeData := make([]map[string]any, 0, len(cards))
for _, card := range cards {
beforeData = append(beforeData, map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"shop_id": card.ShopID,
"status": card.Status,
})
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardAllocate,
OperationDesc: "单卡分配",
ResultStatus: constants.AssetAuditResultSuccess,
BatchTotal: len(cards),
SuccessCount: len(cardIDs),
FailCount: len(failedItems),
BeforeData: map[string]any{
"cards": beforeData,
},
AfterData: map[string]any{
"to_shop_id": req.ToShopID,
"new_status": newStatus,
"failed_items": failedItems,
},
})
return &dto.AllocateStandaloneCardsResponse{
TotalCount: len(cards),
SuccessCount: len(cardIDs),
@@ -449,6 +533,18 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
// 1. 查询卡列表
cards, err := s.getCardsForRecall(ctx, req)
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRecall,
OperationDesc: "单卡回收执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: map[string]any{
"selection_type": req.SelectionType,
"requested_count": len(req.ICCIDs),
},
})
return nil, err
}
@@ -535,6 +631,18 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
}
if len(cardIDs) == 0 {
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRecall,
OperationDesc: "单卡回收被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorMsg: "无可回收卡",
BatchTotal: len(cards),
FailCount: len(failedItems),
AfterData: map[string]any{
"failed_items": failedItems,
"selection_type": req.SelectionType,
},
})
return &dto.RecallStandaloneCardsResponse{
TotalCount: len(cards),
SuccessCount: 0,
@@ -570,6 +678,20 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
})
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRecall,
OperationDesc: "单卡回收执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(cards),
SuccessCount: len(cardIDs),
FailCount: len(failedItems),
AfterData: map[string]any{
"failed_items": failedItems,
},
})
return nil, err
}
@@ -580,6 +702,32 @@ func (s *Service) RecallCards(ctx context.Context, req *dto.RecallStandaloneCard
}
}
beforeData := make([]map[string]any, 0, len(successCards))
for _, card := range successCards {
beforeData = append(beforeData, map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"shop_id": card.ShopID,
"status": card.Status,
})
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRecall,
OperationDesc: "单卡回收",
ResultStatus: constants.AssetAuditResultSuccess,
BatchTotal: len(cards),
SuccessCount: len(cardIDs),
FailCount: len(failedItems),
BeforeData: map[string]any{
"cards": beforeData,
},
AfterData: map[string]any{
"to_shop_id": newShopID,
"new_status": newStatus,
"failed_items": failedItems,
},
})
return &dto.RecallStandaloneCardsResponse{
TotalCount: len(cards),
SuccessCount: len(cardIDs),
@@ -750,10 +898,35 @@ func (s *Service) buildRecallRecords(successCards []*model.IotCard, toShopID *ui
func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCardSeriesBindngRequest, operatorShopID *uint) (*dto.BatchSetCardSeriesBindngResponse, error) {
cards, err := s.iotCardStore.GetByICCIDs(ctx, req.ICCIDs)
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
})
return nil, err
}
if len(cards) == 0 {
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorMsg: "卡不存在",
BatchTotal: len(req.ICCIDs),
FailCount: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
})
return &dto.BatchSetCardSeriesBindngResponse{
SuccessCount: 0,
FailCount: len(req.ICCIDs),
@@ -772,12 +945,53 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
packageSeries, err = s.packageSeriesStore.GetByID(ctx, req.SeriesID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
denyErr := errors.New(errors.CodeNotFound, "套餐系列不存在或已禁用")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
})
return nil, denyErr
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
})
return nil, err
}
if packageSeries.Status != 1 {
return nil, errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
denyErr := errors.New(errors.CodeInvalidParam, "套餐系列不存在或已禁用")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
AfterData: map[string]any{
"series_id": req.SeriesID,
"iccids": req.ICCIDs,
},
})
return nil, denyErr
}
}
@@ -836,10 +1050,67 @@ func (s *Service) BatchSetSeriesBinding(ctx context.Context, req *dto.BatchSetCa
seriesIDPtr = &req.SeriesID
}
if err := s.iotCardStore.BatchUpdateSeriesID(ctx, successCardIDs, seriesIDPtr); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: "卡系列绑定执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
SuccessCount: len(successCardIDs),
FailCount: len(failedItems),
AfterData: map[string]any{
"series_id": req.SeriesID,
"success_ids": successCardIDs,
"failed_items": failedItems,
},
})
return nil, err
}
}
resultStatus := constants.AssetAuditResultSuccess
operationDesc := "批量设置卡系列绑定"
errorMsg := ""
if len(successCardIDs) == 0 && len(failedItems) > 0 {
resultStatus = constants.AssetAuditResultDenied
operationDesc = "卡系列绑定被拒绝"
errorMsg = "无可操作卡"
}
beforeCards := make([]map[string]any, 0, len(successCardIDs))
successSet := make(map[uint]struct{}, len(successCardIDs))
for _, id := range successCardIDs {
successSet[id] = struct{}{}
}
for _, card := range cards {
if _, ok := successSet[card.ID]; !ok {
continue
}
beforeCards = append(beforeCards, map[string]any{
"id": card.ID,
"iccid": card.ICCID,
"series_id": card.SeriesID,
})
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardSeriesBinding,
OperationDesc: operationDesc,
ResultStatus: resultStatus,
ErrorMsg: errorMsg,
BatchTotal: len(req.ICCIDs),
SuccessCount: len(successCardIDs),
FailCount: len(failedItems),
BeforeData: map[string]any{
"cards": beforeCards,
},
AfterData: map[string]any{
"series_id": req.SeriesID,
"success_ids": successCardIDs,
"failed_items": failedItems,
},
})
return &dto.BatchSetCardSeriesBindngResponse{
SuccessCount: len(successCardIDs),
FailCount: len(failedItems),
@@ -1052,19 +1323,74 @@ func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePo
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "IoT卡不存在")
denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "更新卡轮询状态被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AssetIdentifier: "",
AfterData: map[string]any{
"enable_polling": enablePolling,
},
})
return denyErr
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "更新卡轮询状态执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AfterData: map[string]any{
"enable_polling": enablePolling,
},
})
return err
}
// 检查是否需要更新
if card.EnablePolling == enablePolling {
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "更新卡轮询状态",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"enable_polling": card.EnablePolling,
},
AfterData: map[string]any{
"enable_polling": enablePolling,
},
})
return nil // 状态未变化
}
// 更新数据库
card.EnablePolling = enablePolling
if err := s.iotCardStore.Update(ctx, card); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "更新卡轮询状态执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"enable_polling": !enablePolling,
},
AfterData: map[string]any{
"enable_polling": enablePolling,
},
})
return err
}
@@ -1082,6 +1408,20 @@ func (s *Service) UpdatePollingStatus(ctx context.Context, cardID uint, enablePo
}
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "更新卡轮询状态",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"enable_polling": !enablePolling,
},
AfterData: map[string]any{
"enable_polling": enablePolling,
},
})
return nil
}
@@ -1093,6 +1433,21 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint,
// 批量更新数据库
if err := s.iotCardStore.BatchUpdatePollingStatus(ctx, cardIDs, enablePolling); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "批量更新卡轮询状态执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(cardIDs),
FailCount: len(cardIDs),
AfterData: map[string]any{
"card_ids": cardIDs,
"enable_polling": enablePolling,
"trigger_source": "batch",
},
})
return err
}
@@ -1112,6 +1467,19 @@ func (s *Service) BatchUpdatePollingStatus(ctx context.Context, cardIDs []uint,
}
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardPollingStatus,
OperationDesc: "批量更新卡轮询状态",
ResultStatus: constants.AssetAuditResultSuccess,
BatchTotal: len(cardIDs),
SuccessCount: len(cardIDs),
AfterData: map[string]any{
"card_ids": cardIDs,
"enable_polling": enablePolling,
"trigger_source": "batch",
},
})
return nil
}
@@ -1120,12 +1488,42 @@ func (s *Service) DeleteCard(ctx context.Context, cardID uint) error {
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "IoT卡不存在")
denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardDelete,
OperationDesc: "删除卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
})
return denyErr
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardDelete,
OperationDesc: "删除卡执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
})
return err
}
if err := s.iotCardStore.Delete(ctx, cardID); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardDelete,
OperationDesc: "删除卡执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return err
}
@@ -1139,6 +1537,18 @@ func (s *Service) DeleteCard(ctx context.Context, cardID uint) error {
s.pollingCallback.OnCardDeleted(ctx, cardID)
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardDelete,
OperationDesc: "删除卡",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"deleted": true,
},
})
return nil
}
@@ -1147,9 +1557,39 @@ func (s *Service) BatchDeleteCards(ctx context.Context, cardIDs []uint) error {
if len(cardIDs) == 0 {
return nil
}
cards, queryErr := s.iotCardStore.GetByIDs(ctx, cardIDs)
if queryErr != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(queryErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardBatchDelete,
OperationDesc: "批量删除卡执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(cardIDs),
FailCount: len(cardIDs),
AfterData: map[string]any{
"card_ids": cardIDs,
},
})
return queryErr
}
// 批量软删除
if err := s.iotCardStore.BatchDelete(ctx, cardIDs); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardBatchDelete,
OperationDesc: "批量删除卡执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BatchTotal: len(cardIDs),
FailCount: len(cardIDs),
AfterData: map[string]any{
"card_ids": cardIDs,
},
})
return err
}
@@ -1161,6 +1601,24 @@ func (s *Service) BatchDeleteCards(ctx context.Context, cardIDs []uint) error {
s.pollingCallback.OnCardDeleted(ctx, cardID)
}
}
beforeCards := make([]map[string]any, 0, len(cards))
for _, card := range cards {
beforeCards = append(beforeCards, cardSnapshot(card))
}
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardBatchDelete,
OperationDesc: "批量删除卡",
ResultStatus: constants.AssetAuditResultSuccess,
BatchTotal: len(cardIDs),
SuccessCount: len(cardIDs),
BeforeData: map[string]any{
"cards": beforeCards,
},
AfterData: map[string]any{
"card_ids": cardIDs,
"deleted": true,
},
})
return nil
}
@@ -1171,19 +1629,75 @@ func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnam
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "IoT卡不存在")
denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnamePolicy,
OperationDesc: "更新卡实名认证策略被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AfterData: map[string]any{
"realname_policy": realnamePolicy,
},
})
return denyErr
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnamePolicy,
OperationDesc: "更新卡实名认证策略执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AfterData: map[string]any{
"realname_policy": realnamePolicy,
},
})
return wrapErr
}
// 幂等检查
if card.RealnamePolicy == realnamePolicy {
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnamePolicy,
OperationDesc: "更新卡实名认证策略",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"realname_policy": card.RealnamePolicy,
},
AfterData: map[string]any{
"realname_policy": realnamePolicy,
},
})
return nil
}
// 更新数据库
if err := s.iotCardStore.UpdateRealnamePolicy(ctx, cardID, realnamePolicy); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新实名认证策略失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnamePolicy,
OperationDesc: "更新卡实名认证策略执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"realname_policy": card.RealnamePolicy,
},
AfterData: map[string]any{
"realname_policy": realnamePolicy,
},
})
return wrapErr
}
s.logger.Info("更新卡实名认证策略",
@@ -1191,6 +1705,20 @@ func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnam
zap.String("realname_policy", realnamePolicy),
)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnamePolicy,
OperationDesc: "更新卡实名认证策略",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"realname_policy": card.RealnamePolicy,
},
AfterData: map[string]any{
"realname_policy": realnamePolicy,
},
})
return nil
}
@@ -1199,15 +1727,54 @@ func (s *Service) UpdateRealnamePolicy(ctx context.Context, cardID uint, realnam
func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, realNameStatus int) (*model.IotCard, error) {
if realNameStatus != constants.RealNameStatusNotVerified &&
realNameStatus != constants.RealNameStatusVerified {
return nil, errors.New(errors.CodeInvalidParam, "无效的实名状态")
denyErr := errors.New(errors.CodeInvalidParam, "无效的实名状态")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnameStatus,
OperationDesc: "手动更新卡实名状态被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AfterData: map[string]any{
"real_name_status": realNameStatus,
},
})
return nil, denyErr
}
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "IoT卡不存在")
denyErr := errors.New(errors.CodeNotFound, "IoT卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnameStatus,
OperationDesc: "手动更新卡实名状态被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AfterData: map[string]any{
"real_name_status": realNameStatus,
},
})
return nil, denyErr
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnameStatus,
OperationDesc: "手动更新卡实名状态执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
AfterData: map[string]any{
"real_name_status": realNameStatus,
},
})
return nil, wrapErr
}
oldStatus := card.RealNameStatus
@@ -1227,12 +1794,46 @@ func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, r
}
if err := s.iotCardStore.UpdateFields(ctx, cardID, fields); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新卡实名状态失败")
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡实名状态失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnameStatus,
OperationDesc: "手动更新卡实名状态执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"real_name_status": oldStatus,
},
AfterData: map[string]any{
"real_name_status": realNameStatus,
},
})
return nil, wrapErr
}
freshCard, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询更新后的IoT卡失败")
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "查询更新后的IoT卡失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnameStatus,
OperationDesc: "手动更新卡实名状态执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"real_name_status": oldStatus,
},
AfterData: map[string]any{
"real_name_status": realNameStatus,
},
})
return nil, wrapErr
}
if statusChanged {
@@ -1249,6 +1850,22 @@ func (s *Service) ManualUpdateRealnameStatus(ctx context.Context, cardID uint, r
zap.Int("new_status", realNameStatus),
zap.Uint("operator_id", middleware.GetUserIDFromContext(ctx)))
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardRealnameStatus,
OperationDesc: "手动更新卡实名状态",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: freshCard.ID,
AssetIdentifier: freshCard.ICCID,
BeforeData: map[string]any{
"real_name_status": oldStatus,
"first_realname_at": card.FirstRealnameAt,
},
AfterData: map[string]any{
"real_name_status": freshCard.RealNameStatus,
"first_realname_at": freshCard.FirstRealnameAt,
},
})
return freshCard, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/gateway"
"github.com/break/junhong_cmp_fiber/internal/model"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"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"
@@ -36,6 +37,7 @@ type StopResumeService struct {
deviceSimBindingStore *postgres.DeviceSimBindingStore
gatewayClient *gateway.Client
logger *zap.Logger
assetAuditService AssetAuditService
maxRetries int
retryInterval time.Duration
@@ -49,6 +51,7 @@ func NewStopResumeService(
deviceSimBindingStore *postgres.DeviceSimBindingStore,
gatewayClient *gateway.Client,
logger *zap.Logger,
assetAuditService AssetAuditService,
) *StopResumeService {
return &StopResumeService{
redis: redis,
@@ -57,6 +60,7 @@ func NewStopResumeService(
deviceSimBindingStore: deviceSimBindingStore,
gatewayClient: gatewayClient,
logger: logger,
assetAuditService: assetAuditService,
maxRetries: 3,
retryInterval: 2 * time.Second,
}
@@ -363,6 +367,16 @@ func (s *StopResumeService) ResumeCardIfStopped(ctx context.Context, carrierType
func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) error {
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: cardID,
})
return err
}
@@ -407,6 +421,21 @@ func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) e
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID),
zap.Error(err))
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
})
return err
}
@@ -420,6 +449,25 @@ func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) e
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID),
zap.Error(err))
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
return nil
}
@@ -429,13 +477,52 @@ func (s *StopResumeService) resumeSingleCard(ctx context.Context, cardID uint) e
zap.Uint("card_id", cardID),
zap.String("iccid", card.ICCID))
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: assetAuditSvc.SystemOperator("系统任务"),
OperationType: constants.AssetAuditOpCardAutoStart,
OperationDesc: "自动复机",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
return nil
}
// stopCardWithRetry 调用运营商停机接口(带重试机制),并更新 DB 停机原因
func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.IotCard, stopReason string) error {
operator := assetAuditSvc.SystemOperator("系统任务")
operationType := constants.AssetAuditOpCardAutoStop
operationDesc := "自动停卡"
if stopReason == constants.StopReasonManual {
operator = assetAuditSvc.OperatorFromContext(ctx)
operationType = constants.AssetAuditOpCardManualStop
operationDesc = "手动停卡"
}
if s.gatewayClient == nil {
return errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
failErr := errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(failErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc + "执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return failErr
}
s.logger.Info("调用网关停机",
@@ -470,9 +557,44 @@ func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.I
zap.Uint("card_id", card.ID),
zap.String("iccid", card.ICCID),
zap.Error(updateErr))
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(updateErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc + "执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": stopReason,
},
})
}
s.invalidatePollingCache(ctx, card.ID)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc,
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": stopReason,
},
})
return nil
}
@@ -483,6 +605,26 @@ func (s *StopResumeService) stopCardWithRetry(ctx context.Context, card *model.I
zap.Error(err))
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(lastErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
Operator: operator,
OperationType: operationType,
OperationDesc: operationDesc + "执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOffline,
"stop_reason": stopReason,
},
})
return lastErr
}
@@ -529,11 +671,33 @@ func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model
func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) error {
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
if err != nil {
return errors.New(errors.CodeNotFound, "卡不存在")
denyErr := errors.New(errors.CodeNotFound, "卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetIdentifier: iccid,
})
return denyErr
}
if card.RealNameStatus != constants.RealNameStatusVerified {
return errors.New(errors.CodeForbidden, "卡未实名,无法操作")
denyErr := errors.New(errors.CodeForbidden, "卡未实名,无法操作")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return denyErr
}
// 检查绑定设备是否在复机保护期
@@ -542,10 +706,37 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er
if bindErr == nil && binding != nil {
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "start")).Result()
if exists > 0 {
return errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机")
denyErr := errors.New(errors.CodeForbidden, "设备复机保护期内,禁止停机")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"device_id": binding.DeviceID,
},
})
return denyErr
}
} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
return errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStop,
OperationDesc: "手动停卡执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return wrapErr
}
}
@@ -560,11 +751,33 @@ func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) er
func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) error {
card, err := s.iotCardStore.GetByICCID(ctx, iccid)
if err != nil {
return errors.New(errors.CodeNotFound, "卡不存在")
denyErr := errors.New(errors.CodeNotFound, "卡不存在")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetIdentifier: iccid,
})
return denyErr
}
if card.RealNameStatus != constants.RealNameStatusVerified {
return errors.New(errors.CodeForbidden, "卡未实名,无法操作")
denyErr := errors.New(errors.CodeForbidden, "卡未实名,无法操作")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return denyErr
}
// 检查绑定设备是否在停机保护期
@@ -573,15 +786,54 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e
if bindErr == nil && binding != nil {
exists, _ := s.redis.Exists(ctx, constants.RedisDeviceProtectKey(binding.DeviceID, "stop")).Result()
if exists > 0 {
return errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机")
denyErr := errors.New(errors.CodeForbidden, "设备停机保护期内,禁止复机")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(denyErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
AfterData: map[string]any{
"device_id": binding.DeviceID,
},
})
return denyErr
}
} else if bindErr != nil && !stderrors.Is(bindErr, gorm.ErrRecordNotFound) {
return errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
wrapErr := errors.Wrap(errors.CodeInternalError, bindErr, "查询卡绑定关系失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return wrapErr
}
}
if err := s.resumeCardWithRetry(ctx, card); err != nil {
return errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试")
wrapErr := errors.Wrap(errors.CodeGatewayError, err, "调用运营商复机失败,请稍后重试")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: cardSnapshot(card),
})
return wrapErr
}
now := time.Now()
@@ -590,11 +842,46 @@ func (s *StopResumeService) ManualStartCard(ctx context.Context, iccid string) e
"resumed_at": now,
"stop_reason": "",
}); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
wrapErr := errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(wrapErr)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机执行失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
return wrapErr
}
s.invalidatePollingCache(ctx, card.ID)
s.logCardAudit(ctx, assetAuditSvc.BuildLogParams{
OperationType: constants.AssetAuditOpCardManualStart,
OperationDesc: "手动复机",
ResultStatus: constants.AssetAuditResultSuccess,
AssetID: card.ID,
AssetIdentifier: card.ICCID,
BeforeData: map[string]any{
"network_status": card.NetworkStatus,
"stop_reason": card.StopReason,
},
AfterData: map[string]any{
"network_status": constants.NetworkStatusOnline,
"stop_reason": "",
},
})
return nil
}

View File

@@ -0,0 +1,58 @@
package iot_card_import
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AssetAuditService 资产审计服务接口。
type AssetAuditService interface {
LogOperation(ctx context.Context, log *model.AssetOperationLog)
}
func (s *Service) logIotCardImportAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAudit == nil {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.OperationType == "" {
p.OperationType = constants.AssetAuditOpIotCardImportTaskCreate
}
if p.AssetType == "" {
p.AssetType = constants.AssetTypeIotCard
}
s.assetAudit.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
func newIotCardImportAuditParams(
taskID uint,
taskNo string,
req *dto.ImportIotCardRequest,
resultStatus string,
err error,
) assetAuditSvc.BuildLogParams {
afterData := map[string]any{}
if req != nil {
afterData["carrier_id"] = req.CarrierID
afterData["batch_no"] = req.BatchNo
afterData["file_key"] = req.FileKey
afterData["card_category"] = req.CardCategory
afterData["realname_policy"] = req.RealnamePolicy
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
return assetAuditSvc.BuildLogParams{
AssetID: taskID,
AssetIdentifier: taskNo,
OperationDesc: "创建IoT卡导入任务",
ResultStatus: resultStatus,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: afterData,
}
}

View File

@@ -21,6 +21,7 @@ type Service struct {
importTaskStore *postgres.IotCardImportTaskStore
carrierStore carrierGetter
queueClient *queue.Client
assetAudit AssetAuditService
}
type carrierGetter interface {
@@ -43,12 +44,18 @@ func (s *CarrierStore) GetByID(ctx context.Context, id uint) (*model.Carrier, er
return &carrier, nil
}
func New(db *gorm.DB, importTaskStore *postgres.IotCardImportTaskStore, queueClient *queue.Client) *Service {
func New(
db *gorm.DB,
importTaskStore *postgres.IotCardImportTaskStore,
queueClient *queue.Client,
assetAudit AssetAuditService,
) *Service {
return &Service{
db: db,
importTaskStore: importTaskStore,
carrierStore: NewCarrierStore(db),
queueClient: queueClient,
assetAudit: assetAudit,
}
}
@@ -59,12 +66,16 @@ type IotCardImportPayload struct {
func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportIotCardRequest) (*dto.ImportIotCardResponse, error) {
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
s.logIotCardImportAudit(ctx, newIotCardImportAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr))
return nil, appErr
}
carrier, err := s.carrierStore.GetByID(ctx, req.CarrierID)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "运营商不存在")
appErr := errors.New(errors.CodeInvalidParam, "运营商不存在")
s.logIotCardImportAudit(ctx, newIotCardImportAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr))
return nil, appErr
}
taskNo := s.importTaskStore.GenerateTaskNo(ctx)
@@ -91,16 +102,22 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportIotCardRe
task.Updater = userID
if err := s.importTaskStore.Create(ctx, task); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建导入任务失败")
appErr := errors.Wrap(errors.CodeInternalError, err, "创建导入任务失败")
s.logIotCardImportAudit(ctx, newIotCardImportAuditParams(0, taskNo, req, constants.AssetAuditResultFailed, appErr))
return nil, appErr
}
payload := IotCardImportPayload{TaskID: task.ID}
err = s.queueClient.EnqueueTask(ctx, constants.TaskTypeIotCardImport, payload)
if err != nil {
s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败: "+err.Error())
return nil, errors.Wrap(errors.CodeInternalError, err, "任务入队失败")
appErr := errors.Wrap(errors.CodeInternalError, err, "任务入队失败")
s.logIotCardImportAudit(ctx, newIotCardImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultFailed, appErr))
return nil, appErr
}
s.logIotCardImportAudit(ctx, newIotCardImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultSuccess, nil))
return &dto.ImportIotCardResponse{
TaskID: task.ID,
TaskNo: taskNo,

View File

@@ -6,6 +6,7 @@ import (
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/polling"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -21,6 +22,7 @@ type AssetPollingService struct {
iotCardService *iotCardSvc.Service
queueMgr *polling.PollingQueueManager
logger *zap.Logger
assetAuditService assetAuditSvc.OperationLogger
}
// NewAssetPollingService 创建资产轮询管控服务
@@ -30,6 +32,7 @@ func NewAssetPollingService(
iotCardService *iotCardSvc.Service,
queueMgr *polling.PollingQueueManager,
logger *zap.Logger,
assetAuditService assetAuditSvc.OperationLogger,
) *AssetPollingService {
return &AssetPollingService{
deviceStore: deviceStore,
@@ -37,9 +40,23 @@ func NewAssetPollingService(
iotCardService: iotCardService,
queueMgr: queueMgr,
logger: logger,
assetAuditService: assetAuditService,
}
}
func (s *AssetPollingService) logAssetPollingAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAuditService == nil {
return
}
if p.OperationType == "" {
p.OperationType = constants.AssetAuditOpAssetPollingStatus
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
s.assetAuditService.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
// UpdatePollingStatus 更新资产轮询状态
// assetType: "card" 或 "device"
// assetID: 资产ID
@@ -47,12 +64,87 @@ func NewAssetPollingService(
func (s *AssetPollingService) UpdatePollingStatus(ctx context.Context, assetType string, assetID uint, enablePolling bool) error {
switch assetType {
case constants.AssetTypeIotCard:
beforeData := map[string]any{
"asset_type": constants.AssetTypeIotCard,
"asset_id": assetID,
"enable_polling": "unknown",
"source_service": "asset_polling",
}
afterData := map[string]any{
"asset_type": constants.AssetTypeIotCard,
"asset_id": assetID,
"enable_polling": enablePolling,
}
// S2 修复:委托给 IotCardService确保 DB 写入 + PollingCallback 回调一并触发
return s.iotCardService.UpdatePollingStatus(ctx, assetID, enablePolling)
if err := s.iotCardService.UpdatePollingStatus(ctx, assetID, enablePolling); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: assetID,
OperationDesc: "统一入口更新轮询状态失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
AfterData: afterData,
})
return err
}
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeIotCard,
AssetID: assetID,
OperationDesc: "统一入口更新轮询状态",
ResultStatus: constants.AssetAuditResultSuccess,
BeforeData: beforeData,
AfterData: afterData,
})
return nil
case constants.AssetTypeDevice:
device, getErr := s.deviceStore.GetByID(ctx, assetID)
if getErr != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(getErr)
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: assetID,
OperationDesc: "统一入口更新轮询状态失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: map[string]any{
"asset_type": constants.AssetTypeDevice,
"asset_id": assetID,
"enable_polling": enablePolling,
},
})
return getErr
}
beforeData := map[string]any{
"asset_type": constants.AssetTypeDevice,
"asset_id": device.ID,
"asset_identifier": device.VirtualNo,
"enable_polling": device.EnablePolling,
}
afterData := map[string]any{
"asset_type": constants.AssetTypeDevice,
"asset_id": device.ID,
"asset_identifier": device.VirtualNo,
"enable_polling": enablePolling,
}
// 1. 更新设备的 enable_polling 字段
if err := s.deviceStore.UpdatePollingStatus(ctx, assetID, enablePolling); err != nil {
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.VirtualNo,
OperationDesc: "统一入口更新轮询状态失败",
ResultStatus: constants.AssetAuditResultFailed,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
BeforeData: beforeData,
AfterData: afterData,
})
return err
}
bindings, err := s.deviceBindingStore.ListByDeviceID(ctx, assetID)
@@ -81,9 +173,33 @@ func (s *AssetPollingService) UpdatePollingStatus(ctx context.Context, assetType
}
}
}
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: constants.AssetTypeDevice,
AssetID: device.ID,
AssetIdentifier: device.VirtualNo,
OperationDesc: "统一入口更新轮询状态",
ResultStatus: constants.AssetAuditResultSuccess,
BeforeData: beforeData,
AfterData: afterData,
})
return nil
default:
return errors.New(errors.CodeInvalidParam, "资产类型无效,支持 card 或 device")
err := errors.New(errors.CodeInvalidParam, "资产类型无效,支持 card 或 device")
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
s.logAssetPollingAudit(ctx, assetAuditSvc.BuildLogParams{
AssetType: assetType,
AssetID: assetID,
OperationDesc: "统一入口更新轮询状态被拒绝",
ResultStatus: constants.AssetAuditResultDenied,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: map[string]any{
"asset_type": assetType,
"asset_id": assetID,
"enable_polling": enablePolling,
},
})
return err
}
}

View File

@@ -0,0 +1,106 @@
package postgres
import (
"context"
"github.com/break/junhong_cmp_fiber/internal/model"
"gorm.io/gorm"
)
// AssetOperationLogStore 资产操作日志存储层。
type AssetOperationLogStore struct {
db *gorm.DB
}
// NewAssetOperationLogStore 创建资产操作日志存储实例。
func NewAssetOperationLogStore(db *gorm.DB) *AssetOperationLogStore {
return &AssetOperationLogStore{db: db}
}
// Create 创建资产操作日志记录。
func (s *AssetOperationLogStore) Create(ctx context.Context, log *model.AssetOperationLog) error {
return s.db.WithContext(ctx).Create(log).Error
}
// ListByAssetPaged 按资产分页查询日志。
func (s *AssetOperationLogStore) ListByAssetPaged(
ctx context.Context,
assetType string,
assetID uint,
page int,
pageSize int,
operationType string,
resultStatus string,
) ([]*model.AssetOperationLog, int64, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
query := s.db.WithContext(ctx).Model(&model.AssetOperationLog{}).
Where("asset_type = ? AND asset_id = ?", assetType, assetID)
if operationType != "" {
query = query.Where("operation_type = ?", operationType)
}
if resultStatus != "" {
query = query.Where("result_status = ?", resultStatus)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
var logs []*model.AssetOperationLog
if err := query.Order("created_at DESC, id DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}
// ListByAsset 按资产查询日志。
func (s *AssetOperationLogStore) ListByAsset(ctx context.Context, assetType string, assetID uint, limit int) ([]*model.AssetOperationLog, error) {
if limit <= 0 {
limit = 20
}
var logs []*model.AssetOperationLog
err := s.db.WithContext(ctx).
Where("asset_type = ? AND asset_id = ?", assetType, assetID).
Order("created_at DESC").
Limit(limit).
Find(&logs).Error
return logs, err
}
// ListByOperator 按操作人查询日志。
func (s *AssetOperationLogStore) ListByOperator(ctx context.Context, operatorID uint, limit int) ([]*model.AssetOperationLog, error) {
if limit <= 0 {
limit = 20
}
var logs []*model.AssetOperationLog
err := s.db.WithContext(ctx).
Where("operator_id = ?", operatorID).
Order("created_at DESC").
Limit(limit).
Find(&logs).Error
return logs, err
}
// ListByOperationType 按操作类型查询日志。
func (s *AssetOperationLogStore) ListByOperationType(ctx context.Context, operationType string, limit int) ([]*model.AssetOperationLog, error) {
if limit <= 0 {
limit = 20
}
var logs []*model.AssetOperationLog
err := s.db.WithContext(ctx).
Where("operation_type = ?", operationType).
Order("created_at DESC").
Limit(limit).
Find(&logs).Error
return logs, err
}

View File

@@ -0,0 +1,8 @@
-- 回滚资产操作审计日志表
DROP INDEX IF EXISTS idx_asset_log_asset_created;
DROP INDEX IF EXISTS idx_asset_log_identifier_created;
DROP INDEX IF EXISTS idx_asset_log_operator_created;
DROP INDEX IF EXISTS idx_asset_log_operation_created;
DROP INDEX IF EXISTS idx_asset_log_result_created;
DROP TABLE IF EXISTS tb_asset_operation_log;

View File

@@ -0,0 +1,69 @@
-- 创建资产操作审计日志表
CREATE TABLE IF NOT EXISTS tb_asset_operation_log (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
operator_id BIGINT,
operator_type VARCHAR(32) NOT NULL,
operator_name VARCHAR(255) NOT NULL DEFAULT '',
asset_type VARCHAR(32) NOT NULL,
asset_id BIGINT NOT NULL,
asset_identifier VARCHAR(128) NOT NULL DEFAULT '',
operation_type VARCHAR(64) NOT NULL,
operation_desc TEXT NOT NULL,
before_data JSONB,
after_data JSONB,
request_id VARCHAR(255),
ip_address VARCHAR(64),
user_agent TEXT,
request_path VARCHAR(255),
request_method VARCHAR(16),
result_status VARCHAR(16) NOT NULL,
error_code VARCHAR(64),
error_msg TEXT,
batch_total INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
fail_count INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_asset_log_asset_created
ON tb_asset_operation_log(asset_type, asset_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_asset_log_identifier_created
ON tb_asset_operation_log(asset_identifier, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_asset_log_operator_created
ON tb_asset_operation_log(operator_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_asset_log_operation_created
ON tb_asset_operation_log(operation_type, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_asset_log_result_created
ON tb_asset_operation_log(result_status, created_at DESC);
COMMENT ON TABLE tb_asset_operation_log IS '资产操作审计日志表';
COMMENT ON COLUMN tb_asset_operation_log.id IS '主键ID';
COMMENT ON COLUMN tb_asset_operation_log.created_at IS '创建时间';
COMMENT ON COLUMN tb_asset_operation_log.operator_id IS '操作人ID系统触发为空';
COMMENT ON COLUMN tb_asset_operation_log.operator_type IS '操作人类型(system/admin_user/agent_user/enterprise_user/personal_customer)';
COMMENT ON COLUMN tb_asset_operation_log.operator_name IS '操作人名称快照';
COMMENT ON COLUMN tb_asset_operation_log.asset_type IS '资产类型(iot_card/device)';
COMMENT ON COLUMN tb_asset_operation_log.asset_id IS '资产ID';
COMMENT ON COLUMN tb_asset_operation_log.asset_identifier IS '资产标识(ICCID/虚拟号)';
COMMENT ON COLUMN tb_asset_operation_log.operation_type IS '操作类型';
COMMENT ON COLUMN tb_asset_operation_log.operation_desc IS '操作描述(中文)';
COMMENT ON COLUMN tb_asset_operation_log.before_data IS '变更前数据JSONB';
COMMENT ON COLUMN tb_asset_operation_log.after_data IS '变更后数据JSONB';
COMMENT ON COLUMN tb_asset_operation_log.request_id IS '请求ID';
COMMENT ON COLUMN tb_asset_operation_log.ip_address IS '请求IP';
COMMENT ON COLUMN tb_asset_operation_log.user_agent IS '用户代理';
COMMENT ON COLUMN tb_asset_operation_log.request_path IS '请求路径';
COMMENT ON COLUMN tb_asset_operation_log.request_method IS '请求方法';
COMMENT ON COLUMN tb_asset_operation_log.result_status IS '执行结果(success/failed/denied)';
COMMENT ON COLUMN tb_asset_operation_log.error_code IS '错误码';
COMMENT ON COLUMN tb_asset_operation_log.error_msg IS '错误摘要';
COMMENT ON COLUMN tb_asset_operation_log.batch_total IS '批量总数';
COMMENT ON COLUMN tb_asset_operation_log.success_count IS '批量成功数';
COMMENT ON COLUMN tb_asset_operation_log.fail_count IS '批量失败数';

View File

@@ -0,0 +1,58 @@
package constants
// 资产审计日志结果状态常量
const (
AssetAuditResultSuccess = "success" // 成功
AssetAuditResultFailed = "failed" // 失败
AssetAuditResultDenied = "denied" // 拒绝
)
// 资产审计日志操作者类型常量
const (
AssetAuditOperatorTypeSystem = "system" // 系统任务
AssetAuditOperatorTypeAdmin = "admin_user" // 平台/超管用户
AssetAuditOperatorTypeAgent = "agent_user" // 代理用户
AssetAuditOperatorTypeEnterprise = "enterprise_user" // 企业用户
AssetAuditOperatorTypePersonal = "personal_customer" // 个人客户
)
// 资产审计日志操作类型常量
const (
AssetAuditOpCardAllocate = "card_allocate" // 单卡分配
AssetAuditOpCardRecall = "card_recall" // 单卡回收
AssetAuditOpCardSeriesBinding = "card_series_binding" // 卡系列绑定
AssetAuditOpCardPollingStatus = "card_polling_status" // 卡轮询状态更新
AssetAuditOpCardRealnamePolicy = "card_realname_policy" // 卡实名策略更新
AssetAuditOpCardRealnameStatus = "card_realname_status" // 卡实名状态更新
AssetAuditOpCardDelete = "card_delete" // 删除卡
AssetAuditOpCardBatchDelete = "card_batch_delete" // 批量删除卡
AssetAuditOpCardManualStop = "card_manual_stop" // 手动停卡
AssetAuditOpCardManualStart = "card_manual_start" // 手动复机
AssetAuditOpCardAutoStop = "card_auto_stop" // 自动停卡
AssetAuditOpCardAutoStart = "card_auto_start" // 自动复机
AssetAuditOpDeviceDelete = "device_delete" // 删除设备
AssetAuditOpDeviceAllocate = "device_allocate" // 设备分配
AssetAuditOpDeviceRecall = "device_recall" // 设备回收
AssetAuditOpDeviceSeriesBinding = "device_series_binding" // 设备系列绑定
AssetAuditOpDeviceBindCard = "device_bind_card" // 设备绑卡
AssetAuditOpDeviceUnbindCard = "device_unbind_card" // 设备解绑卡
AssetAuditOpDeviceStop = "device_stop" // 设备停机
AssetAuditOpDeviceStart = "device_start" // 设备复机
AssetAuditOpDeviceSpeedLimit = "device_speed_limit" // 设备限速
AssetAuditOpDeviceSetWiFi = "device_set_wifi" // 设备 WiFi 设置
AssetAuditOpDeviceSwitchCard = "device_switch_card" // 设备切卡
AssetAuditOpDeviceSwitchMode = "device_switch_mode" // 设备切卡模式
AssetAuditOpDeviceReboot = "device_reboot" // 设备重启
AssetAuditOpDeviceReset = "device_reset" // 设备恢复出厂
AssetAuditOpAssetDeactivate = "asset_deactivate" // 资产停用
AssetAuditOpAssetPollingStatus = "asset_polling_status" // 统一入口轮询状态更新
AssetAuditOpAssetRealnamePolicy = "asset_realname_policy" // 统一入口实名策略更新
AssetAuditOpDeviceImportTaskCreate = "device_import_task_create" // 设备导入任务创建
AssetAuditOpIotCardImportTaskCreate = "iot_card_import_task_create" // 卡导入任务创建
)
// 资产审计日志数据裁剪常量
const (
AssetAuditDataMaxBytes = 16 * 1024 // before/after 数据最大字节数
AssetAuditPreviewMaxBytes = 2048 // 裁剪预览最大字节数
)

View File

@@ -14,6 +14,8 @@ const (
ContextKeyUserInfo = "user_info" // 完整的用户信息
ContextKeyIP = "ip_address" // IP地址
ContextKeyUserAgent = "user_agent" // User-Agent
ContextKeyRequestPath = "request_path" // 请求路径
ContextKeyRequestMethod = "request_method" // 请求方法
ContextKeySubordinateShopIDs = "subordinate_shop_ids" // 下级店铺ID列表代理用户预计算
)

View File

@@ -1,6 +1,7 @@
package logger
import (
"context"
"time"
"github.com/break/junhong_cmp_fiber/pkg/constants"
@@ -35,6 +36,19 @@ func Middleware() fiber.Handler {
startTime := time.Now()
c.Locals(constants.ContextKeyStartTime, startTime)
// 注入请求上下文,供 Service 层审计日志复用
ctx := c.UserContext()
if rid := c.Locals(constants.ContextKeyRequestID); rid != nil {
if requestID, ok := rid.(string); ok && requestID != "" {
ctx = context.WithValue(ctx, constants.ContextKeyRequestID, requestID)
}
}
ctx = context.WithValue(ctx, constants.ContextKeyIP, c.IP())
ctx = context.WithValue(ctx, constants.ContextKeyUserAgent, c.Get("User-Agent"))
ctx = context.WithValue(ctx, constants.ContextKeyRequestPath, c.Path())
ctx = context.WithValue(ctx, constants.ContextKeyRequestMethod, c.Method())
c.SetUserContext(ctx)
// 获取请求 body在 c.Next() 之前读取)
requestBody := truncateBody(c.Body(), MaxBodyLogSize)

View File

@@ -132,6 +132,28 @@ func GetUserAgentFromContext(ctx context.Context) *string {
return nil
}
// GetRequestPathFromContext 从 context 中提取请求路径。
func GetRequestPathFromContext(ctx context.Context) *string {
if ctx == nil {
return nil
}
if requestPath, ok := ctx.Value(constants.ContextKeyRequestPath).(string); ok {
return &requestPath
}
return nil
}
// GetRequestMethodFromContext 从 context 中提取请求方法。
func GetRequestMethodFromContext(ctx context.Context) *string {
if ctx == nil {
return nil
}
if requestMethod, ok := ctx.Value(constants.ContextKeyRequestMethod).(string); ok {
return &requestMethod
}
return nil
}
// SetUserToFiberContext 将用户信息设置到 Fiber context 的 Locals 中
// 同时也设置到标准 context 中,便于 GORM 查询使用
func SetUserToFiberContext(c *fiber.Ctx, info *UserContextInfo) {

View File

@@ -57,7 +57,7 @@ func BuildDocHandlers() *bootstrap.Handlers {
PollingAlert: admin.NewPollingAlertHandler(nil),
PollingCleanup: admin.NewPollingCleanupHandler(nil),
PollingManualTrigger: admin.NewPollingManualTriggerHandler(nil),
Asset: admin.NewAssetHandler(nil, nil, nil, nil, nil),
Asset: admin.NewAssetHandler(nil, nil, nil, nil, nil, nil),
AssetLifecycle: admin.NewAssetLifecycleHandler(nil),
AssetWallet: admin.NewAssetWalletHandler(nil),
WechatConfig: admin.NewWechatConfigHandler(nil),

View File

@@ -19,6 +19,7 @@ type OrderExpirer interface {
// WorkerStores Worker 侧所有 Store 的集合
type WorkerStores struct {
AssetOperationLog *postgres.AssetOperationLogStore
IotCardImportTask *postgres.IotCardImportTaskStore
IotCard *postgres.IotCardStore
DeviceImportTask *postgres.DeviceImportTaskStore