开放接口,修复上游同步不对的问题
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:
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