收口七月卡状态回调与系列授权兼容契约
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
完成运营商实名回调、业务事件观测序列与受控配置装配,同时恢复 UR43 已交付的 packages[].remove 字段及旧响应兼容,统一更新 OpenSpec、OpenAPI 和交付文档。 Constraint: 七月测试环境里程碑不新增或运行自动化测试 Rejected: 以必填 operation_type 替换 packages[].remove | 会破坏已交付前端契约 Confidence: high Scope-risk: broad Directive: 后续修改系列套餐管理接口必须保持 packages[].remove 和 ShopSeriesGrantResponse 兼容 Tested: go run ./cmd/gendocs;go build -buildvcs=false ./...;openspec validate complete-july-iteration-test-release --strict;git diff --check Not-tested: 按本 Change 约定未运行 go test,真实运营商与 Gateway 联调延期
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"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"
|
||||
pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
@@ -36,6 +38,12 @@ type ClientAssetHandler struct {
|
||||
deviceStore *postgres.DeviceStore
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
observationSeries cardObservationApp.BestEffortSeriesDispatcher
|
||||
}
|
||||
|
||||
// SetObservationSeriesDispatcher 注入读取型后台观测序列端口。
|
||||
func (h *ClientAssetHandler) SetObservationSeriesDispatcher(dispatcher cardObservationApp.BestEffortSeriesDispatcher) {
|
||||
h.observationSeries = dispatcher
|
||||
}
|
||||
|
||||
// NewClientAssetHandler 创建 C 端资产信息处理器
|
||||
@@ -213,10 +221,47 @@ func (h *ClientAssetHandler) GetAssetInfo(c *fiber.Ctx) error {
|
||||
resp.DeviceRealtime = mapDeviceGatewayInfoToClientInfo(realtimeResp.DeviceRealtime)
|
||||
}
|
||||
}
|
||||
h.dispatchAssetReadObservations(c.UserContext(), resolved.Asset)
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *ClientAssetHandler) dispatchAssetReadObservations(ctx context.Context, asset *dto.AssetResolveResponse) {
|
||||
if h.observationSeries == nil || asset == nil {
|
||||
return
|
||||
}
|
||||
cardIDs := make([]uint, 0, len(asset.Cards)+1)
|
||||
if asset.AssetType == "card" {
|
||||
cardIDs = append(cardIDs, asset.AssetID)
|
||||
} else {
|
||||
for _, card := range asset.Cards {
|
||||
cardIDs = append(cardIDs, card.CardID)
|
||||
}
|
||||
}
|
||||
dispatchCardReadSeries(ctx, h.observationSeries, constants.CardObservationSceneClientAssetRead, cardIDs)
|
||||
}
|
||||
|
||||
func dispatchCardReadSeries(ctx context.Context, dispatcher cardObservationApp.BestEffortSeriesDispatcher, scene string, cardIDs []uint) {
|
||||
requestID := ""
|
||||
if value := pkgMiddleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
for _, cardID := range cardIDs {
|
||||
resourceID := strconv.FormatUint(uint64(cardID), 10)
|
||||
for _, syncType := range []string{
|
||||
constants.CardObservationSyncTypeRealname,
|
||||
constants.CardObservationSyncTypeTraffic,
|
||||
constants.CardObservationSyncTypeNetwork,
|
||||
} {
|
||||
dispatcher.Dispatch(ctx, cardObservationApp.SeriesRequest{
|
||||
Scene: scene, ResourceType: constants.CardObservationResourceTypeCard,
|
||||
ResourceID: resourceID, SyncType: syncType, Source: constants.CardObservationSourceBusinessEvent,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAvailablePackages B2 资产可购套餐列表
|
||||
// GET /api/c/v1/asset/packages
|
||||
func (h *ClientAssetHandler) GetAvailablePackages(c *fiber.Ctx) error {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||
deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
@@ -33,9 +34,15 @@ type ClientDeviceHandler struct {
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
gatewayClient *gateway.Client
|
||||
deviceService *deviceSvc.Service
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// SetDeviceService 注入统一设备控制服务,确保各入口共享成功后的观测触发。
|
||||
func (h *ClientDeviceHandler) SetDeviceService(service *deviceSvc.Service) {
|
||||
h.deviceService = service
|
||||
}
|
||||
|
||||
// NewClientDeviceHandler 创建 C 端设备能力处理器
|
||||
func NewClientDeviceHandler(
|
||||
assetService *assetSvc.Service,
|
||||
@@ -195,10 +202,10 @@ func (h *ClientDeviceHandler) RebootDevice(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 重启设备
|
||||
if err := h.gatewayClient.RebootDevice(c.UserContext(), &gateway.DeviceOperationReq{
|
||||
DeviceID: info.IMEI,
|
||||
}); err != nil {
|
||||
if h.deviceService == nil {
|
||||
return errors.New(errors.CodeInternalError, "设备控制服务未配置")
|
||||
}
|
||||
if err := h.deviceService.GatewayRebootDevice(c.UserContext(), info.IMEI); err != nil {
|
||||
h.logger.Error("Gateway重启设备失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
zap.Error(err))
|
||||
@@ -225,10 +232,10 @@ func (h *ClientDeviceHandler) FactoryResetDevice(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 恢复出厂设置
|
||||
if err := h.gatewayClient.ResetDevice(c.UserContext(), &gateway.DeviceOperationReq{
|
||||
DeviceID: info.IMEI,
|
||||
}); err != nil {
|
||||
if h.deviceService == nil {
|
||||
return errors.New(errors.CodeInternalError, "设备控制服务未配置")
|
||||
}
|
||||
if err := h.deviceService.GatewayResetDevice(c.UserContext(), info.IMEI); err != nil {
|
||||
h.logger.Error("Gateway恢复出厂设置失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
zap.Error(err))
|
||||
@@ -256,14 +263,11 @@ func (h *ClientDeviceHandler) SetWiFi(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 配置 WiFi
|
||||
// CardNo 字段虽名为"卡号",但 Gateway 实际要求传入设备 IMEI
|
||||
if err := h.gatewayClient.SetWiFi(c.UserContext(), &gateway.WiFiReq{
|
||||
CardNo: info.IMEI,
|
||||
Params: gateway.WiFiParams{
|
||||
SSIDName: req.SSID,
|
||||
SSIDPassword: req.Password,
|
||||
},
|
||||
if h.deviceService == nil {
|
||||
return errors.New(errors.CodeInternalError, "设备控制服务未配置")
|
||||
}
|
||||
if err := h.deviceService.GatewaySetWiFi(c.UserContext(), info.IMEI, &dto.SetWiFiRequest{
|
||||
SSID: req.SSID, Password: req.Password, Enabled: req.Enabled,
|
||||
}); err != nil {
|
||||
h.logger.Error("Gateway配置WiFi失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
@@ -292,10 +296,11 @@ func (h *ClientDeviceHandler) SwitchCard(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用 Gateway 切卡,CardNo 传设备 IMEI
|
||||
if err := h.gatewayClient.SwitchCard(c.UserContext(), &gateway.SwitchCardReq{
|
||||
CardNo: info.IMEI,
|
||||
ICCID: req.TargetICCID,
|
||||
if h.deviceService == nil {
|
||||
return errors.New(errors.CodeInternalError, "设备控制服务未配置")
|
||||
}
|
||||
if err := h.deviceService.GatewaySwitchCard(c.UserContext(), info.IMEI, &dto.SwitchCardRequest{
|
||||
TargetICCID: req.TargetICCID,
|
||||
}); err != nil {
|
||||
h.logger.Error("Gateway切卡失败",
|
||||
zap.String("imei", info.IMEI),
|
||||
|
||||
@@ -2,9 +2,10 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||
pollingSvc "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
@@ -36,7 +36,7 @@ type ClientRealnameHandler struct {
|
||||
carrierStore *postgres.CarrierStore
|
||||
gatewayClient *gateway.Client
|
||||
logger *zap.Logger
|
||||
manualTriggerSvc *pollingSvc.ManualTriggerService // 手动触发服务(可为nil,nil时跳过自动触发)
|
||||
observationSeries cardObservationApp.BestEffortSeriesDispatcher
|
||||
}
|
||||
|
||||
// NewClientRealnameHandler 创建 C 端实名认证处理器
|
||||
@@ -48,7 +48,7 @@ func NewClientRealnameHandler(
|
||||
carrierStore *postgres.CarrierStore,
|
||||
gatewayClient *gateway.Client,
|
||||
logger *zap.Logger,
|
||||
manualTriggerSvc *pollingSvc.ManualTriggerService, // 可为nil
|
||||
_ *pollingSvc.ManualTriggerService, // 兼容旧构造签名;获取链接改由 0/3/5 观测序列收敛
|
||||
) *ClientRealnameHandler {
|
||||
return &ClientRealnameHandler{
|
||||
assetService: assetSvc,
|
||||
@@ -58,10 +58,14 @@ func NewClientRealnameHandler(
|
||||
carrierStore: carrierStore,
|
||||
gatewayClient: gatewayClient,
|
||||
logger: logger,
|
||||
manualTriggerSvc: manualTriggerSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// SetObservationSeriesDispatcher 注入获取实名链接后的观测序列端口。
|
||||
func (h *ClientRealnameHandler) SetObservationSeriesDispatcher(dispatcher cardObservationApp.BestEffortSeriesDispatcher) {
|
||||
h.observationSeries = dispatcher
|
||||
}
|
||||
|
||||
// GetRealnameLink E1 获取实名认证链接
|
||||
// GET /api/c/v1/realname/link
|
||||
func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
||||
@@ -138,15 +142,29 @@ func (h *ClientRealnameHandler) GetRealnameLink(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 异步触发实名检查,提升检测优先级;失败不影响主流程
|
||||
if h.manualTriggerSvc != nil && config.Get().PollingAutoTrigger.EnableAutoTrigger {
|
||||
systemUserID := uint(config.Get().PollingAutoTrigger.AutoTriggerSystemUserID)
|
||||
go h.triggerRealnameCheck(targetCard.ID, customerID, targetCard.ICCID, systemUserID)
|
||||
}
|
||||
h.dispatchRealnameObservation(ctx, targetCard.ID)
|
||||
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *ClientRealnameHandler) dispatchRealnameObservation(ctx context.Context, cardID uint) {
|
||||
if h.observationSeries == nil {
|
||||
return
|
||||
}
|
||||
requestID := ""
|
||||
if value := pkgMiddleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
h.observationSeries.Dispatch(ctx, cardObservationApp.SeriesRequest{
|
||||
Scene: constants.CardObservationSceneClientRealnameLink,
|
||||
ResourceType: constants.CardObservationResourceTypeCard,
|
||||
ResourceID: strconv.FormatUint(uint64(cardID), 10),
|
||||
SyncType: constants.CardObservationSyncTypeRealname,
|
||||
ExpectedValue: "verified", Source: constants.CardObservationSourceBusinessEvent,
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
}
|
||||
|
||||
// resolveTargetCard 根据资产类型和ICCID定位目标卡
|
||||
// 支持三条路径:直接卡资产、设备+指定ICCID、设备取第一张绑定卡
|
||||
func (h *ClientRealnameHandler) resolveTargetCard(c *fiber.Ctx, asset *dto.AssetResolveResponse, iccid string) (*model.IotCard, error) {
|
||||
@@ -275,33 +293,3 @@ func (h *ClientRealnameHandler) findFirstBoundCard(c *fiber.Ctx, deviceID uint)
|
||||
|
||||
return card, nil
|
||||
}
|
||||
|
||||
// triggerRealnameCheck 异步触发单卡实名检查
|
||||
// 在独立 goroutine 中调用,使用独立 context 避免 Fiber 请求 context 失效问题
|
||||
// 参数全部为值类型,不捕获请求相关指针
|
||||
func (h *ClientRealnameHandler) triggerRealnameCheck(cardID, customerID uint, iccid string, systemUserID uint) {
|
||||
// 必须使用独立 context,禁止复用 Fiber 请求 context(请求返回后即失效)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 使用平台用户身份构建 context,绕过卡归属权限检查
|
||||
// 注意:使用 UserTypePlatform 而非 UserTypeSuperAdmin,SuperAdmin 不受日限制约束
|
||||
sysCtx := pkgMiddleware.SetUserContext(ctx, &pkgMiddleware.UserContextInfo{
|
||||
UserID: systemUserID,
|
||||
UserType: constants.UserTypePlatform,
|
||||
})
|
||||
|
||||
err := h.manualTriggerSvc.TriggerSingle(sysCtx, cardID, constants.TaskTypePollingRealname, systemUserID)
|
||||
if err != nil {
|
||||
h.logger.Warn("自动触发实名检查失败",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("iccid", iccid),
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
h.logger.Info("自动触发实名检查成功",
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.String("iccid", iccid),
|
||||
zap.Uint("card_id", cardID))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user