From d597a25c69887b2c339076a077c4ef9c944c6961 Mon Sep 17 00:00:00 2001 From: huang Date: Thu, 14 May 2026 09:50:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=BA=E5=8D=A1=E5=88=86=E7=A6=BB=E5=A4=8D?= =?UTF-8?q?=E6=9C=BA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/bootstrap/services.go | 2 +- internal/handler/openapi/handler.go | 18 +++ internal/model/dto/agent_open_api_dto.go | 13 +++ internal/routes/open.go | 9 ++ internal/service/agent_open_api/service.go | 25 +++++ internal/service/iot_card/service.go | 8 +- .../service/iot_card/stop_resume_service.go | 106 +++++++++++++++++- internal/task/polling_cardstatus_handler.go | 9 +- pkg/constants/agent_open_api.go | 5 + pkg/constants/iot.go | 9 +- 10 files changed, 190 insertions(+), 14 deletions(-) diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 22322b9..867df44 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -191,7 +191,7 @@ func initServices(s *stores, deps *Dependencies) *services { packageService := packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation) orderService := orderSvc.New(deps.DB, deps.Redis, s.Order, s.OrderItem, s.AgentWallet, s.AssetWallet, s.Payment, purchaseValidation, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.IotCard, s.Device, s.PackageSeries, s.PackageUsage, s.Package, wechatConfig, deps.WechatPayment, paymentLoader, deps.QueueClient, deps.Logger, s.AssetIdentifier, s.PersonalCustomer, s.PersonalCustomerPhone) assetService := 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) - agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet) + agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet) return &services{ Account: account, diff --git a/internal/handler/openapi/handler.go b/internal/handler/openapi/handler.go index 759d275..afd72bb 100644 --- a/internal/handler/openapi/handler.go +++ b/internal/handler/openapi/handler.go @@ -78,6 +78,24 @@ func (h *Handler) GetRealnameStatus(c *fiber.Ctx) error { return response.Success(c, resp) } +// ResumeCard 机卡分离卡复机 +// POST /api/open/v1/cards/resume +func (h *Handler) ResumeCard(c *fiber.Ctx) error { + var req dto.AgentOpenAPICardResumeRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + if err := h.validate(&req); err != nil { + return err + } + + resp, err := h.service.ResumeCard(c.UserContext(), &req) + if err != nil { + return err + } + return response.Success(c, resp) +} + // ListPackages 查询代理可购买套餐列表 // GET /api/open/v1/packages func (h *Handler) ListPackages(c *fiber.Ctx) error { diff --git a/internal/model/dto/agent_open_api_dto.go b/internal/model/dto/agent_open_api_dto.go index d330713..2405927 100644 --- a/internal/model/dto/agent_open_api_dto.go +++ b/internal/model/dto/agent_open_api_dto.go @@ -7,6 +7,19 @@ type AgentOpenAPICardQueryRequest struct { CardNo string `json:"card_no" query:"card_no" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"卡标识(支持 ICCID、虚拟号、MSISDN)"` } +// AgentOpenAPICardResumeRequest 开放接口单卡复机请求 +type AgentOpenAPICardResumeRequest struct { + CardNo string `json:"card_no" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"卡标识(支持 ICCID、虚拟号、MSISDN)"` +} + +// AgentOpenAPICardResumeResponse 开放接口单卡复机响应 +type AgentOpenAPICardResumeResponse struct { + CardNo string `json:"card_no" description:"卡标识"` + CardStatus string `json:"card_status" description:"卡状态 (normal:正常, stopped:停机)"` + CardStatusName string `json:"card_status_name" description:"卡状态名称(中文)"` + GatewayExtend string `json:"gateway_extend" description:"Gateway 卡状态扩展字段,复机成功后本地清空"` +} + // AgentOpenAPIPackageTrafficItem 开放接口套餐流量项 type AgentOpenAPIPackageTrafficItem struct { PackageCode string `json:"package_code" description:"套餐编码"` diff --git a/internal/routes/open.go b/internal/routes/open.go index 2c27b4f..af1bce9 100644 --- a/internal/routes/open.go +++ b/internal/routes/open.go @@ -100,6 +100,15 @@ function buildAgentSign({ method, path, query, body, timestamp, nonce, account, Auth: true, SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, }) + Register(router, doc, basePath, "POST", "/cards/resume", handler.ResumeCard, RouteSpec{ + Summary: "机卡分离卡复机", + Description: authDescription + "\n\n按 card_no 调用上游复机接口。调用前检查本地落库的 Gateway 卡状态扩展字段,仅当 gateway_extend 等于“机卡分离停机”时允许复机,否则返回“仅允许机卡分离状态下的卡复机”。", + Input: new(dto.AgentOpenAPICardResumeRequest), + Output: new(dto.AgentOpenAPICardResumeResponse), + Tags: []string{tag}, + Auth: true, + SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, + }) Register(router, doc, basePath, "GET", "/packages", handler.ListPackages, RouteSpec{ Summary: "查询套餐列表", Description: authDescription + "\n\n分页查询当前账号可购买的套餐,返回套餐基础信息和价格。", diff --git a/internal/service/agent_open_api/service.go b/internal/service/agent_open_api/service.go index b51fd41..741042a 100644 --- a/internal/service/agent_open_api/service.go +++ b/internal/service/agent_open_api/service.go @@ -10,6 +10,7 @@ import ( "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset" + iotCardSvc "github.com/break/junhong_cmp_fiber/internal/service/iot_card" orderSvc "github.com/break/junhong_cmp_fiber/internal/service/order" packageSvc "github.com/break/junhong_cmp_fiber/internal/service/package" shopCommissionSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_commission" @@ -26,6 +27,7 @@ type Service struct { packageService *packageSvc.Service orderService *orderSvc.Service shopCommissionService *shopCommissionSvc.Service + stopResumeService *iotCardSvc.StopResumeService iotCardStore *postgres.IotCardStore packageUsageStore *postgres.PackageUsageStore packageStore *postgres.PackageStore @@ -39,6 +41,7 @@ func New( packageService *packageSvc.Service, orderService *orderSvc.Service, shopCommissionService *shopCommissionSvc.Service, + stopResumeService *iotCardSvc.StopResumeService, iotCardStore *postgres.IotCardStore, packageUsageStore *postgres.PackageUsageStore, packageStore *postgres.PackageStore, @@ -50,6 +53,7 @@ func New( packageService: packageService, orderService: orderService, shopCommissionService: shopCommissionService, + stopResumeService: stopResumeService, iotCardStore: iotCardStore, packageUsageStore: packageUsageStore, packageStore: packageStore, @@ -140,6 +144,27 @@ func (s *Service) GetRealnameStatus(ctx context.Context, req *dto.AgentOpenAPICa }, nil } +// ResumeCard 调用上游复机机卡分离停机卡 +func (s *Service) ResumeCard(ctx context.Context, req *dto.AgentOpenAPICardResumeRequest) (*dto.AgentOpenAPICardResumeResponse, error) { + card, err := s.resolveOpenAPICard(ctx, req.CardNo) + if err != nil { + return nil, err + } + if s.stopResumeService == nil { + return nil, errors.New(errors.CodeInternalError, "开放接口复机依赖未配置") + } + if err := s.stopResumeService.StartMachineSeparatedCard(ctx, card); err != nil { + return nil, err + } + + return &dto.AgentOpenAPICardResumeResponse{ + CardNo: req.CardNo, + CardStatus: constants.AgentOpenAPICardStatusNormal, + CardStatusName: constants.AgentOpenAPICardStatusName(constants.AgentOpenAPICardStatusNormal), + GatewayExtend: "", + }, nil +} + // ListPackages 查询开放接口代理可购买套餐列表 func (s *Service) ListPackages(ctx context.Context, req *dto.AgentOpenAPIPackageListRequest) ([]*dto.AgentOpenAPIPackageItem, int64, int, int, error) { status := constants.StatusEnabled diff --git a/internal/service/iot_card/service.go b/internal/service/iot_card/service.go index 6f34b46..fef1fcb 100644 --- a/internal/service/iot_card/service.go +++ b/internal/service/iot_card/service.go @@ -3,6 +3,7 @@ package iot_card import ( "context" stderrors "errors" + "strings" "time" "github.com/break/junhong_cmp_fiber/internal/gateway" @@ -1421,13 +1422,14 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string) if err != nil { s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err)) } else { - updates["gateway_extend"] = statusResp.Extend - networkStatus, ok := gateway.ParseCardNetworkStatus(statusResp.CardStatus, statusResp.Extend) + gatewayExtend := strings.TrimSpace(statusResp.Extend) + updates["gateway_extend"] = gatewayExtend + networkStatus, ok := gateway.ParseCardNetworkStatus(statusResp.CardStatus, gatewayExtend) if !ok { s.logger.Warn("刷新卡数据:未知 Gateway 卡状态", zap.String("iccid", iccid), zap.String("card_status", statusResp.CardStatus), - zap.String("extend", statusResp.Extend)) + zap.String("extend", gatewayExtend)) } else { updates["network_status"] = networkStatus } diff --git a/internal/service/iot_card/stop_resume_service.go b/internal/service/iot_card/stop_resume_service.go index 355a18f..26239b4 100644 --- a/internal/service/iot_card/stop_resume_service.go +++ b/internal/service/iot_card/stop_resume_service.go @@ -2,14 +2,14 @@ package iot_card import ( "context" + stderrors "errors" + "strings" "time" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" - stderrors "errors" - "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" @@ -672,6 +672,108 @@ func (s *StopResumeService) resumeCardWithRetry(ctx context.Context, card *model return lastErr } +// StartMachineSeparatedCard 对机卡分离停机卡执行复机 +// 调用上游复机前检查本地落库的 Gateway extend,只有“机卡分离停机”才允许复机。 +func (s *StopResumeService) StartMachineSeparatedCard(ctx context.Context, card *model.IotCard) error { + if card == nil { + return errors.New(errors.CodeInvalidParam) + } + if s.gatewayClient == nil { + return errors.New(errors.CodeInternalError, "Gateway 未配置,停复机操作不可用") + } + + gatewayExtend := strings.TrimSpace(card.GatewayExtend) + if gatewayExtend != constants.GatewayCardExtendMachineSeparated { + denyErr := errors.New(errors.CodeForbidden, constants.AgentOpenAPIResumeOnlyMachineSeparatedMessage) + 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{ + "gateway_extend": gatewayExtend, + }, + }) + return denyErr + } + + if err := s.resumeCardWithRetry(ctx, card); err != nil { + 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), + AfterData: map[string]any{ + "gateway_extend": gatewayExtend, + }, + }) + return wrapErr + } + + now := time.Now() + if err := s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{ + "network_status": constants.NetworkStatusOnline, + "resumed_at": now, + "stop_reason": "", + "gateway_extend": "", + }); err != nil { + 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, + "gateway_extend": gatewayExtend, + }, + AfterData: map[string]any{ + "network_status": constants.NetworkStatusOnline, + "stop_reason": "", + "gateway_extend": "", + }, + }) + 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, + "gateway_extend": gatewayExtend, + }, + AfterData: map[string]any{ + "network_status": constants.NetworkStatusOnline, + "stop_reason": "", + "gateway_extend": "", + }, + }) + + return nil +} + // ManualStopCard 手动停机单张卡(通过ICCID) func (s *StopResumeService) ManualStopCard(ctx context.Context, iccid string) error { card, err := s.iotCardStore.GetByICCID(ctx, iccid) diff --git a/internal/task/polling_cardstatus_handler.go b/internal/task/polling_cardstatus_handler.go index c6cb3bb..02ec37d 100644 --- a/internal/task/polling_cardstatus_handler.go +++ b/internal/task/polling_cardstatus_handler.go @@ -2,6 +2,7 @@ package task import ( "context" + "strings" "time" "github.com/hibiken/asynq" @@ -76,15 +77,15 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, t *asynq.Task) er return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus) } gatewayCardStatus = result.CardStatus - gatewayExtend = result.Extend + gatewayExtend = strings.TrimSpace(result.Extend) statusQueried = true - parsedStatus, ok := gateway.ParseCardNetworkStatus(result.CardStatus, result.Extend) + parsedStatus, ok := gateway.ParseCardNetworkStatus(result.CardStatus, gatewayExtend) if !ok { h.base.logger.Warn("卡状态轮询:未知 Gateway 卡状态", zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID), zap.String("card_status", result.CardStatus), - zap.String("extend", result.Extend)) + zap.String("extend", gatewayExtend)) } else { newNetworkStatus = parsedStatus } @@ -93,7 +94,7 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, t *asynq.Task) er zap.Uint("card_id", cardID), zap.String("iccid", card.ICCID), zap.String("card_status", result.CardStatus), - zap.String("extend", result.Extend), + zap.String("extend", gatewayExtend), zap.Int("new_network_status", newNetworkStatus), zap.Bool("changed", newNetworkStatus != card.NetworkStatus)) } diff --git a/pkg/constants/agent_open_api.go b/pkg/constants/agent_open_api.go index 246247e..edec14c 100644 --- a/pkg/constants/agent_open_api.go +++ b/pkg/constants/agent_open_api.go @@ -24,6 +24,11 @@ const ( AgentOpenAPIWalletCurrencyCNY = "CNY" // 人民币币种 ) +// 代理开放接口业务提示 +const ( + AgentOpenAPIResumeOnlyMachineSeparatedMessage = "仅允许机卡分离状态下的卡复机" // 仅允许机卡分离停机卡复机 +) + // AgentOpenAPICardStatusName 返回开放接口卡状态名称 func AgentOpenAPICardStatusName(status string) string { switch status { diff --git a/pkg/constants/iot.go b/pkg/constants/iot.go index 4f59745..b24c2bf 100644 --- a/pkg/constants/iot.go +++ b/pkg/constants/iot.go @@ -57,10 +57,11 @@ const ( // Gateway 卡状态 const ( - GatewayCardStatusReady = "准备" // 网关卡状态:准备 - GatewayCardStatusNormal = "正常" // 网关卡状态:正常 - GatewayCardStatusStopped = "停机" // 网关卡状态:停机 - GatewayCardExtendPendingActivation = "待激活" // 网关扩展状态:待激活 + GatewayCardStatusReady = "准备" // 网关卡状态:准备 + GatewayCardStatusNormal = "正常" // 网关卡状态:正常 + GatewayCardStatusStopped = "停机" // 网关卡状态:停机 + GatewayCardExtendPendingActivation = "待激活" // 网关扩展状态:待激活 + GatewayCardExtendMachineSeparated = "机卡分离停机" // 网关扩展状态:机卡分离停机 ) // IoT 卡停机原因