开放接口,修复上游同步不对的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s
This commit is contained in:
@@ -21,6 +21,7 @@ func generateOpenAPIDocs(outputPath string, logger *zap.Logger) {
|
||||
app := fiber.New()
|
||||
|
||||
// 3. 创建所有 Handler(使用 nil 依赖,因为只需要路由结构)
|
||||
// 新增 Handler 必须注册到 openapi.BuildDocHandlers,代理开放接口也从该入口进入文档生成器。
|
||||
handlers := openapi.BuildDocHandlers()
|
||||
|
||||
// 4. 注册所有路由到文档生成器
|
||||
|
||||
@@ -30,6 +30,7 @@ func generateAdminDocs(outputPath string) error {
|
||||
app := fiber.New()
|
||||
|
||||
// 3. 创建所有 Handler(使用 nil 依赖,因为只需要路由结构)
|
||||
// 新增 Handler 必须注册到 openapi.BuildDocHandlers,代理开放接口也从该入口进入文档生成器。
|
||||
handlers := openapi.BuildDocHandlers()
|
||||
|
||||
// 4. 注册所有路由到文档生成器
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||
openapiHandler "github.com/break/junhong_cmp_fiber/internal/handler/openapi"
|
||||
pollingPkg "github.com/break/junhong_cmp_fiber/internal/polling"
|
||||
clientOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/client_order"
|
||||
pollingSvcPkg "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
||||
@@ -142,5 +143,6 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
Refund: admin.NewRefundHandler(svc.Refund),
|
||||
ClientWechat: app.NewClientWechatHandler(svc.WechatConfig, deps.Redis, deps.Logger),
|
||||
SuperAdmin: admin.NewSuperAdminHandler(svc.OperationPassword),
|
||||
AgentOpenAPI: openapiHandler.NewHandler(svc.AgentOpenAPI, validate),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,16 @@ func initMiddlewares(deps *Dependencies, stores *stores) *Middlewares {
|
||||
// 创建后台认证中间件(传入 ShopStore 以支持预计算下级店铺 ID)
|
||||
adminAuthMiddleware := createAdminAuthMiddleware(tokenManager, stores.Shop)
|
||||
|
||||
agentOpenAPIAuthMiddleware := middleware.NewAgentOpenAPIAuthMiddleware(middleware.AgentOpenAPIAuthConfig{
|
||||
AccountStore: stores.Account,
|
||||
ShopStore: stores.Shop,
|
||||
Redis: deps.Redis,
|
||||
})
|
||||
|
||||
return &Middlewares{
|
||||
PersonalAuth: personalAuthMiddleware,
|
||||
AdminAuth: adminAuthMiddleware,
|
||||
PersonalAuth: personalAuthMiddleware,
|
||||
AdminAuth: adminAuthMiddleware,
|
||||
AgentOpenAPIAuth: agentOpenAPIAuthMiddleware,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
agentOpenAPISvc "github.com/break/junhong_cmp_fiber/internal/service/agent_open_api"
|
||||
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||
@@ -107,6 +108,7 @@ type services struct {
|
||||
Refund *refundSvc.Service
|
||||
TrafficQuery *trafficSvc.QueryService
|
||||
OperationPassword *operationPasswordSvc.Service
|
||||
AgentOpenAPI *agentOpenAPISvc.Service
|
||||
}
|
||||
|
||||
func initServices(s *stores, deps *Dependencies) *services {
|
||||
@@ -185,6 +187,11 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
assetAudit,
|
||||
)
|
||||
operationPassword := operationPasswordSvc.New(deps.Redis)
|
||||
shopCommission := shopCommissionSvc.New(s.Shop, s.Account, s.AgentWallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.AgentWalletTransaction, deps.DB, deps.Logger)
|
||||
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)
|
||||
|
||||
return &services{
|
||||
Account: account,
|
||||
@@ -209,7 +216,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
),
|
||||
Shop: shopSvc.New(s.Shop, s.Account, s.ShopRole, s.Role, s.AccountRole, s.AgentWallet),
|
||||
Auth: authSvc.New(s.Account, s.AccountRole, s.RolePermission, s.Permission, s.Shop, deps.TokenManager, deps.Logger),
|
||||
ShopCommission: shopCommissionSvc.New(s.Shop, s.Account, s.AgentWallet, s.CommissionWithdrawalRequest, s.CommissionWithdrawalSetting, s.CommissionRecord, s.AgentWalletTransaction, deps.DB, deps.Logger),
|
||||
ShopCommission: shopCommission,
|
||||
CommissionWithdrawal: commissionWithdrawalSvc.New(deps.DB, s.Shop, s.Account, s.AgentWallet, s.AgentWalletTransaction, s.CommissionWithdrawalRequest),
|
||||
CommissionWithdrawalSetting: commissionWithdrawalSettingSvc.New(deps.DB, s.Account, s.CommissionWithdrawalSetting),
|
||||
CommissionCalculation: commissionCalculationSvc.New(
|
||||
@@ -242,7 +249,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
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),
|
||||
Package: packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation),
|
||||
Package: packageService,
|
||||
PackageDailyRecord: packageSvc.NewDailyRecordService(deps.DB, deps.Redis, s.PackageUsageDailyRecord, deps.Logger),
|
||||
PackageCustomerView: packageSvc.NewCustomerViewService(deps.DB, deps.Redis, s.PackageUsage, deps.Logger),
|
||||
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.Shop),
|
||||
@@ -250,7 +257,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
ShopSeriesGrant: shopSeriesGrantSvc.New(deps.DB, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries, deps.Logger),
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
PurchaseValidation: purchaseValidation,
|
||||
Order: 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),
|
||||
Order: orderService,
|
||||
Exchange: exchangeSvc.New(deps.DB, s.ExchangeOrder, s.IotCard, s.Device, s.AssetWallet, s.AssetWalletTransaction, s.PackageUsage, s.PackageUsageDailyRecord, s.ResourceTag, s.PersonalCustomerDevice, deps.Logger),
|
||||
Recharge: rechargeSvc.New(deps.DB, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger),
|
||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig, deps.Redis, deps.Logger),
|
||||
@@ -259,7 +266,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
|
||||
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),
|
||||
Asset: assetService,
|
||||
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, assetAudit),
|
||||
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
||||
StopResumeService: stopResumeService,
|
||||
@@ -279,6 +286,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
PackageActivation: packageActivation,
|
||||
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
||||
OperationPassword: operationPassword,
|
||||
AgentOpenAPI: agentOpenAPI,
|
||||
Refund: refundSvc.New(
|
||||
deps.DB,
|
||||
s.RefundRequest,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||
openapiHandler "github.com/break/junhong_cmp_fiber/internal/handler/openapi"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
@@ -64,12 +65,14 @@ type Handlers struct {
|
||||
Refund *admin.RefundHandler
|
||||
ClientWechat *app.ClientWechatHandler
|
||||
SuperAdmin *admin.SuperAdminHandler
|
||||
AgentOpenAPI *openapiHandler.Handler
|
||||
}
|
||||
|
||||
// Middlewares 封装所有中间件
|
||||
// 用于路由注册
|
||||
type Middlewares struct {
|
||||
PersonalAuth *middleware.PersonalAuthMiddleware
|
||||
AdminAuth func(*fiber.Ctx) error
|
||||
PersonalAuth *middleware.PersonalAuthMiddleware
|
||||
AdminAuth func(*fiber.Ctx) error
|
||||
AgentOpenAPIAuth func(*fiber.Ctx) error
|
||||
// TODO: 新增 Middleware 在此添加字段
|
||||
}
|
||||
|
||||
32
internal/gateway/card_status.go
Normal file
32
internal/gateway/card_status.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// ParseCardNetworkStatus 将 Gateway 卡状态转换为系统网络状态。
|
||||
// 只有网关明确返回“正常”才视为开机;“准备”或“待激活”不能按正常卡展示。
|
||||
func ParseCardNetworkStatus(cardStatus, extend string) (int, bool) {
|
||||
status := strings.TrimSpace(cardStatus)
|
||||
ext := strings.TrimSpace(extend)
|
||||
|
||||
switch status {
|
||||
case constants.GatewayCardStatusNormal:
|
||||
return constants.NetworkStatusOnline, true
|
||||
case constants.GatewayCardStatusStopped, constants.GatewayCardStatusReady:
|
||||
return constants.NetworkStatusOffline, true
|
||||
}
|
||||
|
||||
if ext == constants.GatewayCardExtendPendingActivation {
|
||||
return constants.NetworkStatusOffline, true
|
||||
}
|
||||
|
||||
return constants.NetworkStatusOffline, false
|
||||
}
|
||||
|
||||
// IsGatewayCardStopped 判断 Gateway 是否明确返回停机状态。
|
||||
func IsGatewayCardStopped(cardStatus string) bool {
|
||||
return strings.TrimSpace(cardStatus) == constants.GatewayCardStatusStopped
|
||||
}
|
||||
@@ -46,6 +46,37 @@ func (i *FlexInt) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlexFloat 灵活浮点类型
|
||||
// Gateway 部分流量字段会在 number / string 之间漂移,此类型统一解析为 float64。
|
||||
type FlexFloat float64
|
||||
|
||||
// UnmarshalJSON 实现 json.Unmarshaler 接口
|
||||
// 支持:0.00, "0.00", null
|
||||
func (f *FlexFloat) UnmarshalJSON(data []byte) error {
|
||||
raw := strings.TrimSpace(string(data))
|
||||
if raw == "" || raw == "null" {
|
||||
*f = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(raw, "\"") && strings.HasSuffix(raw, "\"") {
|
||||
unquoted, err := strconv.Unquote(raw)
|
||||
if err != nil {
|
||||
*f = 0
|
||||
return nil
|
||||
}
|
||||
raw = strings.TrimSpace(unquoted)
|
||||
}
|
||||
|
||||
n, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
*f = 0
|
||||
return nil
|
||||
}
|
||||
*f = FlexFloat(n)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlexString 灵活字符串类型
|
||||
// Gateway 部分字段会在 string / number / bool 之间漂移,此类型统一转为字符串避免解析失败。
|
||||
type FlexString string
|
||||
@@ -91,7 +122,7 @@ type CardStatusReq struct {
|
||||
// CardStatusResp 是查询流量卡状态的响应
|
||||
type CardStatusResp struct {
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
CardStatus string `json:"cardStatus" description:"卡状态(准备、正常、停机)"`
|
||||
CardStatus string `json:"cardStatus" description:"卡状态(准备、正常、停机;仅正常表示开机)"`
|
||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||
}
|
||||
|
||||
@@ -102,9 +133,9 @@ type FlowQueryReq struct {
|
||||
|
||||
// FlowUsageResp 是查询流量使用的响应
|
||||
type FlowUsageResp struct {
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
Used float64 `json:"used" description:"当月已用流量(MB)"`
|
||||
Unit string `json:"unit" description:"流量单位(MB)"`
|
||||
ICCID string `json:"iccid" description:"ICCID"`
|
||||
Used FlexFloat `json:"used" description:"当月已用流量(MB,兼容数字或字符串)"`
|
||||
Unit string `json:"unit" description:"流量单位(MB)"`
|
||||
}
|
||||
|
||||
// CardOperationReq 是停机/复机请求
|
||||
|
||||
153
internal/handler/openapi/handler.go
Normal file
153
internal/handler/openapi/handler.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
agentOpenAPIService "github.com/break/junhong_cmp_fiber/internal/service/agent_open_api"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// Handler 代理开放接口处理器
|
||||
type Handler struct {
|
||||
service *agentOpenAPIService.Service
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewHandler 创建代理开放接口处理器
|
||||
func NewHandler(service *agentOpenAPIService.Service, validator *validator.Validate) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
validator: validator,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCardTraffic 查询单卡流量
|
||||
// GET /api/open/v1/cards/traffic
|
||||
func (h *Handler) GetCardTraffic(c *fiber.Ctx) error {
|
||||
var req dto.AgentOpenAPICardQueryRequest
|
||||
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.GetCardTraffic(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetCardStatus 查询单卡状态
|
||||
// GET /api/open/v1/cards/status
|
||||
func (h *Handler) GetCardStatus(c *fiber.Ctx) error {
|
||||
var req dto.AgentOpenAPICardQueryRequest
|
||||
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.GetCardStatus(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetRealnameStatus 查询单卡实名状态
|
||||
// GET /api/open/v1/cards/realname-status
|
||||
func (h *Handler) GetRealnameStatus(c *fiber.Ctx) error {
|
||||
var req dto.AgentOpenAPICardQueryRequest
|
||||
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.GetRealnameStatus(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 {
|
||||
var req dto.AgentOpenAPIPackageListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := h.validate(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
items, total, page, pageSize, err := h.service.ListPackages(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, items, total, page, pageSize)
|
||||
}
|
||||
|
||||
// CreateWalletPackageOrders 使用预充值钱包购买套餐
|
||||
// POST /api/open/v1/wallet/package-orders
|
||||
func (h *Handler) CreateWalletPackageOrders(c *fiber.Ctx) error {
|
||||
var req dto.AgentOpenAPIWalletPackageOrderRequest
|
||||
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.CreateWalletPackageOrders(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// GetWalletBalance 查询预充值钱包余额
|
||||
// GET /api/open/v1/wallet/balance
|
||||
func (h *Handler) GetWalletBalance(c *fiber.Ctx) error {
|
||||
resp, err := h.service.GetWalletBalance(c.UserContext())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, resp)
|
||||
}
|
||||
|
||||
// ListWalletTransactions 查询预充值钱包流水
|
||||
// GET /api/open/v1/wallet/transactions
|
||||
func (h *Handler) ListWalletTransactions(c *fiber.Ctx) error {
|
||||
var req dto.AgentOpenAPIWalletTransactionListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if err := h.validate(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := h.service.ListWalletTransactions(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.SuccessWithPagination(c, result.Items, result.Total, result.Page, result.Size)
|
||||
}
|
||||
|
||||
func (h *Handler) validate(req any) error {
|
||||
if h.validator == nil {
|
||||
return nil
|
||||
}
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
249
internal/middleware/agent_open_api_auth.go
Normal file
249
internal/middleware/agent_open_api_auth.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AgentOpenAPIAuthConfig 代理开放接口认证中间件配置
|
||||
type AgentOpenAPIAuthConfig struct {
|
||||
AccountStore *postgres.AccountStore
|
||||
ShopStore *postgres.ShopStore
|
||||
Redis *redis.Client
|
||||
}
|
||||
|
||||
// NewAgentOpenAPIAuthMiddleware 创建代理开放接口认证中间件
|
||||
func NewAgentOpenAPIAuthMiddleware(config AgentOpenAPIAuthConfig) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if c.Method() == fiber.MethodOptions {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
credentials := extractAgentOpenAPICredentials(c)
|
||||
if err := credentials.validateRequired(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account, err := authenticateAgentAccountForOpenAPI(c.UserContext(), config, credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateAgentOpenAPITimestamp(credentials.Timestamp, time.Now()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := reserveAgentOpenAPINonce(c.UserContext(), config.Redis, credentials.Account, credentials.Nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := buildAgentOpenAPISignPayload(c, credentials)
|
||||
if err := verifyAgentOpenAPISign(payload, credentials.Password, credentials.Sign); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := injectAgentOpenAPIContext(c, config.ShopStore, account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
type agentOpenAPICredentials struct {
|
||||
Account string
|
||||
Password string
|
||||
Timestamp string
|
||||
Nonce string
|
||||
Sign string
|
||||
}
|
||||
|
||||
func extractAgentOpenAPICredentials(c *fiber.Ctx) agentOpenAPICredentials {
|
||||
return agentOpenAPICredentials{
|
||||
Account: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderAccount)),
|
||||
Password: c.Get(constants.AgentOpenAPIHeaderPassword),
|
||||
Timestamp: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderTimestamp)),
|
||||
Nonce: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderNonce)),
|
||||
Sign: strings.TrimSpace(c.Get(constants.AgentOpenAPIHeaderSign)),
|
||||
}
|
||||
}
|
||||
|
||||
func (c agentOpenAPICredentials) validateRequired() error {
|
||||
if c.Account == "" || c.Password == "" || c.Timestamp == "" || c.Nonce == "" || c.Sign == "" {
|
||||
return errors.New(errors.CodeOpenAPIAuthFailed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authenticateAgentAccountForOpenAPI(ctx context.Context, config AgentOpenAPIAuthConfig, credentials agentOpenAPICredentials) (*model.Account, error) {
|
||||
if config.AccountStore == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "开放接口认证依赖未配置")
|
||||
}
|
||||
|
||||
account, err := config.AccountStore.GetByUsernameOrPhone(ctx, credentials.Account)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeOpenAPIAuthFailed)
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询开放接口账号失败")
|
||||
}
|
||||
|
||||
if account.UserType != constants.UserTypeAgent || account.ShopID == nil || *account.ShopID == 0 {
|
||||
return nil, errors.New(errors.CodeForbidden, "开放接口仅允许代理账号调用")
|
||||
}
|
||||
if account.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeOpenAPIAuthFailed)
|
||||
}
|
||||
if config.ShopStore == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "开放接口店铺认证依赖未配置")
|
||||
}
|
||||
shop, err := config.ShopStore.GetByID(ctx, *account.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeOpenAPIAuthFailed)
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询开放接口代理店铺失败")
|
||||
}
|
||||
if shop.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeOpenAPIAuthFailed)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)); err != nil {
|
||||
return nil, errors.New(errors.CodeOpenAPIAuthFailed)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func validateAgentOpenAPITimestamp(raw string, now time.Time) error {
|
||||
requestTime, err := parseAgentOpenAPITimestamp(raw)
|
||||
if err != nil {
|
||||
return errors.New(errors.CodeOpenAPITimestampInvalid)
|
||||
}
|
||||
diff := now.Sub(requestTime)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff > constants.AgentOpenAPISignatureWindow {
|
||||
return errors.New(errors.CodeOpenAPITimestampInvalid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAgentOpenAPITimestamp(raw string) (time.Time, error) {
|
||||
if ts, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
||||
if len(raw) == 13 {
|
||||
return time.UnixMilli(ts), nil
|
||||
}
|
||||
return time.Unix(ts, 0), nil
|
||||
}
|
||||
return time.Parse(time.RFC3339, raw)
|
||||
}
|
||||
|
||||
func reserveAgentOpenAPINonce(ctx context.Context, redisClient *redis.Client, account, nonce string) error {
|
||||
if redisClient == nil {
|
||||
return errors.New(errors.CodeInternalError, "Redis 未配置")
|
||||
}
|
||||
|
||||
key := constants.RedisAgentOpenAPINonceKey(account, nonce)
|
||||
ok, err := redisClient.SetNX(ctx, key, "1", constants.AgentOpenAPISignatureWindow).Result()
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeRedisError, err, "记录开放接口 nonce 失败")
|
||||
}
|
||||
if !ok {
|
||||
return errors.New(errors.CodeOpenAPINonceReplay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildAgentOpenAPISignPayload(c *fiber.Ctx, credentials agentOpenAPICredentials) string {
|
||||
bodyHash := sha256.Sum256(c.Body())
|
||||
parts := []string{
|
||||
strings.ToUpper(c.Method()),
|
||||
c.Path(),
|
||||
canonicalAgentOpenAPIQuery(string(c.Request().URI().QueryString())),
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
credentials.Timestamp,
|
||||
credentials.Nonce,
|
||||
credentials.Account,
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func canonicalAgentOpenAPIQuery(rawQuery string) string {
|
||||
values, err := url.ParseQuery(rawQuery)
|
||||
if err != nil || len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
for key := range values {
|
||||
if strings.EqualFold(key, "sign") {
|
||||
delete(values, key)
|
||||
}
|
||||
}
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
sort.Strings(values[key])
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
pairs := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
for _, value := range values[key] {
|
||||
pairs = append(pairs, url.QueryEscape(key)+"="+url.QueryEscape(value))
|
||||
}
|
||||
}
|
||||
return strings.Join(pairs, "&")
|
||||
}
|
||||
|
||||
func verifyAgentOpenAPISign(payload, password, providedSign string) error {
|
||||
mac := hmac.New(sha256.New, []byte(password))
|
||||
_, _ = mac.Write([]byte(payload))
|
||||
expectedSign := hex.EncodeToString(mac.Sum(nil))
|
||||
if subtle.ConstantTimeCompare([]byte(expectedSign), []byte(strings.ToLower(providedSign))) != 1 {
|
||||
return errors.New(errors.CodeOpenAPISignInvalid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func injectAgentOpenAPIContext(c *fiber.Ctx, shopStore *postgres.ShopStore, account *model.Account) error {
|
||||
shopID := uint(0)
|
||||
if account.ShopID != nil {
|
||||
shopID = *account.ShopID
|
||||
}
|
||||
shopIDs := []uint{shopID}
|
||||
if shopStore != nil && shopID > 0 {
|
||||
var err error
|
||||
shopIDs, err = shopStore.GetSubordinateShopIDs(c.UserContext(), shopID)
|
||||
if err != nil || len(shopIDs) == 0 {
|
||||
shopIDs = []uint{shopID}
|
||||
}
|
||||
}
|
||||
|
||||
info := &pkgmiddleware.UserContextInfo{
|
||||
UserID: account.ID,
|
||||
UserType: constants.UserTypeAgent,
|
||||
Username: account.Username,
|
||||
ShopID: shopID,
|
||||
SubordinateShopIDs: shopIDs,
|
||||
}
|
||||
pkgmiddleware.SetUserToFiberContext(c, info)
|
||||
return nil
|
||||
}
|
||||
119
internal/model/dto/agent_open_api_dto.go
Normal file
119
internal/model/dto/agent_open_api_dto.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// AgentOpenAPICardQueryRequest 开放接口单卡查询请求
|
||||
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)"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIPackageTrafficItem 开放接口套餐流量项
|
||||
type AgentOpenAPIPackageTrafficItem struct {
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
PackageType string `json:"package_type" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
PackageTypeName string `json:"package_type_name" description:"套餐类型名称(中文)"`
|
||||
TotalFlowMB int64 `json:"total_flow_mb" description:"套餐总流量(MB)"`
|
||||
UsedFlowMB int64 `json:"used_flow_mb,omitempty" description:"套餐已用流量(MB,仅生效套餐返回)"`
|
||||
RemainingFlowMB int64 `json:"remaining_flow_mb,omitempty" description:"套餐剩余流量(MB,仅生效套餐返回)"`
|
||||
StartAt *time.Time `json:"start_at,omitempty" description:"套餐生效时间"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty" description:"套餐过期时间"`
|
||||
ValidDays int `json:"valid_days,omitempty" description:"套餐有效天数(待生效套餐返回)"`
|
||||
Priority int `json:"priority,omitempty" description:"生效优先级(数字越小优先级越高)"`
|
||||
}
|
||||
|
||||
// AgentOpenAPICardTrafficResponse 开放接口卡流量响应
|
||||
type AgentOpenAPICardTrafficResponse struct {
|
||||
CardNo string `json:"card_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:"当前生效套餐最晚过期时间"`
|
||||
}
|
||||
|
||||
// AgentOpenAPICardStatusResponse 开放接口卡状态响应
|
||||
type AgentOpenAPICardStatusResponse struct {
|
||||
CardNo string `json:"card_no" description:"卡标识"`
|
||||
CardStatus string `json:"card_status" description:"卡状态 (normal:正常, stopped:停机)"`
|
||||
CardStatusName string `json:"card_status_name" description:"卡状态名称(中文)"`
|
||||
StopReason string `json:"stop_reason" description:"停机原因"`
|
||||
StopReasonName string `json:"stop_reason_name" description:"停机原因名称(中文)"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIRealnameStatusResponse 开放接口实名状态响应
|
||||
type AgentOpenAPIRealnameStatusResponse struct {
|
||||
CardNo string `json:"card_no" description:"卡标识"`
|
||||
IsRealnamed bool `json:"is_realnamed" description:"是否已实名"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIPackageListRequest 开放接口套餐列表请求
|
||||
type AgentOpenAPIPackageListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
PackageType *string `json:"package_type" query:"package_type" validate:"omitempty,oneof=formal addon" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
SeriesID *uint `json:"series_id" query:"series_id" validate:"omitempty" description:"套餐系列ID"`
|
||||
PackageName *string `json:"package_name" query:"package_name" validate:"omitempty,max=255" maxLength:"255" description:"套餐名称(模糊搜索)"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIPackageItem 开放接口套餐列表项
|
||||
type AgentOpenAPIPackageItem struct {
|
||||
PackageID uint `json:"package_id" description:"套餐ID"`
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
PackageName string `json:"package_name" description:"套餐名称"`
|
||||
SeriesName string `json:"series_name" description:"套餐系列名称"`
|
||||
PackageType string `json:"package_type" description:"套餐类型 (formal:正式套餐, addon:附加套餐)"`
|
||||
PackageTypeName string `json:"package_type_name" description:"套餐类型名称(中文)"`
|
||||
CostPrice int64 `json:"cost_price" description:"成本价(分)"`
|
||||
RetailPrice int64 `json:"retail_price" description:"生效零售价(分)"`
|
||||
TotalFlowMB int64 `json:"total_flow_mb" description:"总流量(MB)"`
|
||||
ValidDays int `json:"valid_days" description:"有效天数"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIWalletBalanceResponse 开放接口预充值钱包余额响应
|
||||
type AgentOpenAPIWalletBalanceResponse struct {
|
||||
Balance int64 `json:"balance" description:"钱包余额(分)"`
|
||||
FrozenBalance int64 `json:"frozen_balance" description:"冻结余额(分)"`
|
||||
AvailableBalance int64 `json:"available_balance" description:"可用余额(分)"`
|
||||
Currency string `json:"currency" description:"币种"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIWalletTransactionListRequest 开放接口预充值钱包流水请求
|
||||
type AgentOpenAPIWalletTransactionListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页数量"`
|
||||
TransactionType string `json:"transaction_type" query:"transaction_type" validate:"omitempty,max=50" maxLength:"50" description:"交易类型过滤(recharge:充值入账, deduct:套餐扣款, refund:退款)"`
|
||||
StartDate string `json:"start_date" query:"start_date" validate:"omitempty" description:"开始日期(YYYY-MM-DD)"`
|
||||
EndDate string `json:"end_date" query:"end_date" validate:"omitempty" description:"结束日期(YYYY-MM-DD)"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIWalletPackageOrderRequest 开放接口钱包套餐购买请求
|
||||
type AgentOpenAPIWalletPackageOrderRequest struct {
|
||||
CardNos []string `json:"card_nos" validate:"required,min=1,max=100,dive,min=1,max=100" required:"true" minItems:"1" maxItems:"100" description:"卡标识列表(支持 ICCID、虚拟号、MSISDN)"`
|
||||
PackageCode string `json:"package_code" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"套餐编码(一次只能购买一个套餐)"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIWalletPackageOrderItem 开放接口钱包套餐购买成功项
|
||||
type AgentOpenAPIWalletPackageOrderItem struct {
|
||||
CardNo string `json:"card_no" description:"卡标识"`
|
||||
OrderNo string `json:"order_no" description:"订单号"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIWalletPackageOrderFailure 开放接口钱包套餐购买失败项
|
||||
type AgentOpenAPIWalletPackageOrderFailure struct {
|
||||
CardNo string `json:"card_no" description:"卡标识"`
|
||||
Code int `json:"code" description:"错误码"`
|
||||
Message string `json:"message" description:"失败原因"`
|
||||
}
|
||||
|
||||
// AgentOpenAPIWalletPackageOrderResponse 开放接口钱包套餐购买响应
|
||||
type AgentOpenAPIWalletPackageOrderResponse struct {
|
||||
BatchNo string `json:"batch_no" description:"批次号"`
|
||||
PackageCode string `json:"package_code" description:"套餐编码"`
|
||||
SuccessCount int `json:"success_count" description:"成功数量"`
|
||||
FailedCount int `json:"failed_count" description:"失败数量"`
|
||||
Orders []AgentOpenAPIWalletPackageOrderItem `json:"orders" description:"成功订单列表"`
|
||||
FailedCards []AgentOpenAPIWalletPackageOrderFailure `json:"failed_cards" description:"失败卡列表"`
|
||||
}
|
||||
71
internal/routes/open.go
Normal file
71
internal/routes/open.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
openapiHandler "github.com/break/junhong_cmp_fiber/internal/handler/openapi"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
// RegisterOpenAPIRoutes 注册代理开放接口路由
|
||||
func RegisterOpenAPIRoutes(router fiber.Router, handler *openapiHandler.Handler, doc *openapi.Generator, basePath string) {
|
||||
const tag = "代理开放接口"
|
||||
const authDescription = "认证 Header:X-Agent-Account、X-Agent-Password、X-Agent-Timestamp、X-Agent-Nonce、X-Agent-Sign。签名算法为 hex(HMAC-SHA256(password, sign_payload))。所有启用代理账号默认可调用,不需要开放接口账号授权表。"
|
||||
|
||||
Register(router, doc, basePath, "GET", "/cards/traffic", handler.GetCardTraffic, RouteSpec{
|
||||
Summary: "查询单卡流量",
|
||||
Description: authDescription + "\n\n按 card_no 查询代理权限范围内单卡的当前生效套餐、待生效套餐和真实业务口径流量。不返回虚流量、倍率、停机阈值等内部字段。",
|
||||
Input: new(dto.AgentOpenAPICardQueryRequest),
|
||||
Output: new(dto.AgentOpenAPICardTrafficResponse),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
Register(router, doc, basePath, "GET", "/cards/status", handler.GetCardStatus, RouteSpec{
|
||||
Summary: "查询单卡状态",
|
||||
Description: authDescription + "\n\n按 card_no 查询代理权限范围内单卡状态,network_status=1 返回正常,network_status=0 返回停机和停机原因。",
|
||||
Input: new(dto.AgentOpenAPICardQueryRequest),
|
||||
Output: new(dto.AgentOpenAPICardStatusResponse),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
Register(router, doc, basePath, "GET", "/cards/realname-status", handler.GetRealnameStatus, RouteSpec{
|
||||
Summary: "查询单卡实名状态",
|
||||
Description: authDescription + "\n\n按 card_no 查询代理权限范围内单卡实名状态。",
|
||||
Input: new(dto.AgentOpenAPICardQueryRequest),
|
||||
Output: new(dto.AgentOpenAPIRealnameStatusResponse),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
Register(router, doc, basePath, "GET", "/packages", handler.ListPackages, RouteSpec{
|
||||
Summary: "查询代理套餐列表",
|
||||
Description: authDescription + "\n\n分页查询当前代理已分配、启用、上架且非赠送的可购买套餐,返回代理生效零售价。",
|
||||
Input: new(dto.AgentOpenAPIPackageListRequest),
|
||||
Output: new(dto.AgentOpenAPIPackageItem),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
Register(router, doc, basePath, "POST", "/wallet/package-orders", handler.CreateWalletPackageOrders, RouteSpec{
|
||||
Summary: "钱包套餐购买",
|
||||
Description: authDescription + "\n\n使用代理预充值主钱包为多张卡购买同一个套餐编码。每张卡独立下单,允许部分成功。",
|
||||
Input: new(dto.AgentOpenAPIWalletPackageOrderRequest),
|
||||
Output: new(dto.AgentOpenAPIWalletPackageOrderResponse),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
Register(router, doc, basePath, "GET", "/wallet/balance", handler.GetWalletBalance, RouteSpec{
|
||||
Summary: "查询预充值钱包余额",
|
||||
Description: authDescription + "\n\n查询当前代理预充值主钱包余额。主钱包不存在时返回 0。",
|
||||
Output: new(dto.AgentOpenAPIWalletBalanceResponse),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
Register(router, doc, basePath, "GET", "/wallet/transactions", handler.ListWalletTransactions, RouteSpec{
|
||||
Summary: "查询预充值钱包流水",
|
||||
Description: authDescription + "\n\n分页查询当前代理预充值主钱包流水,字段与后台主钱包流水接口保持一致。",
|
||||
Input: new(dto.AgentOpenAPIWalletTransactionListRequest),
|
||||
Output: new(dto.MainWalletTransactionItem),
|
||||
Tags: []string{tag},
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
@@ -32,7 +32,16 @@ func RegisterRoutesWithDoc(app *fiber.App, handlers *bootstrap.Handlers, middlew
|
||||
personalGroup := app.Group("/api/c/v1")
|
||||
RegisterPersonalCustomerRoutes(personalGroup, doc, "/api/c/v1", handlers, middlewares.PersonalAuth)
|
||||
|
||||
// 5. 支付回调路由 (挂载在 /api/callback,无需认证)
|
||||
// 5. 代理开放接口路由 (挂载在 /api/open/v1)
|
||||
if handlers.AgentOpenAPI != nil {
|
||||
openGroup := app.Group("/api/open/v1")
|
||||
if middlewares.AgentOpenAPIAuth != nil {
|
||||
openGroup = app.Group("/api/open/v1", middlewares.AgentOpenAPIAuth)
|
||||
}
|
||||
RegisterOpenAPIRoutes(openGroup, handlers.AgentOpenAPI, doc, "/api/open/v1")
|
||||
}
|
||||
|
||||
// 6. 支付回调路由 (挂载在 /api/callback,无需认证)
|
||||
if handlers.PaymentCallback != nil {
|
||||
callbackGroup := app.Group("/api/callback")
|
||||
registerPaymentCallbackRoutes(callbackGroup, handlers.PaymentCallback, doc, "/api/callback")
|
||||
|
||||
489
internal/service/agent_open_api/service.go
Normal file
489
internal/service/agent_open_api/service.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package agent_open_api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
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"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Service 代理开放接口业务编排服务
|
||||
type Service struct {
|
||||
assetService *assetSvc.Service
|
||||
packageService *packageSvc.Service
|
||||
orderService *orderSvc.Service
|
||||
shopCommissionService *shopCommissionSvc.Service
|
||||
iotCardStore *postgres.IotCardStore
|
||||
packageUsageStore *postgres.PackageUsageStore
|
||||
packageStore *postgres.PackageStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
agentWalletStore *postgres.AgentWalletStore
|
||||
}
|
||||
|
||||
// New 创建代理开放接口业务编排服务
|
||||
func New(
|
||||
assetService *assetSvc.Service,
|
||||
packageService *packageSvc.Service,
|
||||
orderService *orderSvc.Service,
|
||||
shopCommissionService *shopCommissionSvc.Service,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
packageUsageStore *postgres.PackageUsageStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
assetService: assetService,
|
||||
packageService: packageService,
|
||||
orderService: orderService,
|
||||
shopCommissionService: shopCommissionService,
|
||||
iotCardStore: iotCardStore,
|
||||
packageUsageStore: packageUsageStore,
|
||||
packageStore: packageStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
agentWalletStore: agentWalletStore,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCardTraffic 查询开放接口单卡流量
|
||||
func (s *Service) GetCardTraffic(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPICardTrafficResponse, error) {
|
||||
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activeStatus := constants.PackageUsageStatusActive
|
||||
pendingStatus := constants.PackageUsageStatusPending
|
||||
activeUsages, err := s.packageUsageStore.ListByCarrier(ctx, "iot_card", card.ID, &activeStatus)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询生效套餐失败")
|
||||
}
|
||||
pendingUsages, err := s.packageUsageStore.ListByCarrier(ctx, "iot_card", card.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.AgentOpenAPICardTrafficResponse{
|
||||
CardNo: req.CardNo,
|
||||
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
|
||||
}
|
||||
|
||||
// GetCardStatus 查询开放接口单卡状态
|
||||
func (s *Service) GetCardStatus(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPICardStatusResponse, error) {
|
||||
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cardStatus := constants.AgentOpenAPICardStatusStopped
|
||||
stopReason := card.StopReason
|
||||
if card.NetworkStatus == constants.NetworkStatusOnline {
|
||||
cardStatus = constants.AgentOpenAPICardStatusNormal
|
||||
stopReason = ""
|
||||
}
|
||||
|
||||
return &dto.AgentOpenAPICardStatusResponse{
|
||||
CardNo: req.CardNo,
|
||||
CardStatus: cardStatus,
|
||||
CardStatusName: constants.AgentOpenAPICardStatusName(cardStatus),
|
||||
StopReason: stopReason,
|
||||
StopReasonName: constants.AgentOpenAPIStopReasonName(stopReason),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRealnameStatus 查询开放接口单卡实名状态
|
||||
func (s *Service) GetRealnameStatus(ctx context.Context, req *dto.AgentOpenAPICardQueryRequest) (*dto.AgentOpenAPIRealnameStatusResponse, error) {
|
||||
card, err := s.resolveOpenAPICard(ctx, req.CardNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.AgentOpenAPIRealnameStatusResponse{
|
||||
CardNo: req.CardNo,
|
||||
IsRealnamed: card.RealNameStatus == constants.RealNameStatusVerified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListPackages 查询开放接口代理可购买套餐列表
|
||||
func (s *Service) ListPackages(ctx context.Context, req *dto.AgentOpenAPIPackageListRequest) ([]*dto.AgentOpenAPIPackageItem, int64, int, int, error) {
|
||||
status := constants.StatusEnabled
|
||||
shelfStatus := constants.ShelfStatusOn
|
||||
packageReq := &dto.PackageListRequest{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
PackageName: req.PackageName,
|
||||
SeriesID: req.SeriesID,
|
||||
Status: &status,
|
||||
ShelfStatus: &shelfStatus,
|
||||
PackageType: req.PackageType,
|
||||
}
|
||||
packages, total, err := s.packageService.List(ctx, packageReq)
|
||||
if err != nil {
|
||||
return nil, 0, 0, 0, err
|
||||
}
|
||||
|
||||
items := make([]*dto.AgentOpenAPIPackageItem, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
items = append(items, &dto.AgentOpenAPIPackageItem{
|
||||
PackageID: pkg.ID,
|
||||
PackageCode: pkg.PackageCode,
|
||||
PackageName: pkg.PackageName,
|
||||
SeriesName: stringValue(pkg.SeriesName),
|
||||
PackageType: pkg.PackageType,
|
||||
PackageTypeName: constants.AgentOpenAPIPackageTypeName(pkg.PackageType),
|
||||
CostPrice: pkg.CostPrice,
|
||||
RetailPrice: int64Value(pkg.EffectiveRetailPrice),
|
||||
TotalFlowMB: pkg.RealDataMB,
|
||||
ValidDays: packageValidDays(pkg.DurationDays, pkg.DurationMonths),
|
||||
})
|
||||
}
|
||||
|
||||
page, pageSize := normalizePage(req.Page, req.PageSize)
|
||||
return items, total, page, pageSize, nil
|
||||
}
|
||||
|
||||
// GetWalletBalance 查询开放接口预充值主钱包余额
|
||||
func (s *Service) GetWalletBalance(ctx context.Context) (*dto.AgentOpenAPIWalletBalanceResponse, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, shopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return &dto.AgentOpenAPIWalletBalanceResponse{
|
||||
Balance: 0,
|
||||
FrozenBalance: 0,
|
||||
AvailableBalance: 0,
|
||||
Currency: constants.AgentOpenAPIWalletCurrencyCNY,
|
||||
}, nil
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主钱包余额失败")
|
||||
}
|
||||
|
||||
return &dto.AgentOpenAPIWalletBalanceResponse{
|
||||
Balance: wallet.Balance,
|
||||
FrozenBalance: wallet.FrozenBalance,
|
||||
AvailableBalance: wallet.GetAvailableBalance(),
|
||||
Currency: walletCurrency(wallet.Currency),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListWalletTransactions 查询开放接口预充值主钱包流水
|
||||
func (s *Service) ListWalletTransactions(ctx context.Context, req *dto.AgentOpenAPIWalletTransactionListRequest) (*dto.MainWalletTransactionListResponse, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
return s.shopCommissionService.ListMainWalletTransactions(ctx, shopID, &dto.MainWalletTransactionListRequest{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TransactionType: req.TransactionType,
|
||||
StartDate: req.StartDate,
|
||||
EndDate: req.EndDate,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateWalletPackageOrders 使用代理预充值钱包按卡购买套餐
|
||||
func (s *Service) CreateWalletPackageOrders(ctx context.Context, req *dto.AgentOpenAPIWalletPackageOrderRequest) (*dto.AgentOpenAPIWalletPackageOrderResponse, error) {
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
if shopID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized)
|
||||
}
|
||||
|
||||
pkg, err := s.packageStore.GetByCode(ctx, strings.TrimSpace(req.PackageCode))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "套餐不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询套餐失败")
|
||||
}
|
||||
|
||||
resp := &dto.AgentOpenAPIWalletPackageOrderResponse{
|
||||
BatchNo: generateAgentOpenAPIBatchNo(),
|
||||
PackageCode: pkg.PackageCode,
|
||||
Orders: []dto.AgentOpenAPIWalletPackageOrderItem{},
|
||||
FailedCards: []dto.AgentOpenAPIWalletPackageOrderFailure{},
|
||||
}
|
||||
|
||||
for _, rawCardNo := range req.CardNos {
|
||||
cardNo := strings.TrimSpace(rawCardNo)
|
||||
if cardNo == "" {
|
||||
resp.FailedCards = append(resp.FailedCards, buildFailedCard(rawCardNo, errors.New(errors.CodeInvalidParam, "卡标识不能为空")))
|
||||
continue
|
||||
}
|
||||
card, err := s.resolveOpenAPICard(ctx, cardNo)
|
||||
if err != nil {
|
||||
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, err))
|
||||
continue
|
||||
}
|
||||
|
||||
orderResp, err := s.orderService.CreateAdminOrder(ctx, &dto.CreateAdminOrderRequest{
|
||||
Identifier: card.ICCID,
|
||||
PackageIDs: []uint{pkg.ID},
|
||||
PaymentMethod: model.PaymentMethodWallet,
|
||||
}, model.BuyerTypeAgent, shopID)
|
||||
if err != nil {
|
||||
resp.FailedCards = append(resp.FailedCards, buildFailedCard(cardNo, err))
|
||||
continue
|
||||
}
|
||||
|
||||
resp.Orders = append(resp.Orders, dto.AgentOpenAPIWalletPackageOrderItem{
|
||||
CardNo: cardNo,
|
||||
OrderNo: orderResp.OrderNo,
|
||||
})
|
||||
}
|
||||
|
||||
resp.SuccessCount = len(resp.Orders)
|
||||
resp.FailedCount = len(resp.FailedCards)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// resolveOpenAPICard 将开放接口卡标识解析为当前代理可见的 IoT 卡
|
||||
func (s *Service) resolveOpenAPICard(ctx context.Context, cardNo string) (*model.IotCard, error) {
|
||||
identifier := strings.TrimSpace(cardNo)
|
||||
if identifier == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if s.assetService == nil || s.iotCardStore == nil {
|
||||
return nil, errors.New(errors.CodeInternalError, "开放接口资产解析依赖未配置")
|
||||
}
|
||||
|
||||
resolved, err := s.assetService.Resolve(ctx, identifier)
|
||||
if err != nil {
|
||||
if appErr, ok := err.(*errors.AppError); ok && appErr.Code == errors.CodeNotFound {
|
||||
return s.resolveOpenAPICardByIdentifier(ctx, identifier)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if resolved.AssetType != "card" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "开放接口只支持单卡标识")
|
||||
}
|
||||
|
||||
card, err := s.iotCardStore.GetByID(ctx, resolved.AssetID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
// resolveOpenAPICardByIdentifier 兜底支持 ICCID 双列、虚拟号和 MSISDN 精确解析
|
||||
func (s *Service) resolveOpenAPICardByIdentifier(ctx context.Context, identifier string) (*model.IotCard, error) {
|
||||
card, err := s.iotCardStore.GetByIdentifier(ctx, identifier)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询卡信息失败")
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
// loadUsagePackageContext 批量加载套餐和系列信息,避免单卡流量响应出现 N+1 查询
|
||||
func (s *Service) loadUsagePackageContext(ctx context.Context, usages []*model.PackageUsage) (map[uint]*model.Package, map[uint]string, error) {
|
||||
packageIDs := make([]uint, 0)
|
||||
packageIDSeen := make(map[uint]struct{})
|
||||
for _, usage := range usages {
|
||||
if usage.PackageID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := packageIDSeen[usage.PackageID]; !ok {
|
||||
packageIDSeen[usage.PackageID] = struct{}{}
|
||||
packageIDs = append(packageIDs, usage.PackageID)
|
||||
}
|
||||
}
|
||||
|
||||
packages, err := s.packageStore.GetByIDsUnscoped(ctx, packageIDs)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐失败")
|
||||
}
|
||||
packageMap := make(map[uint]*model.Package, len(packages))
|
||||
seriesIDSeen := make(map[uint]struct{})
|
||||
seriesIDs := make([]uint, 0)
|
||||
for _, pkg := range packages {
|
||||
packageMap[pkg.ID] = pkg
|
||||
if pkg.SeriesID > 0 {
|
||||
if _, ok := seriesIDSeen[pkg.SeriesID]; !ok {
|
||||
seriesIDSeen[pkg.SeriesID] = struct{}{}
|
||||
seriesIDs = append(seriesIDs, pkg.SeriesID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seriesList, err := s.packageSeriesStore.GetByIDs(ctx, seriesIDs)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeInternalError, err, "批量查询套餐系列失败")
|
||||
}
|
||||
seriesMap := make(map[uint]string, len(seriesList))
|
||||
for _, series := range seriesList {
|
||||
seriesMap[series.ID] = series.SeriesName
|
||||
}
|
||||
|
||||
return packageMap, seriesMap, nil
|
||||
}
|
||||
|
||||
// buildTrafficItem 按开放接口口径构造套餐流量项,主动隐藏虚流量内部字段
|
||||
func (s *Service) buildTrafficItem(usage *model.PackageUsage, packageMap map[uint]*model.Package, seriesMap map[uint]string, active bool) dto.AgentOpenAPIPackageTrafficItem {
|
||||
pkg := packageMap[usage.PackageID]
|
||||
packageCode := ""
|
||||
packageName := usage.PackageName
|
||||
packageType := ""
|
||||
validDays := 0
|
||||
seriesName := ""
|
||||
if pkg != nil {
|
||||
packageCode = pkg.PackageCode
|
||||
if packageName == "" {
|
||||
packageName = pkg.PackageName
|
||||
}
|
||||
packageType = pkg.PackageType
|
||||
validDays = packageValidDays(nil, pkg.DurationMonths)
|
||||
if pkg.DurationDays > 0 {
|
||||
validDays = pkg.DurationDays
|
||||
}
|
||||
seriesName = seriesMap[pkg.SeriesID]
|
||||
}
|
||||
|
||||
usedFlow := int64(0)
|
||||
remainingFlow := int64(0)
|
||||
if active {
|
||||
usedFlow = visibleUsedFlowMB(usage)
|
||||
remainingFlow = usage.DataLimitMB - usedFlow
|
||||
if remainingFlow < 0 {
|
||||
remainingFlow = 0
|
||||
}
|
||||
}
|
||||
|
||||
return dto.AgentOpenAPIPackageTrafficItem{
|
||||
PackageCode: packageCode,
|
||||
PackageName: packageName,
|
||||
SeriesName: seriesName,
|
||||
PackageType: packageType,
|
||||
PackageTypeName: constants.AgentOpenAPIPackageTypeName(packageType),
|
||||
TotalFlowMB: usage.DataLimitMB,
|
||||
UsedFlowMB: usedFlow,
|
||||
RemainingFlowMB: remainingFlow,
|
||||
StartAt: usage.ActivatedAt,
|
||||
ExpiresAt: usage.ExpiresAt,
|
||||
ValidDays: validDays,
|
||||
Priority: usage.Priority,
|
||||
}
|
||||
}
|
||||
|
||||
// visibleUsedFlowMB 计算对代理展示的已用流量,并限制不超过真实总量
|
||||
func visibleUsedFlowMB(usage *model.PackageUsage) int64 {
|
||||
ratio := usage.DisplayGainRatioSnapshot
|
||||
if ratio <= 0 {
|
||||
ratio = 1
|
||||
}
|
||||
used := int64(float64(usage.DataUsageMB) * ratio)
|
||||
if used > usage.DataLimitMB {
|
||||
return usage.DataLimitMB
|
||||
}
|
||||
if used < 0 {
|
||||
return 0
|
||||
}
|
||||
return used
|
||||
}
|
||||
|
||||
// packageValidDays 将套餐有效期统一折算为开放接口展示天数
|
||||
func packageValidDays(durationDays *int, durationMonths int) int {
|
||||
if durationDays != nil && *durationDays > 0 {
|
||||
return *durationDays
|
||||
}
|
||||
if durationMonths > 0 {
|
||||
return durationMonths * 30
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func normalizePage(page, pageSize int) (int, int) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
if pageSize > constants.MaxPageSize {
|
||||
pageSize = constants.MaxPageSize
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func stringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func int64Value(value *int64) int64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func walletCurrency(currency string) string {
|
||||
if currency == "" {
|
||||
return constants.AgentOpenAPIWalletCurrencyCNY
|
||||
}
|
||||
return currency
|
||||
}
|
||||
|
||||
func buildFailedCard(cardNo string, err error) dto.AgentOpenAPIWalletPackageOrderFailure {
|
||||
code := errors.CodeInternalError
|
||||
message := errors.GetMessage(code, "zh-CN")
|
||||
if appErr, ok := err.(*errors.AppError); ok {
|
||||
code = appErr.Code
|
||||
message = appErr.Message
|
||||
}
|
||||
return dto.AgentOpenAPIWalletPackageOrderFailure{
|
||||
CardNo: cardNo,
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func generateAgentOpenAPIBatchNo() string {
|
||||
return fmt.Sprintf("%s%s%06d", constants.AgentOpenAPIBatchNoPrefix, time.Now().Format("20060102150405"), rand.Intn(1000000))
|
||||
}
|
||||
@@ -1241,8 +1241,15 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else {
|
||||
networkStatus := parseNetworkStatus(statusResp.CardStatus)
|
||||
updates["network_status"] = networkStatus
|
||||
networkStatus, ok := gateway.ParseCardNetworkStatus(statusResp.CardStatus, statusResp.Extend)
|
||||
if !ok {
|
||||
s.logger.Warn("刷新卡数据:未知 Gateway 卡状态",
|
||||
zap.String("iccid", iccid),
|
||||
zap.String("card_status", statusResp.CardStatus),
|
||||
zap.String("extend", statusResp.Extend))
|
||||
} else {
|
||||
updates["network_status"] = networkStatus
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 查询实名状态
|
||||
@@ -1267,7 +1274,7 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
if err != nil {
|
||||
s.logger.Warn("刷新卡数据:查询流量失败", zap.String("iccid", iccid), zap.Error(err))
|
||||
} else {
|
||||
flowIncrementMB = s.calculateRefreshFlowUpdates(card, flowResp.Used, syncTime, updates)
|
||||
flowIncrementMB = s.calculateRefreshFlowUpdates(card, float64(flowResp.Used), syncTime, updates)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,15 +1392,6 @@ func isRefreshResetWindow(now time.Time, resetDay int) bool {
|
||||
return today == prevDay
|
||||
}
|
||||
|
||||
// parseNetworkStatus 将网关返回的卡状态字符串转换为 network_status 数值
|
||||
// 停机→0,其他(准备/正常)→1
|
||||
func parseNetworkStatus(cardStatus string) int {
|
||||
if cardStatus == "停机" {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值
|
||||
// true=已实名(1),false=未实名(0)
|
||||
func parseGatewayRealnameStatus(realStatus bool) int {
|
||||
|
||||
@@ -2064,6 +2064,12 @@ func (s *Service) resolveAssetByIdentifier(ctx context.Context, identifier strin
|
||||
}
|
||||
}
|
||||
}
|
||||
if card, cardErr := s.iotCardStore.GetByIdentifier(ctx, identifier); cardErr == nil && card != nil {
|
||||
return card, nil, nil
|
||||
}
|
||||
if device, deviceErr := s.deviceStore.GetByIdentifier(ctx, identifier); deviceErr == nil && device != nil {
|
||||
return nil, device, nil
|
||||
}
|
||||
return nil, nil, errors.New(errors.CodeNotFound, "未找到对应资产,请使用 ICCID 或虚拟号")
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,24 @@ func (s *IotCardStore) GetByICCID(ctx context.Context, iccid string) (*model.Iot
|
||||
return &card, err
|
||||
}
|
||||
|
||||
// GetByIdentifier 通过 ICCID、虚拟号或 MSISDN 精确查询 IoT 卡
|
||||
func (s *IotCardStore) GetByIdentifier(ctx context.Context, identifier string) (*model.IotCard, error) {
|
||||
var card model.IotCard
|
||||
query := s.db.WithContext(ctx).Where(
|
||||
"virtual_no = ? OR iccid = ? OR msisdn = ? OR iccid_19 = ? OR iccid_20 = ?",
|
||||
identifier,
|
||||
identifier,
|
||||
identifier,
|
||||
identifier,
|
||||
identifier,
|
||||
)
|
||||
query = middleware.ApplyShopFilter(ctx, query)
|
||||
if err := query.First(&card).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &card, nil
|
||||
}
|
||||
|
||||
func (s *IotCardStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.IotCard, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*model.IotCard{}, nil
|
||||
|
||||
@@ -81,7 +81,7 @@ func (h *PollingCarddataHandler) Handle(ctx context.Context, t *asynq.Task) erro
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, false, time.Since(startTime))
|
||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
||||
}
|
||||
gatewayFlowMB = result.Used
|
||||
gatewayFlowMB = float64(result.Used)
|
||||
} else {
|
||||
gatewayFlowMB = card.CurrentMonthUsageMB
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// PollingCardStatusHandler 卡开停机状态轮询任务处理器
|
||||
// 职责:调 Gateway 查卡状态(准备/正常/停机)→ 映射为 network_status → 写 DB → 触发停复机评估
|
||||
// 职责:调 Gateway 查卡状态(正常/停机/准备)→ 映射为 network_status → 写 DB → 触发停复机评估
|
||||
// 不做:直接停复机决策,委托给 StopResumeService.EvaluateAndAct
|
||||
type PollingCardStatusHandler struct {
|
||||
base *PollingBase
|
||||
@@ -63,7 +63,8 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, t *asynq.Task) er
|
||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)
|
||||
}
|
||||
|
||||
var newNetworkStatus int
|
||||
newNetworkStatus := card.NetworkStatus
|
||||
gatewayCardStatus := ""
|
||||
if h.gateway != nil {
|
||||
result, gwErr := h.gateway.QueryCardStatus(ctx, &gateway.CardStatusReq{CardNo: card.ICCID})
|
||||
if gwErr != nil {
|
||||
@@ -72,22 +73,26 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, t *asynq.Task) er
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingCardStatus, false, time.Since(startTime))
|
||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)
|
||||
}
|
||||
// "停机" → 0(停机),"准备"/"正常" → 1(开机)
|
||||
if result.CardStatus == "停机" {
|
||||
newNetworkStatus = constants.NetworkStatusOffline
|
||||
gatewayCardStatus = result.CardStatus
|
||||
parsedStatus, ok := gateway.ParseCardNetworkStatus(result.CardStatus, result.Extend)
|
||||
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))
|
||||
} else {
|
||||
newNetworkStatus = constants.NetworkStatusOnline
|
||||
newNetworkStatus = parsedStatus
|
||||
}
|
||||
if h.base.verboseLog {
|
||||
h.base.logger.Info("卡状态轮询详情",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.String("iccid", card.ICCID),
|
||||
zap.String("card_status", result.CardStatus),
|
||||
zap.String("extend", result.Extend),
|
||||
zap.Int("new_network_status", newNetworkStatus),
|
||||
zap.Bool("changed", newNetworkStatus != card.NetworkStatus))
|
||||
}
|
||||
} else {
|
||||
newNetworkStatus = card.NetworkStatus
|
||||
}
|
||||
|
||||
statusChanged := newNetworkStatus != card.NetworkStatus
|
||||
@@ -98,7 +103,9 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, t *asynq.Task) er
|
||||
if statusChanged {
|
||||
fields["network_status"] = newNetworkStatus
|
||||
// 网关侧停机事件:补写 stop_reason,便于状态追踪和区分手动停机
|
||||
if newNetworkStatus == constants.NetworkStatusOffline && card.StopReason == "" {
|
||||
if newNetworkStatus == constants.NetworkStatusOffline &&
|
||||
card.StopReason == "" &&
|
||||
gateway.IsGatewayCardStopped(gatewayCardStatus) {
|
||||
fields["stop_reason"] = constants.StopReasonCarrierStopped
|
||||
}
|
||||
}
|
||||
|
||||
73
pkg/constants/agent_open_api.go
Normal file
73
pkg/constants/agent_open_api.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// 代理开放接口请求头名称
|
||||
const (
|
||||
AgentOpenAPIHeaderAccount = "X-Agent-Account" // 开放接口账号请求头
|
||||
AgentOpenAPIHeaderPassword = "X-Agent-Password" // 开放接口密码请求头
|
||||
AgentOpenAPIHeaderTimestamp = "X-Agent-Timestamp" // 开放接口时间戳请求头
|
||||
AgentOpenAPIHeaderNonce = "X-Agent-Nonce" // 开放接口随机串请求头
|
||||
AgentOpenAPIHeaderSign = "X-Agent-Sign" // 开放接口签名请求头
|
||||
)
|
||||
|
||||
// 代理开放接口签名配置
|
||||
const (
|
||||
AgentOpenAPISignatureWindow = 5 * time.Minute // 签名允许时间窗
|
||||
AgentOpenAPIBatchNoPrefix = "AOP" // 开放接口批次号前缀
|
||||
)
|
||||
|
||||
// 代理开放接口卡状态
|
||||
const (
|
||||
AgentOpenAPICardStatusNormal = "normal" // 正常
|
||||
AgentOpenAPICardStatusStopped = "stopped" // 停机
|
||||
AgentOpenAPIWalletCurrencyCNY = "CNY" // 人民币币种
|
||||
)
|
||||
|
||||
// AgentOpenAPICardStatusName 返回开放接口卡状态名称
|
||||
func AgentOpenAPICardStatusName(status string) string {
|
||||
switch status {
|
||||
case AgentOpenAPICardStatusNormal:
|
||||
return "正常"
|
||||
case AgentOpenAPICardStatusStopped:
|
||||
return "停机"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// AgentOpenAPIPackageTypeName 返回开放接口套餐类型名称
|
||||
func AgentOpenAPIPackageTypeName(packageType string) string {
|
||||
switch packageType {
|
||||
case PackageTypeFormal:
|
||||
return "正式套餐"
|
||||
case PackageTypeAddon:
|
||||
return "附加套餐"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// AgentOpenAPIStopReasonName 返回开放接口停机原因名称
|
||||
func AgentOpenAPIStopReasonName(reason string) string {
|
||||
switch reason {
|
||||
case StopReasonTrafficExhausted:
|
||||
return "流量耗尽"
|
||||
case StopReasonManual:
|
||||
return "手动停机"
|
||||
case StopReasonArrears:
|
||||
return "欠费"
|
||||
case StopReasonProtectPeriod:
|
||||
return "保护期自动停机"
|
||||
case StopReasonNoPackage:
|
||||
return "无有效套餐"
|
||||
case StopReasonNotRealname:
|
||||
return "未完成实名认证"
|
||||
case StopReasonCarrierStopped:
|
||||
return "运营商侧停机"
|
||||
case "":
|
||||
return ""
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,14 @@ const (
|
||||
NetworkStatusOnline = 1 // 开机
|
||||
)
|
||||
|
||||
// Gateway 卡状态
|
||||
const (
|
||||
GatewayCardStatusReady = "准备" // 网关卡状态:准备
|
||||
GatewayCardStatusNormal = "正常" // 网关卡状态:正常
|
||||
GatewayCardStatusStopped = "停机" // 网关卡状态:停机
|
||||
GatewayCardExtendPendingActivation = "待激活" // 网关扩展状态:待激活
|
||||
)
|
||||
|
||||
// IoT 卡停机原因
|
||||
const (
|
||||
StopReasonTrafficExhausted = "traffic_exhausted" // 流量耗尽
|
||||
|
||||
@@ -13,6 +13,13 @@ func RedisAuthTokenKey(token string) string {
|
||||
return fmt.Sprintf("auth:token:%s", token)
|
||||
}
|
||||
|
||||
// RedisAgentOpenAPINonceKey 生成代理开放接口 nonce 防重放 Redis 键
|
||||
// 用途:记录账号在签名时间窗内已使用的随机串,防止重复请求
|
||||
// 过期时间:与开放接口签名时间窗一致
|
||||
func RedisAgentOpenAPINonceKey(account, nonce string) string {
|
||||
return fmt.Sprintf("agent_open_api:nonce:%s:%s", account, nonce)
|
||||
}
|
||||
|
||||
// RedisRefreshTokenKey 生成刷新令牌的 Redis 键
|
||||
// 用途:存储用户 refresh token 信息
|
||||
// 过期时间:7 天(可配置)
|
||||
|
||||
@@ -48,6 +48,8 @@ const (
|
||||
CodeWechatUserInfoFailed = 1045 // 获取微信用户信息失败
|
||||
CodeWechatPayFailed = 1046 // 微信支付发起失败
|
||||
CodeWechatCallbackInvalid = 1047 // 微信回调签名验证失败
|
||||
CodeOpenAPIAuthFailed = 1048 // 开放接口认证失败
|
||||
CodeOpenAPISignInvalid = 1049 // 开放接口签名无效
|
||||
|
||||
// 组织相关错误 (1030-1049)
|
||||
CodeShopNotFound = 1030 // 店铺不存在
|
||||
@@ -152,6 +154,8 @@ const (
|
||||
CodeNeedRealname = 1187 // 该套餐需实名认证后购买
|
||||
CodeOpenIDNotFound = 1188 // 未找到微信授权信息,请先完成授权
|
||||
CodeRealnameNotAvailable = 1189 // 请先完成充值或购买套餐后再进行实名认证
|
||||
CodeOpenAPITimestampInvalid = 1190 // 开放接口时间戳无效
|
||||
CodeOpenAPINonceReplay = 1191 // 开放接口 nonce 重放
|
||||
|
||||
// 换货相关错误 (1200-1209)
|
||||
CodeExchangeOrderNotFound = 1200 // 换货单不存在
|
||||
@@ -219,6 +223,8 @@ var allErrorCodes = []int{
|
||||
CodeWechatUserInfoFailed,
|
||||
CodeWechatPayFailed,
|
||||
CodeWechatCallbackInvalid,
|
||||
CodeOpenAPIAuthFailed,
|
||||
CodeOpenAPISignInvalid,
|
||||
CodeInvalidStatus,
|
||||
CodeInsufficientBalance,
|
||||
CodeWithdrawalNotFound,
|
||||
@@ -289,6 +295,8 @@ var allErrorCodes = []int{
|
||||
CodeNeedRealname,
|
||||
CodeOpenIDNotFound,
|
||||
CodeRealnameNotAvailable,
|
||||
CodeOpenAPITimestampInvalid,
|
||||
CodeOpenAPINonceReplay,
|
||||
CodeExchangeOrderNotFound,
|
||||
CodeExchangeInProgress,
|
||||
CodeExchangeStatusInvalid,
|
||||
@@ -436,6 +444,10 @@ var errorMessages = map[int]string{
|
||||
CodeWechatUserInfoFailed: "获取微信用户信息失败",
|
||||
CodeWechatPayFailed: "微信支付发起失败",
|
||||
CodeWechatCallbackInvalid: "微信回调验证失败",
|
||||
CodeOpenAPIAuthFailed: "开放接口认证失败",
|
||||
CodeOpenAPISignInvalid: "开放接口签名无效",
|
||||
CodeOpenAPITimestampInvalid: "开放接口时间戳无效",
|
||||
CodeOpenAPINonceReplay: "重复请求,请勿重放",
|
||||
CodeInternalError: "内部服务器错误",
|
||||
CodeDatabaseError: "数据库错误",
|
||||
CodeRedisError: "缓存服务错误",
|
||||
@@ -464,7 +476,7 @@ func GetHTTPStatus(code int) int {
|
||||
return 200 // OK
|
||||
case CodeInvalidParam, CodeRequestTooLarge:
|
||||
return 400 // Bad Request
|
||||
case CodeMissingToken, CodeInvalidToken, CodeUnauthorized:
|
||||
case CodeMissingToken, CodeInvalidToken, CodeUnauthorized, CodeOpenAPIAuthFailed, CodeOpenAPISignInvalid, CodeOpenAPITimestampInvalid:
|
||||
return 401 // Unauthorized
|
||||
case CodeForbidden:
|
||||
return 403 // Forbidden
|
||||
@@ -478,7 +490,8 @@ func GetHTTPStatus(code int) int {
|
||||
CodeShopCodeExists,
|
||||
CodeEnterpriseCodeExists,
|
||||
CodeCustomerPhoneExists,
|
||||
CodeCarrierCodeExists:
|
||||
CodeCarrierCodeExists,
|
||||
CodeOpenAPINonceReplay:
|
||||
return 409 // Conflict
|
||||
case CodeTooManyRequests:
|
||||
return 429 // Too Many Requests
|
||||
|
||||
@@ -2,9 +2,12 @@ package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -28,6 +31,89 @@ func truncateBody(body []byte, maxSize int) string {
|
||||
return string(body[:maxSize]) + "... (truncated)"
|
||||
}
|
||||
|
||||
// maskSensitiveValue 按字段名判断并脱敏访问日志中的敏感值
|
||||
func maskSensitiveValue(key, value string) string {
|
||||
if value == "" {
|
||||
return value
|
||||
}
|
||||
normalized := strings.ToLower(key)
|
||||
if strings.Contains(normalized, "password") ||
|
||||
strings.Contains(normalized, "sign") ||
|
||||
strings.Contains(normalized, "nonce") ||
|
||||
strings.Contains(normalized, "token") ||
|
||||
strings.Contains(normalized, "secret") {
|
||||
return "***"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// sanitizeQuery 脱敏 query 中的密码、签名、nonce、token 等字段
|
||||
func sanitizeQuery(rawQuery string) string {
|
||||
if rawQuery == "" {
|
||||
return ""
|
||||
}
|
||||
values, err := url.ParseQuery(rawQuery)
|
||||
if err != nil {
|
||||
return rawQuery
|
||||
}
|
||||
for key, items := range values {
|
||||
for index, item := range items {
|
||||
items[index] = maskSensitiveValue(key, item)
|
||||
}
|
||||
values[key] = items
|
||||
}
|
||||
return values.Encode()
|
||||
}
|
||||
|
||||
// sanitizeBody 脱敏 JSON 请求体后再写入访问日志
|
||||
func sanitizeBody(rawBody []byte) string {
|
||||
if len(rawBody) == 0 {
|
||||
return ""
|
||||
}
|
||||
var payload any
|
||||
if err := sonic.Unmarshal(rawBody, &payload); err != nil {
|
||||
return truncateBody(rawBody, MaxBodyLogSize)
|
||||
}
|
||||
sanitizeJSONValue(payload)
|
||||
data, err := sonic.Marshal(payload)
|
||||
if err != nil {
|
||||
return truncateBody(rawBody, MaxBodyLogSize)
|
||||
}
|
||||
return truncateBody(data, MaxBodyLogSize)
|
||||
}
|
||||
|
||||
// sanitizeJSONValue 递归脱敏 JSON 对象中的敏感字段
|
||||
func sanitizeJSONValue(value any) {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range typed {
|
||||
if masked, ok := item.(string); ok {
|
||||
typed[key] = maskSensitiveValue(key, masked)
|
||||
continue
|
||||
}
|
||||
if shouldMaskField(key) {
|
||||
typed[key] = "***"
|
||||
continue
|
||||
}
|
||||
sanitizeJSONValue(item)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
sanitizeJSONValue(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shouldMaskField 判断字段名是否属于访问日志敏感字段
|
||||
func shouldMaskField(key string) bool {
|
||||
normalized := strings.ToLower(key)
|
||||
return strings.Contains(normalized, "password") ||
|
||||
strings.Contains(normalized, "sign") ||
|
||||
strings.Contains(normalized, "nonce") ||
|
||||
strings.Contains(normalized, "token") ||
|
||||
strings.Contains(normalized, "secret")
|
||||
}
|
||||
|
||||
// Middleware 创建 Fiber 日志中间件
|
||||
// 记录所有 HTTP 请求到访问日志(包括请求和响应 body)
|
||||
func Middleware() fiber.Handler {
|
||||
@@ -50,10 +136,10 @@ func Middleware() fiber.Handler {
|
||||
c.SetUserContext(ctx)
|
||||
|
||||
// 获取请求 body(在 c.Next() 之前读取)
|
||||
requestBody := truncateBody(c.Body(), MaxBodyLogSize)
|
||||
requestBody := sanitizeBody(c.Body())
|
||||
|
||||
// 获取 query 参数
|
||||
queryParams := string(c.Request().URI().QueryString())
|
||||
queryParams := sanitizeQuery(string(c.Request().URI().QueryString()))
|
||||
|
||||
// 处理请求
|
||||
err := c.Next()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||
openapiHandler "github.com/break/junhong_cmp_fiber/internal/handler/openapi"
|
||||
)
|
||||
|
||||
// BuildDocHandlers 构造文档生成用的 handlers(所有依赖传 nil)
|
||||
@@ -65,5 +66,6 @@ func BuildDocHandlers() *bootstrap.Handlers {
|
||||
Refund: admin.NewRefundHandler(nil),
|
||||
ClientWechat: app.NewClientWechatHandler(nil, nil, nil),
|
||||
SuperAdmin: admin.NewSuperAdminHandler(nil),
|
||||
AgentOpenAPI: openapiHandler.NewHandler(nil, nil),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user