mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +08:00
* feat(i18n): translate web console messages in the frontend and AI-client errors in the backend
Chinese text was hard-coded in API messages, stored log content, emails and
server logs, so it reached users of every interface language.
Text is now translated where its reader is:
- Web console APIs return the English source text as message_key with
message_params, and the web console translates it in all seven languages.
message keeps the English text for other API callers.
- Errors returned to AI clients, emails and notifications are translated by
the backend (en, zh-CN, zh-TW) in the language saved on the account, then
the language of the request's Accept-Language header.
- A reader who states no language still gets the original Chinese text of the
messages that were Chinese before, word for word. Gateways that use another
new-api as their upstream match that text, for example to disable a channel
whose account ran out of quota.
- Usage, top-up and system log content is stored as content_parts (key and
params) and rendered in the viewer's language. Task fail reasons and refund
reasons written by the gateway are English keys. Rows written earlier are
shown as before.
- Server logs are English.
Also:
- The saved interface language zhTW, and zh-HK, zh-MO and zh-Hant, now select
Traditional Chinese in the backend instead of Simplified Chinese.
- The built-in group descriptions are translated; descriptions an
administrator saved are shown as written.
- .agents/rules/i18n.md records the conventions. Tests in i18n/ fail on a
non-English string literal, on a web console message without translations
and on a locale key without a constant.
* feat(i18n): print server logs in Chinese when LOG_LANGUAGE asks for it
Server log lines are English by default. With LOG_LANGUAGE=zh-CN, the lines
that were Chinese before print their original Chinese text.
common.LogText formats a log line from its English format, which is also the
key of its Chinese text in common/log_text.zh-CN.json. A line without an entry
prints in English. go vet checks the arguments of every call, and
TestServerLogTexts checks that each entry still matches a call and has the
same verbs as its key.
* docs: list LOG_LANGUAGE in the docker compose example
* feat(i18n): translate every server log line and add DEFAULT_LANGUAGE
Every line the gateway prints goes through common.LogText. With
DEFAULT_LANGUAGE=zh-CN, the lines that have a Chinese text in
common/log_text.zh-CN.json print it and the others print English.
DEFAULT_LANGUAGE replaces LOG_LANGUAGE. It also sets the language of
backend messages, emails and notifications for a reader who states no
language: en, zh-CN or zh-TW, English when unset. The list of messages
that stayed Chinese for such readers is removed.
Review fixes:
- Channel test logs store the token name "Model test"; the console shows
it in the interface language.
- Task fail reasons written by the gateway are fixed sentences the console
translates. The HTTP status and the upstream detail go to the server log
when a polled task is failed.
- The auto-disable reason of a slow channel is written in DEFAULT_LANGUAGE.
Also:
- Drop the repository-scanning lint tests, the i18n rules file and its
read gate. AGENTS.md keeps the conventions in four lines.
- Payment error toasts keep their previous text and only add the
translated message key.
- Remove guards and helpers that no caller needs.
* fix(i18n): address review on language lookup, notices and error responses
- Relay requests no longer load the user a second time to find the
language: the settings TokenAuth put into the request are used as they
are, also when they hold no language.
- Upstream model update notices are rendered in each recipient's saved
language, DEFAULT_LANGUAGE when none.
- /api/usage/token and /api/log/token answer with backend-translated
messages again, with the keys and status codes used before this PR.
Their callers are scripts, not the web console.
- Webhook notifications fill {{value}} placeholders like the other
senders. Running the rendered text through Sprintf appended
%!(EXTRA ...) and broke %2F in links.
- RootUserLanguage reads the setting of the root user it already loaded.
- zh_TW and zh_HK are read as Traditional Chinese.
- Add common.ApiErrorStatus for error responses under another HTTP status
and use it where those responses were built by hand.
- Remove middleware.GetLanguage, which had no callers.
435 lines
16 KiB
Go
435 lines
16 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/thanhpk/randstr"
|
|
waffo "github.com/waffo-com/waffo-go"
|
|
"github.com/waffo-com/waffo-go/config"
|
|
"github.com/waffo-com/waffo-go/core"
|
|
"github.com/waffo-com/waffo-go/types/order"
|
|
)
|
|
|
|
func getWaffoSDK() (*waffo.Waffo, error) {
|
|
env := config.Sandbox
|
|
apiKey := setting.WaffoSandboxApiKey
|
|
privateKey := setting.WaffoSandboxPrivateKey
|
|
publicKey := setting.WaffoSandboxPublicCert
|
|
if !setting.WaffoSandbox {
|
|
env = config.Production
|
|
apiKey = setting.WaffoApiKey
|
|
privateKey = setting.WaffoPrivateKey
|
|
publicKey = setting.WaffoPublicCert
|
|
}
|
|
builder := config.NewConfigBuilder().
|
|
APIKey(apiKey).
|
|
PrivateKey(privateKey).
|
|
WaffoPublicKey(publicKey).
|
|
Environment(env)
|
|
if setting.WaffoMerchantId != "" {
|
|
builder = builder.MerchantID(setting.WaffoMerchantId)
|
|
}
|
|
cfg, err := builder.Build()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return waffo.New(cfg), nil
|
|
}
|
|
|
|
func getWaffoUserEmail(user *model.User) string {
|
|
return fmt.Sprintf("%[email protected]", user.Id)
|
|
}
|
|
|
|
func getWaffoCurrency() string {
|
|
if setting.WaffoCurrency != "" {
|
|
return setting.WaffoCurrency
|
|
}
|
|
return "USD"
|
|
}
|
|
|
|
func buildWaffoTopUpGoodsInfo(amount int64) *order.GoodsInfo {
|
|
appName := strings.TrimSpace(common.SystemName)
|
|
if appName == "" {
|
|
appName = "New API"
|
|
}
|
|
return &order.GoodsInfo{
|
|
GoodsName: fmt.Sprintf("Recharge %d credits", amount),
|
|
AppName: appName,
|
|
}
|
|
}
|
|
|
|
// zeroDecimalCurrencies 零小数位币种,金额不能带小数点
|
|
var zeroDecimalCurrencies = map[string]bool{
|
|
"IDR": true, "JPY": true, "KRW": true, "VND": true,
|
|
}
|
|
|
|
func formatWaffoAmount(amount float64, currency string) string {
|
|
if zeroDecimalCurrencies[currency] {
|
|
return fmt.Sprintf("%.0f", amount)
|
|
}
|
|
return fmt.Sprintf("%.2f", amount)
|
|
}
|
|
|
|
// getWaffoPayMoney converts the user-facing amount to USD for Waffo payment.
|
|
// Waffo only accepts USD, so this function handles the conversion from different
|
|
// display types (USD/CNY/TOKENS) to the actual USD amount to charge.
|
|
func getWaffoPayMoney(amount float64, group string) float64 {
|
|
originalAmount := amount
|
|
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
|
amount = amount / common.QuotaPerUnit
|
|
}
|
|
topupGroupRatio := common.GetTopupGroupRatio(group)
|
|
if topupGroupRatio == 0 {
|
|
topupGroupRatio = 1
|
|
}
|
|
discount := 1.0
|
|
if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
|
|
if ds > 0 {
|
|
discount = ds
|
|
}
|
|
}
|
|
return amount * setting.WaffoUnitPrice * topupGroupRatio * discount
|
|
}
|
|
|
|
type WaffoPayRequest struct {
|
|
Amount int64 `json:"amount"`
|
|
PayMethodIndex *int `json:"pay_method_index"` // 服务端支付方式列表的索引,nil 表示由 Waffo 自动选择
|
|
PayMethodType string `json:"pay_method_type"` // Deprecated: 兼容旧前端,优先使用 pay_method_index
|
|
PayMethodName string `json:"pay_method_name"` // Deprecated: 兼容旧前端,优先使用 pay_method_index
|
|
}
|
|
|
|
func RequestWaffoAmount(c *gin.Context) {
|
|
var req WaffoPayRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
paymentError(c, common.NewMessage("Invalid parameters"))
|
|
return
|
|
}
|
|
|
|
waffoMinTopup := int64(setting.WaffoMinTopUp)
|
|
if req.Amount < waffoMinTopup {
|
|
paymentError(c, common.NewMessage("Top-up amount cannot be less than {{min}}", map[string]any{"min": waffoMinTopup}))
|
|
return
|
|
}
|
|
id := c.GetInt("id")
|
|
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
|
return
|
|
}
|
|
|
|
group, err := model.GetUserGroup(id, true)
|
|
if err != nil {
|
|
paymentError(c, common.NewMessage("Failed to get the user group"))
|
|
return
|
|
}
|
|
|
|
payMoney := getWaffoPayMoney(float64(req.Amount), group)
|
|
if payMoney <= 0.01 {
|
|
paymentError(c, common.NewMessage("Payment amount is too low"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
|
|
}
|
|
|
|
// RequestWaffoPay 创建 Waffo 支付订单
|
|
func RequestWaffoPay(c *gin.Context) {
|
|
if !setting.WaffoEnabled {
|
|
paymentError(c, common.NewMessage("Waffo payment is not enabled"))
|
|
return
|
|
}
|
|
|
|
var req WaffoPayRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
paymentError(c, common.NewMessage("Invalid parameters"))
|
|
return
|
|
}
|
|
waffoMinTopup := int64(setting.WaffoMinTopUp)
|
|
if req.Amount < waffoMinTopup {
|
|
paymentError(c, common.NewMessage("Top-up amount cannot be less than {{min}}", map[string]any{"min": waffoMinTopup}))
|
|
return
|
|
}
|
|
id := c.GetInt("id")
|
|
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
|
return
|
|
}
|
|
|
|
user, err := model.GetUserById(id, false)
|
|
if err != nil || user == nil {
|
|
paymentError(c, common.NewMessage("User does not exist"))
|
|
return
|
|
}
|
|
|
|
// 从服务端配置查找支付方式,客户端只传索引或旧字段
|
|
var resolvedPayMethodType, resolvedPayMethodName string
|
|
methods := setting.GetWaffoPayMethods()
|
|
if req.PayMethodIndex != nil {
|
|
// 新协议:按索引查找
|
|
idx := *req.PayMethodIndex
|
|
if idx < 0 || idx >= len(methods) {
|
|
logger.LogWarn(c.Request.Context(), common.LogText("Waffo invalid payment method index user_id=%d pay_method_index=%d method_count=%d", id, idx, len(methods)))
|
|
paymentError(c, common.NewMessage("Unsupported payment method"))
|
|
return
|
|
}
|
|
resolvedPayMethodType = methods[idx].PayMethodType
|
|
resolvedPayMethodName = methods[idx].PayMethodName
|
|
} else if req.PayMethodType != "" {
|
|
// 兼容旧前端:验证客户端传的值在服务端列表中
|
|
valid := false
|
|
for _, m := range methods {
|
|
if m.PayMethodType == req.PayMethodType && m.PayMethodName == req.PayMethodName {
|
|
valid = true
|
|
resolvedPayMethodType = m.PayMethodType
|
|
resolvedPayMethodName = m.PayMethodName
|
|
break
|
|
}
|
|
}
|
|
if !valid {
|
|
logger.LogWarn(c.Request.Context(), common.LogText("Waffo invalid payment method user_id=%d pay_method_type=%s pay_method_name=%q", id, req.PayMethodType, req.PayMethodName))
|
|
paymentError(c, common.NewMessage("Unsupported payment method"))
|
|
return
|
|
}
|
|
}
|
|
// resolvedPayMethodType/Name 为空时,Waffo 自动选择支付方式
|
|
|
|
group, _ := model.GetUserGroup(id, true)
|
|
payMoney := getWaffoPayMoney(float64(req.Amount), group)
|
|
if payMoney < 0.01 {
|
|
paymentError(c, common.NewMessage("Payment amount is too low"))
|
|
return
|
|
}
|
|
|
|
// 生成唯一订单号,paymentRequestId 与 merchantOrderId 保持一致,简化追踪
|
|
merchantOrderId := fmt.Sprintf("WAFFO-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(6))
|
|
paymentRequestId := merchantOrderId
|
|
|
|
// Token 模式下归一化 Amount(存等价美元/CNY 数量,避免 RechargeWaffo 双重放大)
|
|
amount := req.Amount
|
|
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
|
amount = max(int64(float64(req.Amount)/common.QuotaPerUnit), 1)
|
|
}
|
|
|
|
// 创建本地订单
|
|
topUp := &model.TopUp{
|
|
UserId: id,
|
|
Amount: amount,
|
|
Money: payMoney,
|
|
TradeNo: merchantOrderId,
|
|
PaymentMethod: model.PaymentMethodWaffo,
|
|
PaymentProvider: model.PaymentProviderWaffo,
|
|
CreateTime: time.Now().Unix(),
|
|
Status: common.TopUpStatusPending,
|
|
}
|
|
if err := topUp.Insert(); err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo failed to create top-up order user_id=%d trade_no=%s amount=%d error=%q", id, merchantOrderId, req.Amount, err.Error()))
|
|
paymentError(c, common.NewMessage("Failed to create order"))
|
|
return
|
|
}
|
|
|
|
sdk, err := getWaffoSDK()
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo SDK initialization failed user_id=%d trade_no=%s error=%q", id, merchantOrderId, err.Error()))
|
|
topUp.Status = common.TopUpStatusFailed
|
|
_ = topUp.Update()
|
|
paymentError(c, common.NewMessage("Payment configuration error"))
|
|
return
|
|
}
|
|
|
|
callbackAddr := service.GetCallbackAddress()
|
|
notifyUrl := callbackAddr + "/api/waffo/webhook"
|
|
if setting.WaffoNotifyUrl != "" {
|
|
notifyUrl = setting.WaffoNotifyUrl
|
|
}
|
|
returnUrl := paymentReturnPath("/wallet?show_history=true")
|
|
if setting.WaffoReturnUrl != "" {
|
|
returnUrl = setting.WaffoReturnUrl
|
|
}
|
|
|
|
currency := getWaffoCurrency()
|
|
goodsInfo := buildWaffoTopUpGoodsInfo(req.Amount)
|
|
createParams := &order.CreateOrderParams{
|
|
PaymentRequestID: paymentRequestId,
|
|
MerchantOrderID: merchantOrderId,
|
|
OrderAmount: formatWaffoAmount(payMoney, currency),
|
|
OrderCurrency: currency,
|
|
OrderDescription: goodsInfo.GoodsName,
|
|
OrderRequestedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
|
|
NotifyURL: notifyUrl,
|
|
MerchantInfo: &order.MerchantInfo{
|
|
MerchantID: setting.WaffoMerchantId,
|
|
},
|
|
UserInfo: &order.UserInfo{
|
|
UserID: strconv.Itoa(user.Id),
|
|
UserEmail: getWaffoUserEmail(user),
|
|
UserTerminal: "WEB",
|
|
},
|
|
PaymentInfo: &order.PaymentInfo{
|
|
ProductName: "ONE_TIME_PAYMENT",
|
|
PayMethodType: resolvedPayMethodType,
|
|
PayMethodName: resolvedPayMethodName,
|
|
},
|
|
GoodsInfo: goodsInfo,
|
|
SuccessRedirectURL: returnUrl,
|
|
FailedRedirectURL: returnUrl,
|
|
}
|
|
resp, err := sdk.Order().Create(c.Request.Context(), createParams, nil)
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo failed to create order user_id=%d trade_no=%s error=%q", id, merchantOrderId, err.Error()))
|
|
topUp.Status = common.TopUpStatusFailed
|
|
_ = topUp.Update()
|
|
paymentError(c, common.NewMessage("Failed to start payment"))
|
|
return
|
|
}
|
|
if !resp.IsSuccess() {
|
|
logger.LogWarn(c.Request.Context(), common.LogText("Waffo order creation rejected user_id=%d trade_no=%s code=%s message=%q response=%q", id, merchantOrderId, resp.Code, resp.Message, common.GetJsonString(resp)))
|
|
topUp.Status = common.TopUpStatusFailed
|
|
_ = topUp.Update()
|
|
paymentError(c, common.NewMessage("Failed to start payment"))
|
|
return
|
|
}
|
|
|
|
orderData := resp.GetData()
|
|
logger.LogInfo(c.Request.Context(), common.LogText("Waffo top-up order created user_id=%d trade_no=%s amount=%d money=%.2f pay_method_type=%s pay_method_name=%q", id, merchantOrderId, req.Amount, payMoney, resolvedPayMethodType, resolvedPayMethodName))
|
|
|
|
paymentUrl := orderData.FetchRedirectURL()
|
|
if paymentUrl == "" {
|
|
paymentUrl = orderData.OrderAction
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "success",
|
|
"data": gin.H{
|
|
"payment_url": paymentUrl,
|
|
"order_id": merchantOrderId,
|
|
},
|
|
})
|
|
}
|
|
|
|
// webhookPayloadWithSubInfo 扩展 PAYMENT_NOTIFICATION,包含 SDK 未定义的 subscriptionInfo 字段
|
|
type webhookPayloadWithSubInfo struct {
|
|
EventType string `json:"eventType"`
|
|
Result struct {
|
|
core.PaymentNotificationResult
|
|
SubscriptionInfo *webhookSubscriptionInfo `json:"subscriptionInfo,omitempty"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type webhookSubscriptionInfo struct {
|
|
Period string `json:"period,omitempty"`
|
|
MerchantRequest string `json:"merchantRequest,omitempty"`
|
|
SubscriptionID string `json:"subscriptionId,omitempty"`
|
|
SubscriptionRequest string `json:"subscriptionRequest,omitempty"`
|
|
}
|
|
|
|
// WaffoWebhook 处理 Waffo 回调通知(支付/退款/订阅)
|
|
func WaffoWebhook(c *gin.Context) {
|
|
if !isWaffoWebhookEnabled() {
|
|
logger.LogWarn(c.Request.Context(), common.LogText("Waffo webhook rejected reason=webhook_disabled path=%q client_ip=%s", c.Request.RequestURI, c.ClientIP()))
|
|
c.AbortWithStatus(http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo webhook failed to read request body path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
sdk, err := getWaffoSDK()
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo webhook SDK initialization failed path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
c.AbortWithStatus(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
wh := sdk.Webhook()
|
|
bodyStr := string(bodyBytes)
|
|
signature := c.GetHeader("X-SIGNATURE")
|
|
logger.LogInfo(c.Request.Context(), common.LogText("Waffo webhook received request path=%q client_ip=%s signature=%q body=%q", c.Request.RequestURI, c.ClientIP(), signature, bodyStr))
|
|
|
|
// 验证请求签名
|
|
if !wh.VerifySignature(bodyStr, signature) {
|
|
logger.LogWarn(c.Request.Context(), common.LogText("Waffo webhook signature verification failed path=%q client_ip=%s signature=%q body=%q", c.Request.RequestURI, c.ClientIP(), signature, bodyStr))
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var event core.WebhookEvent
|
|
if err := common.Unmarshal(bodyBytes, &event); err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo webhook failed to parse path=%q client_ip=%s error=%q body=%q", c.Request.RequestURI, c.ClientIP(), err.Error(), bodyStr))
|
|
sendWaffoWebhookResponse(c, wh, false, "invalid payload")
|
|
return
|
|
}
|
|
|
|
switch event.EventType {
|
|
case core.EventPayment:
|
|
// 解析为扩展类型,区分普通支付和订阅支付
|
|
var payload webhookPayloadWithSubInfo
|
|
if err := common.Unmarshal(bodyBytes, &payload); err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo failed to parse payment callback payload event_type=%s client_ip=%s error=%q body=%q", event.EventType, c.ClientIP(), err.Error(), bodyStr))
|
|
sendWaffoWebhookResponse(c, wh, false, "invalid payment payload")
|
|
return
|
|
}
|
|
logger.LogInfo(c.Request.Context(), common.LogText("Waffo webhook verified and parsed event_type=%s merchant_order_id=%s order_status=%s client_ip=%s", event.EventType, payload.Result.MerchantOrderID, payload.Result.OrderStatus, c.ClientIP()))
|
|
handleWaffoPayment(c, wh, &payload.Result.PaymentNotificationResult)
|
|
default:
|
|
logger.LogInfo(c.Request.Context(), common.LogText("Waffo webhook ignored event event_type=%s client_ip=%s", event.EventType, c.ClientIP()))
|
|
sendWaffoWebhookResponse(c, wh, true, "")
|
|
}
|
|
}
|
|
|
|
// handleWaffoPayment 处理支付完成通知
|
|
func handleWaffoPayment(c *gin.Context, wh *core.WebhookHandler, result *core.PaymentNotificationResult) {
|
|
if result.OrderStatus != "PAY_SUCCESS" {
|
|
logger.LogInfo(c.Request.Context(), common.LogText("Waffo order not successful, top-up skipped trade_no=%s order_status=%s client_ip=%s", result.MerchantOrderID, result.OrderStatus, c.ClientIP()))
|
|
// 终态失败订单标记为 failed,避免永远停在 pending
|
|
if result.MerchantOrderID != "" {
|
|
if err := model.UpdatePendingTopUpStatus(result.MerchantOrderID, model.PaymentProviderWaffo, common.TopUpStatusFailed); err != nil &&
|
|
!errors.Is(err, model.ErrTopUpNotFound) &&
|
|
!errors.Is(err, model.ErrTopUpStatusInvalid) {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo failed to mark order as failed trade_no=%s error=%q", result.MerchantOrderID, err.Error()))
|
|
}
|
|
}
|
|
sendWaffoWebhookResponse(c, wh, true, "")
|
|
return
|
|
}
|
|
|
|
merchantOrderId := result.MerchantOrderID
|
|
|
|
LockOrder(merchantOrderId)
|
|
defer UnlockOrder(merchantOrderId)
|
|
|
|
if err := model.RechargeWaffo(merchantOrderId, c.ClientIP()); err != nil {
|
|
logger.LogError(c.Request.Context(), common.LogText("Waffo top-up processing failed trade_no=%s client_ip=%s error=%q", merchantOrderId, c.ClientIP(), err.Error()))
|
|
sendWaffoWebhookResponse(c, wh, false, err.Error())
|
|
return
|
|
}
|
|
|
|
logger.LogInfo(c.Request.Context(), common.LogText("Waffo top-up succeeded trade_no=%s client_ip=%s", merchantOrderId, c.ClientIP()))
|
|
sendWaffoWebhookResponse(c, wh, true, "")
|
|
}
|
|
|
|
// sendWaffoWebhookResponse 发送签名响应
|
|
func sendWaffoWebhookResponse(c *gin.Context, wh *core.WebhookHandler, success bool, msg string) {
|
|
var body, sig string
|
|
if success {
|
|
body, sig = wh.BuildSuccessResponse()
|
|
} else {
|
|
body, sig = wh.BuildFailedResponse(msg)
|
|
}
|
|
c.Header("X-SIGNATURE", sig)
|
|
c.Data(http.StatusOK, "application/json", []byte(body))
|
|
}
|