From c6e65fde9b7c869cbdf28b3009ee8a43278d40fd Mon Sep 17 00:00:00 2001 From: Break Date: Tue, 2 Jun 2026 16:56:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E6=94=BE=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 | 69 +++++++++++++ internal/model/dto/agent_open_api_dto.go | 27 +++++ internal/routes/open.go | 33 +++++++ internal/service/agent_open_api/service.go | 110 +++++++++++++++++++++ opencode.json | 16 +-- tests/agent_open_api/main.go | 74 +++++++++++++- 7 files changed, 321 insertions(+), 10 deletions(-) diff --git a/internal/bootstrap/services.go b/internal/bootstrap/services.go index 91334e7..acd3f5b 100644 --- a/internal/bootstrap/services.go +++ b/internal/bootstrap/services.go @@ -193,7 +193,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, stopResumeService, s.IotCard, s.PackageUsage, s.Package, s.PackageSeries, s.AgentWallet) + agentOpenAPI := agentOpenAPISvc.New(assetService, packageService, orderService, shopCommission, stopResumeService, device, 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 afd72bb..d16089c 100644 --- a/internal/handler/openapi/handler.go +++ b/internal/handler/openapi/handler.go @@ -160,6 +160,75 @@ func (h *Handler) ListWalletTransactions(c *fiber.Ctx) error { return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size) } +// GetDeviceTraffic 查询设备套餐内流量 +// GET /api/open/v1/devices/traffic +func (h *Handler) GetDeviceTraffic(c *fiber.Ctx) error { + var req dto.AgentOpenAPIDeviceQueryRequest + if err := c.QueryParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + if err := h.validate(&req); err != nil { + return err + } + + resp, err := h.service.GetDeviceTraffic(c.UserContext(), &req) + if err != nil { + return err + } + return response.Success(c, resp) +} + +// SwitchDeviceCard 切网(多卡设备切换到指定 ICCID) +// POST /api/open/v1/devices/switch-card +func (h *Handler) SwitchDeviceCard(c *fiber.Ctx) error { + var req dto.AgentOpenAPIDeviceSwitchCardRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + if err := h.validate(&req); err != nil { + return err + } + + if err := h.service.SwitchDeviceCard(c.UserContext(), &req); err != nil { + return err + } + return response.Success(c, nil) +} + +// RebootDevice 重启设备 +// POST /api/open/v1/devices/reboot +func (h *Handler) RebootDevice(c *fiber.Ctx) error { + var req dto.AgentOpenAPIDeviceOperationRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + if err := h.validate(&req); err != nil { + return err + } + + if err := h.service.RebootDevice(c.UserContext(), &req); err != nil { + return err + } + return response.Success(c, nil) +} + +// ResetDevice 恢复出厂设置 +// POST /api/open/v1/devices/reset +func (h *Handler) ResetDevice(c *fiber.Ctx) error { + var req dto.AgentOpenAPIDeviceOperationRequest + if err := c.BodyParser(&req); err != nil { + return errors.New(errors.CodeInvalidParam) + } + if err := h.validate(&req); err != nil { + return err + } + + if err := h.service.ResetDevice(c.UserContext(), &req); err != nil { + return err + } + return response.Success(c, nil) +} + func (h *Handler) validate(req any) error { if h.validator == nil { return nil diff --git a/internal/model/dto/agent_open_api_dto.go b/internal/model/dto/agent_open_api_dto.go index 2405927..9ac6853 100644 --- a/internal/model/dto/agent_open_api_dto.go +++ b/internal/model/dto/agent_open_api_dto.go @@ -131,3 +131,30 @@ type AgentOpenAPIWalletPackageOrderResponse struct { Orders []AgentOpenAPIWalletPackageOrderItem `json:"orders" description:"成功订单列表"` FailedCards []AgentOpenAPIWalletPackageOrderFailure `json:"failed_cards" description:"失败卡列表"` } + +// AgentOpenAPIDeviceQueryRequest 开放接口设备查询请求 +type AgentOpenAPIDeviceQueryRequest struct { + DeviceNo string `json:"device_no" query:"device_no" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"设备标识(支持虚拟号、IMEI)"` +} + +// AgentOpenAPIDeviceSwitchCardRequest 开放接口设备切网请求 +type AgentOpenAPIDeviceSwitchCardRequest struct { + DeviceNo string `json:"device_no" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"设备标识(支持虚拟号、IMEI)"` + ICCID string `json:"iccid" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"目标卡 ICCID"` +} + +// AgentOpenAPIDeviceOperationRequest 开放接口设备操作请求(重启/恢复出厂) +type AgentOpenAPIDeviceOperationRequest struct { + DeviceNo string `json:"device_no" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"设备标识(支持虚拟号、IMEI)"` +} + +// AgentOpenAPIDeviceTrafficResponse 开放接口设备流量响应 +type AgentOpenAPIDeviceTrafficResponse struct { + DeviceNo string `json:"device_no" description:"设备标识"` + ActiveRemainingFlowMB int64 `json:"active_remaining_flow_mb" description:"当前生效套餐剩余流量(MB)"` + ActiveTotalFlowMB int64 `json:"active_total_flow_mb" description:"当前生效套餐总流量(MB)"` + ActiveUsedFlowMB int64 `json:"active_used_flow_mb" description:"当前生效套餐已用流量(MB)"` + ActivePackages []AgentOpenAPIPackageTrafficItem `json:"active_packages" description:"当前生效套餐列表"` + PendingPackages []AgentOpenAPIPackageTrafficItem `json:"pending_packages" description:"待生效套餐列表"` + ActiveExpiresAt *time.Time `json:"active_expires_at,omitempty" description:"当前生效套餐最晚过期时间"` +} diff --git a/internal/routes/open.go b/internal/routes/open.go index af1bce9..f9d0f99 100644 --- a/internal/routes/open.go +++ b/internal/routes/open.go @@ -144,4 +144,37 @@ function buildAgentSign({ method, path, query, body, timestamp, nonce, account, Auth: true, SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, }) + Register(router, doc, basePath, "GET", "/devices/traffic", handler.GetDeviceTraffic, RouteSpec{ + Summary: "查询设备套餐内流量", + Description: authDescription + "\n\n按 device_no 查询设备套餐内流量、当前套餐和待生效套餐。device_no 支持虚拟号、IMEI。", + Input: new(dto.AgentOpenAPIDeviceQueryRequest), + Output: new(dto.AgentOpenAPIDeviceTrafficResponse), + Tags: []string{tag}, + Auth: true, + SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, + }) + Register(router, doc, basePath, "POST", "/devices/switch-card", handler.SwitchDeviceCard, RouteSpec{ + Summary: "切网(多卡设备切换 ICCID)", + Description: authDescription + "\n\n按 device_no 将多卡设备切换到指定 ICCID 的卡。", + Input: new(dto.AgentOpenAPIDeviceSwitchCardRequest), + Tags: []string{tag}, + Auth: true, + SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, + }) + Register(router, doc, basePath, "POST", "/devices/reboot", handler.RebootDevice, RouteSpec{ + Summary: "重启设备", + Description: authDescription + "\n\n按 device_no 远程重启设备。", + Input: new(dto.AgentOpenAPIDeviceOperationRequest), + Tags: []string{tag}, + Auth: true, + SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, + }) + Register(router, doc, basePath, "POST", "/devices/reset", handler.ResetDevice, RouteSpec{ + Summary: "恢复出厂设置", + Description: authDescription + "\n\n按 device_no 远程恢复设备出厂设置。", + Input: new(dto.AgentOpenAPIDeviceOperationRequest), + Tags: []string{tag}, + Auth: true, + SecurityScheme: openapi.SecuritySchemeAgentOpenAPI, + }) } diff --git a/internal/service/agent_open_api/service.go b/internal/service/agent_open_api/service.go index 741042a..4b84463 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" + deviceSvc "github.com/break/junhong_cmp_fiber/internal/service/device" 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" @@ -28,6 +29,7 @@ type Service struct { orderService *orderSvc.Service shopCommissionService *shopCommissionSvc.Service stopResumeService *iotCardSvc.StopResumeService + deviceService *deviceSvc.Service iotCardStore *postgres.IotCardStore packageUsageStore *postgres.PackageUsageStore packageStore *postgres.PackageStore @@ -42,6 +44,7 @@ func New( orderService *orderSvc.Service, shopCommissionService *shopCommissionSvc.Service, stopResumeService *iotCardSvc.StopResumeService, + deviceService *deviceSvc.Service, iotCardStore *postgres.IotCardStore, packageUsageStore *postgres.PackageUsageStore, packageStore *postgres.PackageStore, @@ -54,6 +57,7 @@ func New( orderService: orderService, shopCommissionService: shopCommissionService, stopResumeService: stopResumeService, + deviceService: deviceService, iotCardStore: iotCardStore, packageUsageStore: packageUsageStore, packageStore: packageStore, @@ -302,6 +306,112 @@ func (s *Service) CreateWalletPackageOrders(ctx context.Context, req *dto.AgentO return resp, nil } +// resolveOpenAPIDevice 将开放接口设备标识解析为当前代理可见的设备 +// 设备不存在或不属于代理管辖店铺时统一返回 CodeForbidden,避免信息泄露 +func (s *Service) resolveOpenAPIDevice(ctx context.Context, deviceNo string) (*model.Device, error) { + identifier := strings.TrimSpace(deviceNo) + if identifier == "" { + return nil, errors.New(errors.CodeInvalidParam) + } + if s.deviceService == nil { + return nil, errors.New(errors.CodeInternalError, "开放接口设备解析依赖未配置") + } + + device, err := s.deviceService.GetDeviceByIdentifier(ctx, identifier) + if err != nil { + return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } + + if device.ShopID == nil { + return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } + + subordinateIDs := middleware.GetSubordinateShopIDs(ctx) + found := false + for _, id := range subordinateIDs { + if id == *device.ShopID { + found = true + break + } + } + if !found { + return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") + } + + return device, nil +} + +// GetDeviceTraffic 查询开放接口设备套餐内流量 +func (s *Service) GetDeviceTraffic(ctx context.Context, req *dto.AgentOpenAPIDeviceQueryRequest) (*dto.AgentOpenAPIDeviceTrafficResponse, error) { + device, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo) + if err != nil { + return nil, err + } + + activeStatus := constants.PackageUsageStatusActive + pendingStatus := constants.PackageUsageStatusPending + activeUsages, err := s.packageUsageStore.ListByCarrier(ctx, "device", device.ID, &activeStatus) + if err != nil { + return nil, errors.Wrap(errors.CodeInternalError, err, "查询生效套餐失败") + } + pendingUsages, err := s.packageUsageStore.ListByCarrier(ctx, "device", device.ID, &pendingStatus) + if err != nil { + return nil, errors.Wrap(errors.CodeInternalError, err, "查询待生效套餐失败") + } + + packageMap, seriesMap, err := s.loadUsagePackageContext(ctx, append(activeUsages, pendingUsages...)) + if err != nil { + return nil, err + } + + resp := &dto.AgentOpenAPIDeviceTrafficResponse{ + DeviceNo: req.DeviceNo, + ActivePackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(activeUsages)), + PendingPackages: make([]dto.AgentOpenAPIPackageTrafficItem, 0, len(pendingUsages)), + } + for _, usage := range activeUsages { + item := s.buildTrafficItem(usage, packageMap, seriesMap, true) + resp.ActivePackages = append(resp.ActivePackages, item) + resp.ActiveTotalFlowMB += item.TotalFlowMB + resp.ActiveUsedFlowMB += item.UsedFlowMB + resp.ActiveRemainingFlowMB += item.RemainingFlowMB + if item.ExpiresAt != nil && (resp.ActiveExpiresAt == nil || item.ExpiresAt.After(*resp.ActiveExpiresAt)) { + resp.ActiveExpiresAt = item.ExpiresAt + } + } + for _, usage := range pendingUsages { + resp.PendingPackages = append(resp.PendingPackages, s.buildTrafficItem(usage, packageMap, seriesMap, false)) + } + + return resp, nil +} + +// SwitchDeviceCard 开放接口切网(多卡设备切换到指定 ICCID) +func (s *Service) SwitchDeviceCard(ctx context.Context, req *dto.AgentOpenAPIDeviceSwitchCardRequest) error { + if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil { + return err + } + return s.deviceService.GatewaySwitchCard(ctx, req.DeviceNo, &dto.SwitchCardRequest{ + TargetICCID: req.ICCID, + }) +} + +// RebootDevice 开放接口重启设备 +func (s *Service) RebootDevice(ctx context.Context, req *dto.AgentOpenAPIDeviceOperationRequest) error { + if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil { + return err + } + return s.deviceService.GatewayRebootDevice(ctx, req.DeviceNo) +} + +// ResetDevice 开放接口恢复出厂设置 +func (s *Service) ResetDevice(ctx context.Context, req *dto.AgentOpenAPIDeviceOperationRequest) error { + if _, err := s.resolveOpenAPIDevice(ctx, req.DeviceNo); err != nil { + return err + } + return s.deviceService.GatewayResetDevice(ctx, req.DeviceNo) +} + // resolveOpenAPICard 将开放接口卡标识解析为当前代理可见的 IoT 卡 func (s *Service) resolveOpenAPICard(ctx context.Context, cardNo string) (*model.IotCard, error) { identifier := strings.TrimSpace(cardNo) diff --git a/opencode.json b/opencode.json index 8bf5b43..0d5a3ca 100644 --- a/opencode.json +++ b/opencode.json @@ -3,16 +3,16 @@ "provider": { "anthropic": { "options": { - "baseURL": "http://45.155.220.179:8317/v1", - "apiKey": "sk-ZBGcMXCdwtSK7G35s" - } - }, - "openai": { - "options": { - "baseURL": "http://45.155.220.179:8317/v1", - "apiKey": "sk-ZBGcMXCdwtSK7G35s" + "baseURL": "https://relay.apirelay.co/v1", + "apiKey": "sk-f7176eb7ae4c39ed5e6f2946a1876d9bec38c54b60cb6fb2c13d639e77c4d7a5" } } + // "openai": { + // "options": { + // "baseURL": "https://relay.apirelay.co/v1", + // "apiKey": "sk-f7176eb7ae4c39ed5e6f2946a1876d9bec38c54b60cb6fb2c13d639e77c4d7a5" + // } + // } }, "mcp": { "context7": { diff --git a/tests/agent_open_api/main.go b/tests/agent_open_api/main.go index daa8aa9..82cb83d 100644 --- a/tests/agent_open_api/main.go +++ b/tests/agent_open_api/main.go @@ -41,6 +41,22 @@ type walletPackageOrderRequest struct { PackageCode string `json:"package_code"` } +// cardResumeRequest 是"机卡分离卡复机"的请求体。 +type cardResumeRequest struct { + CardNo string `json:"card_no"` +} + +// deviceSwitchCardRequest 是"设备切网"的请求体。 +type deviceSwitchCardRequest struct { + DeviceNo string `json:"device_no"` + ICCID string `json:"iccid"` +} + +// deviceOperationRequest 是设备操作(重启/恢复出厂)的请求体。 +type deviceOperationRequest struct { + DeviceNo string `json:"device_no"` +} + // client 是第三方调用方视角的独立客户端,不依赖当前项目内部代码。 type client struct { baseURL string @@ -53,8 +69,10 @@ func main() { baseURL := flag.String("base-url", envOrDefault("AGENT_OPEN_API_BASE_URL", defaultBaseURL), "接口服务地址") account := flag.String("account", envOrDefault("AGENT_OPEN_API_ACCOUNT", defaultAccount), "代理账号用户名或手机号") password := flag.String("password", envOrDefault("AGENT_OPEN_API_PASSWORD", defaultPassword), "代理账号登录密码") - action := flag.String("action", "wallet-balance", "调用动作:realname-status、card-status、card-traffic、packages、wallet-balance、package-order、wallet-transactions") + action := flag.String("action", "wallet-balance", "调用动作:realname-status、card-status、card-traffic、card-resume、packages、wallet-balance、package-order、wallet-transactions、device-traffic、device-switch-card、device-reboot、device-reset") cardNo := flag.String("card-no", "", "卡标识,支持 ICCID、虚拟号、MSISDN") + deviceNo := flag.String("device-no", "", "设备标识,支持虚拟号、IMEI") + iccid := flag.String("iccid", "", "目标卡 ICCID,device-switch-card 时使用") cardNos := flag.String("card-nos", "", "卡标识列表,多个值用英文逗号分隔") packageCode := flag.String("package-code", "", "套餐编码") page := flag.Int("page", 0, "页码,不传或 0 表示省略") @@ -91,6 +109,8 @@ func main() { transactionType: *transactionType, startDate: *startDate, endDate: *endDate, + deviceNo: *deviceNo, + iccid: *iccid, }) if err != nil { fmt.Fprintf(os.Stderr, "调用失败:%v\n", err) @@ -118,6 +138,8 @@ type runOptions struct { transactionType string startDate string endDate string + deviceNo string + iccid string } func dispatch(ctx context.Context, c *client, opts runOptions) (int, []byte, error) { @@ -137,6 +159,11 @@ func dispatch(ctx context.Context, c *client, opts runOptions) (int, []byte, err return 0, nil, errors.New("card-traffic 需要传入 -card-no") } return c.queryCardTraffic(ctx, opts.cardNo) + case "card-resume": + if opts.cardNo == "" { + return 0, nil, errors.New("card-resume 需要传入 -card-no") + } + return c.resumeCard(ctx, opts.cardNo) case "packages": return c.queryPackages(ctx, packageQuery(opts)) case "wallet-balance": @@ -152,6 +179,29 @@ func dispatch(ctx context.Context, c *client, opts runOptions) (int, []byte, err return 0, nil, err } return c.queryWalletTransactions(ctx, walletTransactionQuery(opts)) + case "device-traffic": + if opts.deviceNo == "" { + return 0, nil, errors.New("device-traffic 需要传入 -device-no") + } + return c.queryDeviceTraffic(ctx, opts.deviceNo) + case "device-switch-card": + if opts.deviceNo == "" { + return 0, nil, errors.New("device-switch-card 需要传入 -device-no") + } + if opts.iccid == "" { + return 0, nil, errors.New("device-switch-card 需要传入 -iccid") + } + return c.switchDeviceCard(ctx, opts.deviceNo, opts.iccid) + case "device-reboot": + if opts.deviceNo == "" { + return 0, nil, errors.New("device-reboot 需要传入 -device-no") + } + return c.rebootDevice(ctx, opts.deviceNo) + case "device-reset": + if opts.deviceNo == "" { + return 0, nil, errors.New("device-reset 需要传入 -device-no") + } + return c.resetDevice(ctx, opts.deviceNo) default: return 0, nil, fmt.Errorf("未知 action:%s", opts.action) } @@ -191,6 +241,28 @@ func (c *client) queryWalletTransactions(ctx context.Context, query url.Values) return c.get(ctx, "/api/open/v1/wallet/transactions", query) } +func (c *client) resumeCard(ctx context.Context, cardNo string) (int, []byte, error) { + return c.postJSON(ctx, "/api/open/v1/cards/resume", cardResumeRequest{CardNo: cardNo}) +} + +func (c *client) queryDeviceTraffic(ctx context.Context, deviceNo string) (int, []byte, error) { + query := url.Values{} + query.Add("device_no", deviceNo) + return c.get(ctx, "/api/open/v1/devices/traffic", query) +} + +func (c *client) switchDeviceCard(ctx context.Context, deviceNo string, iccid string) (int, []byte, error) { + return c.postJSON(ctx, "/api/open/v1/devices/switch-card", deviceSwitchCardRequest{DeviceNo: deviceNo, ICCID: iccid}) +} + +func (c *client) rebootDevice(ctx context.Context, deviceNo string) (int, []byte, error) { + return c.postJSON(ctx, "/api/open/v1/devices/reboot", deviceOperationRequest{DeviceNo: deviceNo}) +} + +func (c *client) resetDevice(ctx context.Context, deviceNo string) (int, []byte, error) { + return c.postJSON(ctx, "/api/open/v1/devices/reset", deviceOperationRequest{DeviceNo: deviceNo}) +} + func (c *client) get(ctx context.Context, path string, query url.Values) (int, []byte, error) { return c.do(ctx, http.MethodGet, path, query, nil) }