feat(代理自充): AUG26-017 代理自充收款方式与线下预存款审批字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m52s

- 受控配置新增代理在线自充允许范围(仅微信/仅支付宝/同时支持),读侧与创建侧取允许范围与可用商户池交集,两侧失败关闭
- 新增允许范围查询与修改端点,读限代理与平台账号、写限超级管理员,复用受控配置写服务留痕
- tb_agent_recharge_record 新增交易流水号、线下收款方式三列快照与其他凭证列(成对迁移 000213)
- 线下申请校验启用的收款方式字典项与必填交易流水号,交易流水号独立于在线渠道交易号、不参与去重
- 扩展 offline_recharge_approval 场景可映射字段白名单与字典引用保护
- 新增付款凭证识别能力与交易流水号预填接口,识别不落库、日志不记录载荷
This commit is contained in:
2026-09-11 15:21:23 +08:00
parent e687a266e6
commit 7891189712
32 changed files with 1168 additions and 172 deletions

View File

@@ -99,8 +99,6 @@ func (c *Client) WithRetry(maxRetries int) *Client {
// 流程:包装参数 → 序列化 → 加密 → 签名 → HTTP POST带重试→ 解析响应 → 检查业务状态码
// params: 请求参数结构体,内部自动包装为 {"params": <JSON>} 格式
func (c *Client) doRequest(ctx context.Context, path string, params interface{}) (json.RawMessage, error) {
startTime := time.Now()
// 将参数包装为 {"params": ...} 格式后序列化
wrapper := requestWrapper{Params: params}
dataBytes, err := sonic.Marshal(wrapper)
@@ -115,6 +113,28 @@ func (c *Client) doRequest(ctx context.Context, path string, params interface{})
return nil, err
}
return c.executeWithRetry(ctx, path, encryptedData, true)
}
// doRequestWithoutPayloadLog 执行 Gateway 请求,但不记录请求体与响应体。
// 仅用于载荷含敏感内容(如付款凭证图片)的能力;成功与失败都只记录路径、耗时与结果摘要。
func (c *Client) doRequestWithoutPayloadLog(ctx context.Context, path string, params interface{}) (json.RawMessage, error) {
dataBytes, err := sonic.Marshal(requestWrapper{Params: params})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "序列化业务数据失败")
}
encryptedData, err := aesEncrypt(dataBytes, c.appSecret)
if err != nil {
return nil, err
}
return c.executeWithRetry(ctx, path, encryptedData, false)
}
// executeWithRetry 按现有重试语义发送一次已加密请求。
// logPayload 为 false 时不记录响应体,只记录路径、耗时与结果字节数摘要。
func (c *Client) executeWithRetry(ctx context.Context, path, encryptedData string, logPayload bool) (json.RawMessage, error) {
startTime := time.Now()
// 带重试的 HTTP 请求
var lastErr error
observer, _ := ctx.Value(attemptObserverKey{}).(AttemptObserver)
@@ -160,11 +180,19 @@ func (c *Client) doRequest(ctx context.Context, path string, params interface{})
// 成功
duration := time.Since(startTime)
c.logger.Debug("Gateway 请求成功",
zap.String("path", path),
zap.Duration("duration", duration),
zap.Any("result", result),
)
if logPayload {
c.logger.Debug("Gateway 请求成功",
zap.String("path", path),
zap.Duration("duration", duration),
zap.Any("result", result),
)
} else {
c.logger.Debug("Gateway 请求成功",
zap.String("path", path),
zap.Duration("duration", duration),
zap.Int("result_bytes", len(result)),
)
}
return result, nil
}

View File

@@ -0,0 +1,60 @@
// Package gateway 提供付款凭证识别能力,仅用于交易流水号表单预填。
// 识别结果不是资金事实,也不代表系统已完成任何资金动作。
package gateway
import (
"context"
"reflect"
"strings"
"github.com/bytedance/sonic"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// paymentVoucherRecognitionPath 是付款凭证识别接口路径。
const paymentVoucherRecognitionPath = "/ai/ocr/extract-payment"
// PaymentVoucherExtractionRequest 是付款凭证识别请求,只传图片内容。
type PaymentVoucherExtractionRequest struct {
// ImageBase64 是付款凭证图片的 base64 编码内容。
ImageBase64 string `json:"image_base64"`
}
// paymentVoucherExtraction 只解码本系统消费的支付单号。
// 识别响应还含 amount数值、payee、payment_method、payment_time 与 remark
// 这些字段刻意不声明、不解码:既不进入响应、不预填、不落库,也不被本进程持有,
// 同时避免上游字段类型漂移(实测 amount 为 JSON 数值)导致整条响应解析失败。
type paymentVoucherExtraction struct {
OrderNumber string `json:"order_number"`
}
// ExtractPaymentVoucherOrderNumber 识别付款凭证图片并只返回识别出的支付单号。
// 该能力刻意不使用记录完整请求体与响应体的泛型入口,日志只含路径、耗时与结果摘要;
// 返回空字符串表示识别服务未给出支付单号,由调用方决定失败口径。
func (c *Client) ExtractPaymentVoucherOrderNumber(ctx context.Context, imageBase64 string) (string, error) {
if strings.TrimSpace(imageBase64) == "" {
return "", errors.New(errors.CodeInvalidParam, "付款凭证图片内容不能为空")
}
data, err := c.doRequestWithoutPayloadLog(ctx, paymentVoucherRecognitionPath, PaymentVoucherExtractionRequest{
ImageBase64: imageBase64,
})
if err != nil {
return "", err
}
var extraction paymentVoucherExtraction
if err := sonic.Unmarshal(data, &extraction); err != nil {
// 刻意不记录 err.Error()sonic 的类型错误消息会内嵌响应 JSON 原文片段,
// 一旦进入日志或错误上下文就等于记录识别原始结果(金额、单号等)。
// 只记录可诊断且非敏感的摘要:路径、响应字节数与错误类型名,
// 足以区分语法错、类型错与空响应,又不携带任何载荷内容。
c.logger.Warn("付款凭证识别响应解析失败",
zap.String("path", paymentVoucherRecognitionPath),
zap.Int("result_bytes", len(data)),
zap.String("err_kind", reflect.TypeOf(err).String()),
)
return "", errors.New(errors.CodeGatewayInvalidResp, "解析付款凭证识别结果失败")
}
return strings.TrimSpace(extraction.OrderNumber), nil
}