开放接口

This commit is contained in:
Break
2026-06-02 16:56:01 +08:00
parent 1b47fb8f32
commit c6e65fde9b
7 changed files with 321 additions and 10 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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:"当前生效套餐最晚过期时间"`
}

View File

@@ -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,
})
}

View File

@@ -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)