开放接口,修复上游同步不对的问题
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()
|
app := fiber.New()
|
||||||
|
|
||||||
// 3. 创建所有 Handler(使用 nil 依赖,因为只需要路由结构)
|
// 3. 创建所有 Handler(使用 nil 依赖,因为只需要路由结构)
|
||||||
|
// 新增 Handler 必须注册到 openapi.BuildDocHandlers,代理开放接口也从该入口进入文档生成器。
|
||||||
handlers := openapi.BuildDocHandlers()
|
handlers := openapi.BuildDocHandlers()
|
||||||
|
|
||||||
// 4. 注册所有路由到文档生成器
|
// 4. 注册所有路由到文档生成器
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func generateAdminDocs(outputPath string) error {
|
|||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
// 3. 创建所有 Handler(使用 nil 依赖,因为只需要路由结构)
|
// 3. 创建所有 Handler(使用 nil 依赖,因为只需要路由结构)
|
||||||
|
// 新增 Handler 必须注册到 openapi.BuildDocHandlers,代理开放接口也从该入口进入文档生成器。
|
||||||
handlers := openapi.BuildDocHandlers()
|
handlers := openapi.BuildDocHandlers()
|
||||||
|
|
||||||
// 4. 注册所有路由到文档生成器
|
// 4. 注册所有路由到文档生成器
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
"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"
|
pollingPkg "github.com/break/junhong_cmp_fiber/internal/polling"
|
||||||
clientOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/client_order"
|
clientOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/client_order"
|
||||||
pollingSvcPkg "github.com/break/junhong_cmp_fiber/internal/service/polling"
|
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),
|
Refund: admin.NewRefundHandler(svc.Refund),
|
||||||
ClientWechat: app.NewClientWechatHandler(svc.WechatConfig, deps.Redis, deps.Logger),
|
ClientWechat: app.NewClientWechatHandler(svc.WechatConfig, deps.Redis, deps.Logger),
|
||||||
SuperAdmin: admin.NewSuperAdminHandler(svc.OperationPassword),
|
SuperAdmin: admin.NewSuperAdminHandler(svc.OperationPassword),
|
||||||
|
AgentOpenAPI: openapiHandler.NewHandler(svc.AgentOpenAPI, validate),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,16 @@ func initMiddlewares(deps *Dependencies, stores *stores) *Middlewares {
|
|||||||
// 创建后台认证中间件(传入 ShopStore 以支持预计算下级店铺 ID)
|
// 创建后台认证中间件(传入 ShopStore 以支持预计算下级店铺 ID)
|
||||||
adminAuthMiddleware := createAdminAuthMiddleware(tokenManager, stores.Shop)
|
adminAuthMiddleware := createAdminAuthMiddleware(tokenManager, stores.Shop)
|
||||||
|
|
||||||
|
agentOpenAPIAuthMiddleware := middleware.NewAgentOpenAPIAuthMiddleware(middleware.AgentOpenAPIAuthConfig{
|
||||||
|
AccountStore: stores.Account,
|
||||||
|
ShopStore: stores.Shop,
|
||||||
|
Redis: deps.Redis,
|
||||||
|
})
|
||||||
|
|
||||||
return &Middlewares{
|
return &Middlewares{
|
||||||
PersonalAuth: personalAuthMiddleware,
|
PersonalAuth: personalAuthMiddleware,
|
||||||
AdminAuth: adminAuthMiddleware,
|
AdminAuth: adminAuthMiddleware,
|
||||||
|
AgentOpenAPIAuth: agentOpenAPIAuthMiddleware,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/polling"
|
"github.com/break/junhong_cmp_fiber/internal/polling"
|
||||||
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
|
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||||
accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
|
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"
|
assetAllocationRecordSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_allocation_record"
|
||||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||||
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
authSvc "github.com/break/junhong_cmp_fiber/internal/service/auth"
|
||||||
@@ -107,6 +108,7 @@ type services struct {
|
|||||||
Refund *refundSvc.Service
|
Refund *refundSvc.Service
|
||||||
TrafficQuery *trafficSvc.QueryService
|
TrafficQuery *trafficSvc.QueryService
|
||||||
OperationPassword *operationPasswordSvc.Service
|
OperationPassword *operationPasswordSvc.Service
|
||||||
|
AgentOpenAPI *agentOpenAPISvc.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func initServices(s *stores, deps *Dependencies) *services {
|
func initServices(s *stores, deps *Dependencies) *services {
|
||||||
@@ -185,6 +187,11 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
assetAudit,
|
assetAudit,
|
||||||
)
|
)
|
||||||
operationPassword := operationPasswordSvc.New(deps.Redis)
|
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{
|
return &services{
|
||||||
Account: account,
|
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),
|
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),
|
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),
|
CommissionWithdrawal: commissionWithdrawalSvc.New(deps.DB, s.Shop, s.Account, s.AgentWallet, s.AgentWalletTransaction, s.CommissionWithdrawalRequest),
|
||||||
CommissionWithdrawalSetting: commissionWithdrawalSettingSvc.New(deps.DB, s.Account, s.CommissionWithdrawalSetting),
|
CommissionWithdrawalSetting: commissionWithdrawalSettingSvc.New(deps.DB, s.Account, s.CommissionWithdrawalSetting),
|
||||||
CommissionCalculation: commissionCalculationSvc.New(
|
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),
|
AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account),
|
||||||
Carrier: carrierSvc.New(s.Carrier),
|
Carrier: carrierSvc.New(s.Carrier),
|
||||||
PackageSeries: packageSeriesSvc.New(s.PackageSeries, s.ShopSeriesAllocation, s.Package),
|
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),
|
PackageDailyRecord: packageSvc.NewDailyRecordService(deps.DB, deps.Redis, s.PackageUsageDailyRecord, deps.Logger),
|
||||||
PackageCustomerView: packageSvc.NewCustomerViewService(deps.DB, deps.Redis, s.PackageUsage, 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),
|
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),
|
ShopSeriesGrant: shopSeriesGrantSvc.New(deps.DB, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries, deps.Logger),
|
||||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||||
PurchaseValidation: purchaseValidation,
|
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),
|
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),
|
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),
|
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),
|
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
|
||||||
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
||||||
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, 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),
|
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, assetAudit),
|
||||||
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
||||||
StopResumeService: stopResumeService,
|
StopResumeService: stopResumeService,
|
||||||
@@ -279,6 +286,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
PackageActivation: packageActivation,
|
PackageActivation: packageActivation,
|
||||||
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
||||||
OperationPassword: operationPassword,
|
OperationPassword: operationPassword,
|
||||||
|
AgentOpenAPI: agentOpenAPI,
|
||||||
Refund: refundSvc.New(
|
Refund: refundSvc.New(
|
||||||
deps.DB,
|
deps.DB,
|
||||||
s.RefundRequest,
|
s.RefundRequest,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
"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/break/junhong_cmp_fiber/internal/middleware"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@@ -64,12 +65,14 @@ type Handlers struct {
|
|||||||
Refund *admin.RefundHandler
|
Refund *admin.RefundHandler
|
||||||
ClientWechat *app.ClientWechatHandler
|
ClientWechat *app.ClientWechatHandler
|
||||||
SuperAdmin *admin.SuperAdminHandler
|
SuperAdmin *admin.SuperAdminHandler
|
||||||
|
AgentOpenAPI *openapiHandler.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middlewares 封装所有中间件
|
// Middlewares 封装所有中间件
|
||||||
// 用于路由注册
|
// 用于路由注册
|
||||||
type Middlewares struct {
|
type Middlewares struct {
|
||||||
PersonalAuth *middleware.PersonalAuthMiddleware
|
PersonalAuth *middleware.PersonalAuthMiddleware
|
||||||
AdminAuth func(*fiber.Ctx) error
|
AdminAuth func(*fiber.Ctx) error
|
||||||
|
AgentOpenAPIAuth func(*fiber.Ctx) error
|
||||||
// TODO: 新增 Middleware 在此添加字段
|
// 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
|
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 灵活字符串类型
|
// FlexString 灵活字符串类型
|
||||||
// Gateway 部分字段会在 string / number / bool 之间漂移,此类型统一转为字符串避免解析失败。
|
// Gateway 部分字段会在 string / number / bool 之间漂移,此类型统一转为字符串避免解析失败。
|
||||||
type FlexString string
|
type FlexString string
|
||||||
@@ -91,7 +122,7 @@ type CardStatusReq struct {
|
|||||||
// CardStatusResp 是查询流量卡状态的响应
|
// CardStatusResp 是查询流量卡状态的响应
|
||||||
type CardStatusResp struct {
|
type CardStatusResp struct {
|
||||||
ICCID string `json:"iccid" description:"ICCID"`
|
ICCID string `json:"iccid" description:"ICCID"`
|
||||||
CardStatus string `json:"cardStatus" description:"卡状态(准备、正常、停机)"`
|
CardStatus string `json:"cardStatus" description:"卡状态(准备、正常、停机;仅正常表示开机)"`
|
||||||
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
Extend string `json:"extend,omitempty" description:"扩展字段(广电国网特殊参数)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +133,9 @@ type FlowQueryReq struct {
|
|||||||
|
|
||||||
// FlowUsageResp 是查询流量使用的响应
|
// FlowUsageResp 是查询流量使用的响应
|
||||||
type FlowUsageResp struct {
|
type FlowUsageResp struct {
|
||||||
ICCID string `json:"iccid" description:"ICCID"`
|
ICCID string `json:"iccid" description:"ICCID"`
|
||||||
Used float64 `json:"used" description:"当月已用流量(MB)"`
|
Used FlexFloat `json:"used" description:"当月已用流量(MB,兼容数字或字符串)"`
|
||||||
Unit string `json:"unit" description:"流量单位(MB)"`
|
Unit string `json:"unit" description:"流量单位(MB)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CardOperationReq 是停机/复机请求
|
// 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")
|
personalGroup := app.Group("/api/c/v1")
|
||||||
RegisterPersonalCustomerRoutes(personalGroup, doc, "/api/c/v1", handlers, middlewares.PersonalAuth)
|
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 {
|
if handlers.PaymentCallback != nil {
|
||||||
callbackGroup := app.Group("/api/callback")
|
callbackGroup := app.Group("/api/callback")
|
||||||
registerPaymentCallbackRoutes(callbackGroup, handlers.PaymentCallback, doc, "/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 {
|
if err != nil {
|
||||||
s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err))
|
s.logger.Warn("刷新卡数据:查询网络状态失败", zap.String("iccid", iccid), zap.Error(err))
|
||||||
} else {
|
} else {
|
||||||
networkStatus := parseNetworkStatus(statusResp.CardStatus)
|
networkStatus, ok := gateway.ParseCardNetworkStatus(statusResp.CardStatus, statusResp.Extend)
|
||||||
updates["network_status"] = networkStatus
|
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. 查询实名状态
|
// 2. 查询实名状态
|
||||||
@@ -1267,7 +1274,7 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("刷新卡数据:查询流量失败", zap.String("iccid", iccid), zap.Error(err))
|
s.logger.Warn("刷新卡数据:查询流量失败", zap.String("iccid", iccid), zap.Error(err))
|
||||||
} else {
|
} 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
|
return today == prevDay
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseNetworkStatus 将网关返回的卡状态字符串转换为 network_status 数值
|
|
||||||
// 停机→0,其他(准备/正常)→1
|
|
||||||
func parseNetworkStatus(cardStatus string) int {
|
|
||||||
if cardStatus == "停机" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值
|
// parseGatewayRealnameStatus 将网关返回的实名状态布尔值转换为 real_name_status 数值
|
||||||
// true=已实名(1),false=未实名(0)
|
// true=已实名(1),false=未实名(0)
|
||||||
func parseGatewayRealnameStatus(realStatus bool) int {
|
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 或虚拟号")
|
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
|
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) {
|
func (s *IotCardStore) GetByIDs(ctx context.Context, ids []uint) ([]*model.IotCard, error) {
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return []*model.IotCard{}, nil
|
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))
|
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, false, time.Since(startTime))
|
||||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
|
||||||
}
|
}
|
||||||
gatewayFlowMB = result.Used
|
gatewayFlowMB = float64(result.Used)
|
||||||
} else {
|
} else {
|
||||||
gatewayFlowMB = card.CurrentMonthUsageMB
|
gatewayFlowMB = card.CurrentMonthUsageMB
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// PollingCardStatusHandler 卡开停机状态轮询任务处理器
|
// PollingCardStatusHandler 卡开停机状态轮询任务处理器
|
||||||
// 职责:调 Gateway 查卡状态(准备/正常/停机)→ 映射为 network_status → 写 DB → 触发停复机评估
|
// 职责:调 Gateway 查卡状态(正常/停机/准备)→ 映射为 network_status → 写 DB → 触发停复机评估
|
||||||
// 不做:直接停复机决策,委托给 StopResumeService.EvaluateAndAct
|
// 不做:直接停复机决策,委托给 StopResumeService.EvaluateAndAct
|
||||||
type PollingCardStatusHandler struct {
|
type PollingCardStatusHandler struct {
|
||||||
base *PollingBase
|
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)
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newNetworkStatus int
|
newNetworkStatus := card.NetworkStatus
|
||||||
|
gatewayCardStatus := ""
|
||||||
if h.gateway != nil {
|
if h.gateway != nil {
|
||||||
result, gwErr := h.gateway.QueryCardStatus(ctx, &gateway.CardStatusReq{CardNo: card.ICCID})
|
result, gwErr := h.gateway.QueryCardStatus(ctx, &gateway.CardStatusReq{CardNo: card.ICCID})
|
||||||
if gwErr != nil {
|
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))
|
h.base.updateStats(ctx, constants.TaskTypePollingCardStatus, false, time.Since(startTime))
|
||||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)
|
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCardStatus)
|
||||||
}
|
}
|
||||||
// "停机" → 0(停机),"准备"/"正常" → 1(开机)
|
gatewayCardStatus = result.CardStatus
|
||||||
if result.CardStatus == "停机" {
|
parsedStatus, ok := gateway.ParseCardNetworkStatus(result.CardStatus, result.Extend)
|
||||||
newNetworkStatus = constants.NetworkStatusOffline
|
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 {
|
} else {
|
||||||
newNetworkStatus = constants.NetworkStatusOnline
|
newNetworkStatus = parsedStatus
|
||||||
}
|
}
|
||||||
if h.base.verboseLog {
|
if h.base.verboseLog {
|
||||||
h.base.logger.Info("卡状态轮询详情",
|
h.base.logger.Info("卡状态轮询详情",
|
||||||
zap.Uint("card_id", cardID),
|
zap.Uint("card_id", cardID),
|
||||||
zap.String("iccid", card.ICCID),
|
zap.String("iccid", card.ICCID),
|
||||||
zap.String("card_status", result.CardStatus),
|
zap.String("card_status", result.CardStatus),
|
||||||
|
zap.String("extend", result.Extend),
|
||||||
zap.Int("new_network_status", newNetworkStatus),
|
zap.Int("new_network_status", newNetworkStatus),
|
||||||
zap.Bool("changed", newNetworkStatus != card.NetworkStatus))
|
zap.Bool("changed", newNetworkStatus != card.NetworkStatus))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
newNetworkStatus = card.NetworkStatus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statusChanged := newNetworkStatus != card.NetworkStatus
|
statusChanged := newNetworkStatus != card.NetworkStatus
|
||||||
@@ -98,7 +103,9 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, t *asynq.Task) er
|
|||||||
if statusChanged {
|
if statusChanged {
|
||||||
fields["network_status"] = newNetworkStatus
|
fields["network_status"] = newNetworkStatus
|
||||||
// 网关侧停机事件:补写 stop_reason,便于状态追踪和区分手动停机
|
// 网关侧停机事件:补写 stop_reason,便于状态追踪和区分手动停机
|
||||||
if newNetworkStatus == constants.NetworkStatusOffline && card.StopReason == "" {
|
if newNetworkStatus == constants.NetworkStatusOffline &&
|
||||||
|
card.StopReason == "" &&
|
||||||
|
gateway.IsGatewayCardStopped(gatewayCardStatus) {
|
||||||
fields["stop_reason"] = constants.StopReasonCarrierStopped
|
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 // 开机
|
NetworkStatusOnline = 1 // 开机
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Gateway 卡状态
|
||||||
|
const (
|
||||||
|
GatewayCardStatusReady = "准备" // 网关卡状态:准备
|
||||||
|
GatewayCardStatusNormal = "正常" // 网关卡状态:正常
|
||||||
|
GatewayCardStatusStopped = "停机" // 网关卡状态:停机
|
||||||
|
GatewayCardExtendPendingActivation = "待激活" // 网关扩展状态:待激活
|
||||||
|
)
|
||||||
|
|
||||||
// IoT 卡停机原因
|
// IoT 卡停机原因
|
||||||
const (
|
const (
|
||||||
StopReasonTrafficExhausted = "traffic_exhausted" // 流量耗尽
|
StopReasonTrafficExhausted = "traffic_exhausted" // 流量耗尽
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ func RedisAuthTokenKey(token string) string {
|
|||||||
return fmt.Sprintf("auth:token:%s", token)
|
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 键
|
// RedisRefreshTokenKey 生成刷新令牌的 Redis 键
|
||||||
// 用途:存储用户 refresh token 信息
|
// 用途:存储用户 refresh token 信息
|
||||||
// 过期时间:7 天(可配置)
|
// 过期时间:7 天(可配置)
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const (
|
|||||||
CodeWechatUserInfoFailed = 1045 // 获取微信用户信息失败
|
CodeWechatUserInfoFailed = 1045 // 获取微信用户信息失败
|
||||||
CodeWechatPayFailed = 1046 // 微信支付发起失败
|
CodeWechatPayFailed = 1046 // 微信支付发起失败
|
||||||
CodeWechatCallbackInvalid = 1047 // 微信回调签名验证失败
|
CodeWechatCallbackInvalid = 1047 // 微信回调签名验证失败
|
||||||
|
CodeOpenAPIAuthFailed = 1048 // 开放接口认证失败
|
||||||
|
CodeOpenAPISignInvalid = 1049 // 开放接口签名无效
|
||||||
|
|
||||||
// 组织相关错误 (1030-1049)
|
// 组织相关错误 (1030-1049)
|
||||||
CodeShopNotFound = 1030 // 店铺不存在
|
CodeShopNotFound = 1030 // 店铺不存在
|
||||||
@@ -152,6 +154,8 @@ const (
|
|||||||
CodeNeedRealname = 1187 // 该套餐需实名认证后购买
|
CodeNeedRealname = 1187 // 该套餐需实名认证后购买
|
||||||
CodeOpenIDNotFound = 1188 // 未找到微信授权信息,请先完成授权
|
CodeOpenIDNotFound = 1188 // 未找到微信授权信息,请先完成授权
|
||||||
CodeRealnameNotAvailable = 1189 // 请先完成充值或购买套餐后再进行实名认证
|
CodeRealnameNotAvailable = 1189 // 请先完成充值或购买套餐后再进行实名认证
|
||||||
|
CodeOpenAPITimestampInvalid = 1190 // 开放接口时间戳无效
|
||||||
|
CodeOpenAPINonceReplay = 1191 // 开放接口 nonce 重放
|
||||||
|
|
||||||
// 换货相关错误 (1200-1209)
|
// 换货相关错误 (1200-1209)
|
||||||
CodeExchangeOrderNotFound = 1200 // 换货单不存在
|
CodeExchangeOrderNotFound = 1200 // 换货单不存在
|
||||||
@@ -219,6 +223,8 @@ var allErrorCodes = []int{
|
|||||||
CodeWechatUserInfoFailed,
|
CodeWechatUserInfoFailed,
|
||||||
CodeWechatPayFailed,
|
CodeWechatPayFailed,
|
||||||
CodeWechatCallbackInvalid,
|
CodeWechatCallbackInvalid,
|
||||||
|
CodeOpenAPIAuthFailed,
|
||||||
|
CodeOpenAPISignInvalid,
|
||||||
CodeInvalidStatus,
|
CodeInvalidStatus,
|
||||||
CodeInsufficientBalance,
|
CodeInsufficientBalance,
|
||||||
CodeWithdrawalNotFound,
|
CodeWithdrawalNotFound,
|
||||||
@@ -289,6 +295,8 @@ var allErrorCodes = []int{
|
|||||||
CodeNeedRealname,
|
CodeNeedRealname,
|
||||||
CodeOpenIDNotFound,
|
CodeOpenIDNotFound,
|
||||||
CodeRealnameNotAvailable,
|
CodeRealnameNotAvailable,
|
||||||
|
CodeOpenAPITimestampInvalid,
|
||||||
|
CodeOpenAPINonceReplay,
|
||||||
CodeExchangeOrderNotFound,
|
CodeExchangeOrderNotFound,
|
||||||
CodeExchangeInProgress,
|
CodeExchangeInProgress,
|
||||||
CodeExchangeStatusInvalid,
|
CodeExchangeStatusInvalid,
|
||||||
@@ -436,6 +444,10 @@ var errorMessages = map[int]string{
|
|||||||
CodeWechatUserInfoFailed: "获取微信用户信息失败",
|
CodeWechatUserInfoFailed: "获取微信用户信息失败",
|
||||||
CodeWechatPayFailed: "微信支付发起失败",
|
CodeWechatPayFailed: "微信支付发起失败",
|
||||||
CodeWechatCallbackInvalid: "微信回调验证失败",
|
CodeWechatCallbackInvalid: "微信回调验证失败",
|
||||||
|
CodeOpenAPIAuthFailed: "开放接口认证失败",
|
||||||
|
CodeOpenAPISignInvalid: "开放接口签名无效",
|
||||||
|
CodeOpenAPITimestampInvalid: "开放接口时间戳无效",
|
||||||
|
CodeOpenAPINonceReplay: "重复请求,请勿重放",
|
||||||
CodeInternalError: "内部服务器错误",
|
CodeInternalError: "内部服务器错误",
|
||||||
CodeDatabaseError: "数据库错误",
|
CodeDatabaseError: "数据库错误",
|
||||||
CodeRedisError: "缓存服务错误",
|
CodeRedisError: "缓存服务错误",
|
||||||
@@ -464,7 +476,7 @@ func GetHTTPStatus(code int) int {
|
|||||||
return 200 // OK
|
return 200 // OK
|
||||||
case CodeInvalidParam, CodeRequestTooLarge:
|
case CodeInvalidParam, CodeRequestTooLarge:
|
||||||
return 400 // Bad Request
|
return 400 // Bad Request
|
||||||
case CodeMissingToken, CodeInvalidToken, CodeUnauthorized:
|
case CodeMissingToken, CodeInvalidToken, CodeUnauthorized, CodeOpenAPIAuthFailed, CodeOpenAPISignInvalid, CodeOpenAPITimestampInvalid:
|
||||||
return 401 // Unauthorized
|
return 401 // Unauthorized
|
||||||
case CodeForbidden:
|
case CodeForbidden:
|
||||||
return 403 // Forbidden
|
return 403 // Forbidden
|
||||||
@@ -478,7 +490,8 @@ func GetHTTPStatus(code int) int {
|
|||||||
CodeShopCodeExists,
|
CodeShopCodeExists,
|
||||||
CodeEnterpriseCodeExists,
|
CodeEnterpriseCodeExists,
|
||||||
CodeCustomerPhoneExists,
|
CodeCustomerPhoneExists,
|
||||||
CodeCarrierCodeExists:
|
CodeCarrierCodeExists,
|
||||||
|
CodeOpenAPINonceReplay:
|
||||||
return 409 // Conflict
|
return 409 // Conflict
|
||||||
case CodeTooManyRequests:
|
case CodeTooManyRequests:
|
||||||
return 429 // Too Many Requests
|
return 429 // Too Many Requests
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -28,6 +31,89 @@ func truncateBody(body []byte, maxSize int) string {
|
|||||||
return string(body[:maxSize]) + "... (truncated)"
|
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 日志中间件
|
// Middleware 创建 Fiber 日志中间件
|
||||||
// 记录所有 HTTP 请求到访问日志(包括请求和响应 body)
|
// 记录所有 HTTP 请求到访问日志(包括请求和响应 body)
|
||||||
func Middleware() fiber.Handler {
|
func Middleware() fiber.Handler {
|
||||||
@@ -50,10 +136,10 @@ func Middleware() fiber.Handler {
|
|||||||
c.SetUserContext(ctx)
|
c.SetUserContext(ctx)
|
||||||
|
|
||||||
// 获取请求 body(在 c.Next() 之前读取)
|
// 获取请求 body(在 c.Next() 之前读取)
|
||||||
requestBody := truncateBody(c.Body(), MaxBodyLogSize)
|
requestBody := sanitizeBody(c.Body())
|
||||||
|
|
||||||
// 获取 query 参数
|
// 获取 query 参数
|
||||||
queryParams := string(c.Request().URI().QueryString())
|
queryParams := sanitizeQuery(string(c.Request().URI().QueryString()))
|
||||||
|
|
||||||
// 处理请求
|
// 处理请求
|
||||||
err := c.Next()
|
err := c.Next()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
"github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||||
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
authHandler "github.com/break/junhong_cmp_fiber/internal/handler/auth"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
"github.com/break/junhong_cmp_fiber/internal/handler/callback"
|
||||||
|
openapiHandler "github.com/break/junhong_cmp_fiber/internal/handler/openapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildDocHandlers 构造文档生成用的 handlers(所有依赖传 nil)
|
// BuildDocHandlers 构造文档生成用的 handlers(所有依赖传 nil)
|
||||||
@@ -65,5 +66,6 @@ func BuildDocHandlers() *bootstrap.Handlers {
|
|||||||
Refund: admin.NewRefundHandler(nil),
|
Refund: admin.NewRefundHandler(nil),
|
||||||
ClientWechat: app.NewClientWechatHandler(nil, nil, nil),
|
ClientWechat: app.NewClientWechatHandler(nil, nil, nil),
|
||||||
SuperAdmin: admin.NewSuperAdminHandler(nil),
|
SuperAdmin: admin.NewSuperAdminHandler(nil),
|
||||||
|
AgentOpenAPI: openapiHandler.NewHandler(nil, nil),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user