Files
pay-bridge/docs/tech-design.md
2026-03-13 15:51:59 +08:00

44 KiB
Raw Blame History

pay-bridge 技术详细设计文档Tech Design

字段 内容
文档版本 v1.0
创建日期 2026-02-27
状态 草稿

1. 数据库表结构DDL

所有表使用 MySQL 8.0,字符集 utf8mb4存储引擎 InnoDB。金额字段统一使用 BIGINT 以分为单位存储,避免浮点精度问题。

1.1 接入应用app

CREATE TABLE `app` (
  `id`          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `app_id`      VARCHAR(32)  NOT NULL COMMENT '应用 ID下游系统鉴权凭证',
  `app_secret`  VARCHAR(128) NOT NULL COMMENT '应用密钥AES 加密存储)',
  `app_name`    VARCHAR(64)  NOT NULL COMMENT '应用名称',
  `status`      TINYINT      NOT NULL DEFAULT 1 COMMENT '1=启用 0=禁用',
  `created_at`  DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`  DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_app_id` (`app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='接入应用';

1.2 交易订单trade_order

CREATE TABLE `trade_order` (
  `id`                   BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `trade_no`             VARCHAR(32)  NOT NULL COMMENT 'pay-bridge 生成的交易号',
  `merchant_order_no`    VARCHAR(64)  NOT NULL COMMENT '下游系统的商户订单号',
  `app_id`               VARCHAR(32)  NOT NULL COMMENT '所属应用 ID',
  `channel_code`         VARCHAR(32)  NOT NULL COMMENT '支付渠道编码(如 HEPAY',
  `channel_trade_no`     VARCHAR(64)  DEFAULT NULL COMMENT '上游渠道交易号',
  `pay_method`           VARCHAR(32)  NOT NULL COMMENT '支付方式WECHAT_JSAPI/WECHAT_H5/WECHAT_NATIVE/WECHAT_MINI/ALIPAY/QUICK_PAY/TRANSFER',
  `amount`               BIGINT       NOT NULL COMMENT '订单金额(分)',
  `profit_sharing_amount` BIGINT      NOT NULL DEFAULT 0 COMMENT '分润金额0=不分润',
  `service_fee_amount`   BIGINT       NOT NULL DEFAULT 0 COMMENT '服务费金额(分),支付完成后计算填入',
  `subject`              VARCHAR(256) NOT NULL COMMENT '商品描述',
  `notify_url`           VARCHAR(512) NOT NULL COMMENT '下游通知地址',
  `status`               VARCHAR(20)  NOT NULL DEFAULT 'CREATING' COMMENT '状态(见状态机)',
  `extra`                JSON         DEFAULT NULL COMMENT '支付方式相关扩展参数(如 openid',
  `channel_extra`        JSON         DEFAULT NULL COMMENT '渠道返回的支付凭证原始数据',
  `expire_time`          DATETIME(3)  NOT NULL COMMENT '订单过期时间',
  `pay_time`             DATETIME(3)  DEFAULT NULL COMMENT '支付成功时间',
  `created_at`           DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`           DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_trade_no` (`trade_no`),
  UNIQUE KEY `uk_app_merchant_order` (`app_id`, `merchant_order_no`),
  KEY `idx_channel_trade_no` (`channel_trade_no`),
  KEY `idx_app_status_created` (`app_id`, `status`, `created_at`),
  KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易订单';

1.3 退款记录refund_order

CREATE TABLE `refund_order` (
  `id`                  BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `refund_no`           VARCHAR(32)  NOT NULL COMMENT 'pay-bridge 退款单号',
  `trade_no`            VARCHAR(32)  NOT NULL COMMENT '关联交易号',
  `app_id`              VARCHAR(32)  NOT NULL,
  `channel_code`        VARCHAR(32)  NOT NULL,
  `channel_refund_no`   VARCHAR(64)  DEFAULT NULL COMMENT '上游渠道退款单号',
  `refund_amount`       BIGINT       NOT NULL COMMENT '退款金额(分)',
  `reason`              VARCHAR(256) DEFAULT NULL COMMENT '退款原因',
  `status`              VARCHAR(20)  NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/PROCESSING/SUCCESS/FAILED',
  `notify_url`          VARCHAR(512) DEFAULT NULL,
  `refund_time`         DATETIME(3)  DEFAULT NULL COMMENT '退款完成时间',
  `created_at`          DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`          DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_refund_no` (`refund_no`),
  KEY `idx_trade_no` (`trade_no`),
  KEY `idx_app_id_status` (`app_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款记录';

1.4 渠道配置channel_config

CREATE TABLE `channel_config` (
  `id`              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `app_id`          VARCHAR(32)  NOT NULL COMMENT '关联应用 ID',
  `channel_code`    VARCHAR(32)  NOT NULL COMMENT '渠道编码',
  `merchant_id`     VARCHAR(64)  NOT NULL COMMENT '渠道商户 ID',
  `api_key`         TEXT         DEFAULT NULL COMMENT 'API 密钥AES 加密)',
  `private_key`     TEXT         DEFAULT NULL COMMENT 'RSA 私钥AES 加密)',
  `public_key`      TEXT         DEFAULT NULL COMMENT '渠道公钥',
  `notify_url`      VARCHAR(512) NOT NULL COMMENT '上游回调接收地址',
  `sandbox`         TINYINT      NOT NULL DEFAULT 0 COMMENT '1=沙箱 0=生产',
  `extra_config`    JSON         DEFAULT NULL COMMENT '渠道特有扩展配置',
  `status`          TINYINT      NOT NULL DEFAULT 1,
  `created_at`      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_app_channel` (`app_id`, `channel_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='渠道配置';

1.5 通知记录notify_log

CREATE TABLE `notify_log` (
  `id`              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `trade_no`        VARCHAR(32)  NOT NULL COMMENT '关联交易号',
  `notify_type`     VARCHAR(20)  NOT NULL COMMENT 'PAYMENT / REFUND',
  `notify_url`      VARCHAR(512) NOT NULL,
  `status`          VARCHAR(20)  NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SUCCESS/FAILED/GIVEUP',
  `retry_count`     INT          NOT NULL DEFAULT 0 COMMENT '已重试次数',
  `next_retry_time` DATETIME(3)  DEFAULT NULL COMMENT '下次重试时间',
  `last_response`   TEXT         DEFAULT NULL COMMENT '最近一次响应内容',
  `created_at`      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_trade_notify_type` (`trade_no`, `notify_type`),
  KEY `idx_status_next_retry` (`status`, `next_retry_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='下游通知记录';

1.6 分润配置profit_sharing_config

CREATE TABLE `profit_sharing_config` (
  `id`                   BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `app_id`               VARCHAR(32)  NOT NULL,
  `receiver_merchant_id` VARCHAR(64)  NOT NULL COMMENT '分润接收方商户 ID上游平台',
  `receiver_type`        VARCHAR(20)  NOT NULL COMMENT 'PLATFORM / SUB_MERCHANT',
  `max_sharing_ratio`    DECIMAL(5,4) NOT NULL COMMENT '最大分润比例0.0001-1.0000',
  `status`               TINYINT      NOT NULL DEFAULT 1,
  `created_at`           DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`           DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_app_id` (`app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分润配置(应用级)';

1.7 分润记录profit_sharing_order

CREATE TABLE `profit_sharing_order` (
  `id`                   BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `sharing_no`           VARCHAR(32)  NOT NULL COMMENT '分润单号',
  `trade_no`             VARCHAR(32)  NOT NULL COMMENT '关联交易号',
  `app_id`               VARCHAR(32)  NOT NULL,
  `receiver_merchant_id` VARCHAR(64)  NOT NULL,
  `sharing_amount`       BIGINT       NOT NULL COMMENT '分润金额(分)',
  `status`               VARCHAR(20)  NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/PROCESSING/SUCCESS/FAILED/ROLLBACK',
  `channel_sharing_no`   VARCHAR(64)  DEFAULT NULL COMMENT '上游分账单号',
  `fail_reason`          VARCHAR(256) DEFAULT NULL,
  `sharing_time`         DATETIME(3)  DEFAULT NULL,
  `created_at`           DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`           DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_sharing_no` (`sharing_no`),
  UNIQUE KEY `uk_trade_no` (`trade_no`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分润记录';

1.8 服务费配置service_fee_config

CREATE TABLE `service_fee_config` (
  `id`                    BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `app_id`                VARCHAR(32)  NOT NULL,
  `pay_method_group`      VARCHAR(20)  NOT NULL COMMENT 'SCAN扫码/TRANSFER对公转账/BALANCE余额',
  `fee_rate`              DECIMAL(6,4) NOT NULL COMMENT '费率0=免费,最大 9.9999%',
  `fee_receiver_merchant_id` VARCHAR(64) NOT NULL COMMENT '服务费收款方商户 ID',
  `status`                TINYINT      NOT NULL DEFAULT 1,
  `created_at`            DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`            DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_app_method` (`app_id`, `pay_method_group`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务费配置';

1.9 服务费流水service_fee_log

CREATE TABLE `service_fee_log` (
  `id`                    BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `trade_no`              VARCHAR(32)  NOT NULL,
  `config_id`             BIGINT UNSIGNED NOT NULL,
  `fee_amount`            BIGINT       NOT NULL COMMENT '服务费金额(分)',
  `fee_rate`              DECIMAL(6,4) NOT NULL COMMENT '实际使用的费率快照',
  `receiver_merchant_id`  VARCHAR(64)  NOT NULL,
  `action`                VARCHAR(20)  NOT NULL COMMENT 'CHARGE / ROLLBACK',
  `status`                VARCHAR(20)  NOT NULL DEFAULT 'PENDING',
  `channel_sharing_no`    VARCHAR(64)  DEFAULT NULL,
  `created_at`            DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`            DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_trade_action` (`trade_no`, `action`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务费流水';

1.10 子商户收款账户sub_merchant_account

CREATE TABLE `sub_merchant_account` (
  `id`               BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `app_id`           VARCHAR(32)  NOT NULL,
  `sub_merchant_id`  VARCHAR(64)  NOT NULL COMMENT '子商户 ID上游平台',
  `channel_code`     VARCHAR(32)  NOT NULL,
  `account_type`     VARCHAR(20)  NOT NULL COMMENT 'BANK_CARD / 其他',
  `account_no`       VARCHAR(64)  NOT NULL COMMENT '银行账号(脱敏存储,完整值加密)',
  `account_no_enc`   TEXT         DEFAULT NULL COMMENT '银行账号AES 加密完整值)',
  `account_name`     VARCHAR(128) NOT NULL COMMENT '账户名称',
  `bank_name`        VARCHAR(64)  DEFAULT NULL COMMENT '开户行',
  `status`           TINYINT      NOT NULL DEFAULT 1,
  `created_at`       DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`       DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  KEY `idx_app_merchant` (`app_id`, `sub_merchant_id`),
  KEY `idx_account_no` (`account_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='子商户固定收款账户';

1.11 收款匹配记录payment_match_log

CREATE TABLE `payment_match_log` (
  `id`              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `account_id`      BIGINT UNSIGNED NOT NULL COMMENT '关联子商户收款账户 ID',
  `trade_no`        VARCHAR(32)  DEFAULT NULL COMMENT '匹配成功的交易号',
  `incoming_amount` BIGINT       NOT NULL COMMENT '入账金额(分)',
  `incoming_remark` VARCHAR(256) DEFAULT NULL COMMENT '转账备注',
  `payer_name`      VARCHAR(128) DEFAULT NULL COMMENT '付款方账户名称',
  `channel_bill_no` VARCHAR(64)  DEFAULT NULL COMMENT '上游入账流水号',
  `match_status`    VARCHAR(20)  NOT NULL DEFAULT 'PENDING_MANUAL' COMMENT 'MATCHED/PENDING_MANUAL/NAME_DIFF',
  `name_diff`       TINYINT      NOT NULL DEFAULT 0 COMMENT '1=名称不一致(已标记差异)',
  `match_time`      DATETIME(3)  DEFAULT NULL,
  `operator`        VARCHAR(64)  DEFAULT NULL COMMENT '人工操作者(非自动匹配时填入)',
  `created_at`      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  KEY `idx_account_status` (`account_id`, `match_status`),
  KEY `idx_channel_bill_no` (`channel_bill_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收款匹配记录';

1.12 订单编码序列order_sequence

CREATE TABLE `order_sequence` (
  `id`             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `app_id`         VARCHAR(32)  NOT NULL,
  `seq_type`       VARCHAR(20)  NOT NULL COMMENT 'TRADE / REFUND / SHARING',
  `prefix`         VARCHAR(8)   NOT NULL COMMENT '序号前缀(如 PAY/REF/SHA',
  `current_value`  BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前序号',
  `step`           INT          NOT NULL DEFAULT 1 COMMENT '步长(批量预分配时用)',
  `updated_at`     DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_app_type` (`app_id`, `seq_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单编码序列';

1.13 对账记录reconciliation

CREATE TABLE `reconciliation` (
  `id`          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `recon_date`  DATE         NOT NULL COMMENT '对账日期',
  `channel_code` VARCHAR(32) NOT NULL,
  `app_id`      VARCHAR(32)  NOT NULL,
  `status`      VARCHAR(20)  NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCESS/FAILED',
  `match_count` INT          NOT NULL DEFAULT 0,
  `long_count`  INT          NOT NULL DEFAULT 0 COMMENT '长款数(上游有本地无)',
  `short_count` INT          NOT NULL DEFAULT 0 COMMENT '短款数(本地有上游无)',
  `diff_count`  INT          NOT NULL DEFAULT 0 COMMENT '金额不一致数',
  `created_at`  DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`  DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_date_channel_app` (`recon_date`, `channel_code`, `app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对账记录';

1.14 商户merchant

CREATE TABLE `merchant` (
  `id`                  BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `merchant_id`         VARCHAR(32)  NOT NULL COMMENT '内部商户 ID',
  `merchant_name`       VARCHAR(128) NOT NULL,
  `license_no`          VARCHAR(64)  DEFAULT NULL COMMENT '营业执照号',
  `legal_person`        VARCHAR(64)  DEFAULT NULL COMMENT '法人姓名',
  `bank_account`        VARCHAR(64)  DEFAULT NULL COMMENT '银行账号(脱敏)',
  `channel_merchant_id` VARCHAR(64)  DEFAULT NULL COMMENT '上游平台商户 ID',
  `status`              VARCHAR(20)  NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/ACTIVE/FROZEN/REJECTED',
  `created_at`          DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`          DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_merchant_id` (`merchant_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户';

1.15 微信绑定wechat_binding

CREATE TABLE `wechat_binding` (
  `id`           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `merchant_id`  VARCHAR(32)  NOT NULL,
  `openid`       VARCHAR(64)  NOT NULL,
  `union_id`     VARCHAR(64)  DEFAULT NULL,
  `nickname`     VARCHAR(64)  DEFAULT NULL,
  `notify_types` JSON         DEFAULT NULL COMMENT '订阅的消息类型数组',
  `status`       TINYINT      NOT NULL DEFAULT 1 COMMENT '1=有效 0=已解绑',
  `bind_time`    DATETIME(3)  NOT NULL,
  `created_at`   DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `updated_at`   DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_merchant_openid` (`merchant_id`, `openid`),
  KEY `idx_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信绑定';

2. 核心接口定义Go Interface

2.1 渠道适配器接口

// package channel

// PaymentChannel 支付渠道统一接口,所有渠道适配器必须实现此接口
type PaymentChannel interface {
    // Code 返回渠道编码,如 "HEPAY"
    Code() string

    // CreateOrder 统一下单,返回支付凭证
    CreateOrder(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error)

    // QueryOrder 查询订单状态
    QueryOrder(ctx context.Context, req *QueryOrderReq) (*QueryOrderResp, error)

    // CloseOrder 关闭订单
    CloseOrder(ctx context.Context, req *CloseOrderReq) (*CloseOrderResp, error)

    // Refund 发起退款
    Refund(ctx context.Context, req *RefundReq) (*RefundResp, error)

    // QueryRefund 查询退款状态
    QueryRefund(ctx context.Context, req *QueryRefundReq) (*QueryRefundResp, error)

    // VerifyNotify 验证上游回调签名,返回解析后的通知数据
    VerifyNotify(ctx context.Context, rawBody []byte, headers map[string]string) (*NotifyData, error)

    // ProfitSharing 发起分账(渠道不支持时返回 ErrNotSupported
    ProfitSharing(ctx context.Context, req *ProfitSharingReq) (*ProfitSharingResp, error)

    // RollbackProfitSharing 回退分账(用于退款场景)
    RollbackProfitSharing(ctx context.Context, req *RollbackSharingReq) (*RollbackSharingResp, error)

    // DownloadBill 下载对账账单
    DownloadBill(ctx context.Context, req *DownloadBillReq) (*BillData, error)

    // MerchantApply 商户进件
    MerchantApply(ctx context.Context, req *MerchantApplyReq) (*MerchantApplyResp, error)

    // QueryMerchantStatus 查询商户审核状态
    QueryMerchantStatus(ctx context.Context, channelMerchantID string) (*MerchantStatusResp, error)
}

// ChannelFactory 渠道工厂函数类型
type ChannelFactory func(config *ChannelConfig) PaymentChannel

// CreateOrderReq 下单请求(统一格式)
type CreateOrderReq struct {
    AppID          string          // 内部应用 ID
    TradeNo        string          // pay-bridge 交易号
    MerchantOrderNo string         // 下游商户订单号
    PayMethod      PayMethod       // 支付方式
    Amount         int64           // 金额(分)
    Subject        string          // 商品描述
    NotifyURL      string          // 回调地址
    ExpireTime     time.Time       // 过期时间
    Extra          map[string]any  // 支付方式特有参数(如 openid
}

// CreateOrderResp 下单响应
type CreateOrderResp struct {
    ChannelTradeNo string          // 渠道交易号
    PayCredential  map[string]any  // 支付凭证(各支付方式格式不同)
    RawResponse    []byte          // 渠道原始响应(用于日志)
}

// NotifyData 上游回调解析结果
type NotifyData struct {
    TradeNo        string     // pay-bridge 交易号
    ChannelTradeNo string     // 渠道交易号
    Status         TradeStatus
    Amount         int64
    PayTime        time.Time
    NotifyType     NotifyType // PAYMENT / REFUND
    RefundNo       string     // 退款单号(退款通知时)
    RawData        []byte     // 原始报文(用于验签)
}

2.2 交易服务接口

// package service

type TradeService interface {
    // CreateOrder 统一下单
    CreateOrder(ctx context.Context, req *CreateOrderServiceReq) (*CreateOrderServiceResp, error)

    // QueryOrder 查询交易
    QueryOrder(ctx context.Context, appID, tradeNo string) (*TradeOrderDTO, error)

    // CloseOrder 关闭订单
    CloseOrder(ctx context.Context, appID, tradeNo string) error

    // HandleUpstreamNotify 处理上游回调
    HandleUpstreamNotify(ctx context.Context, channelCode string, rawBody []byte, headers map[string]string) (string, error)
}

type RefundService interface {
    // CreateRefund 发起退款
    CreateRefund(ctx context.Context, req *CreateRefundReq) (*RefundOrderDTO, error)

    // QueryRefund 查询退款
    QueryRefund(ctx context.Context, appID, refundNo string) (*RefundOrderDTO, error)
}

type ProfitSharingService interface {
    // TriggerSharing 支付成功后触发分润(异步)
    TriggerSharing(ctx context.Context, tradeNo string) error

    // RollbackSharing 退款前回退分润
    RollbackSharing(ctx context.Context, tradeNo string) error

    // QuerySharing 查询分润状态
    QuerySharing(ctx context.Context, sharingNo string) (*ProfitSharingDTO, error)
}

type NotifyService interface {
    // SendNotify 向下游发送通知(含首次)
    SendNotify(ctx context.Context, tradeNo string, notifyType NotifyType) error

    // ProcessRetryQueue 处理重试队列(由 Poller 调用)
    ProcessRetryQueue(ctx context.Context) error
}

type PaymentMatchService interface {
    // HandleIncomingPayment 处理固定账户入账通知
    HandleIncomingPayment(ctx context.Context, req *IncomingPaymentReq) error

    // ManualBindOrder 人工关联入账与订单
    ManualBindOrder(ctx context.Context, matchID int64, tradeNo string, operator string) error
}

type SequenceService interface {
    // NextTradeNo 生成下一个交易号
    NextTradeNo(ctx context.Context, appID string) (string, error)

    // NextRefundNo 生成下一个退款单号
    NextRefundNo(ctx context.Context, appID string) (string, error)

    // NextSharingNo 生成下一个分润单号
    NextSharingNo(ctx context.Context, appID string) (string, error)
}

2.3 仓储接口

// package repository

type TradeOrderRepository interface {
    Create(ctx context.Context, order *TradeOrder) error
    GetByTradeNo(ctx context.Context, tradeNo string) (*TradeOrder, error)
    GetByMerchantOrderNo(ctx context.Context, appID, merchantOrderNo string) (*TradeOrder, error)
    UpdateStatus(ctx context.Context, tradeNo string, fromStatus, toStatus TradeStatus, updates map[string]any) (bool, error)
    ListPaying(ctx context.Context, before time.Time, limit int) ([]*TradeOrder, error)
}

type NotifyLogRepository interface {
    Upsert(ctx context.Context, log *NotifyLog) error
    GetByTradeNo(ctx context.Context, tradeNo string, notifyType NotifyType) (*NotifyLog, error)
    ListPendingRetry(ctx context.Context, before time.Time, limit int) ([]*NotifyLog, error)
    IncrRetryCount(ctx context.Context, id int64, nextRetryTime time.Time, lastResponse string) error
    MarkSuccess(ctx context.Context, id int64) error
    MarkGiveup(ctx context.Context, id int64) error
}

3. 状态机定义

3.1 交易订单状态机

                     ┌──────────┐
                     │ CREATING │  (下单请求到达,尚未调用渠道)
                     └────┬─────┘
               渠道下单成功│         │渠道下单失败
                     ┌────▼─────┐  ┌────────────┐
                     │  PAYING  │  │CREATE_FAILED│
                     └────┬─────┘  └────────────┘
                          │
         ┌────────────────┼──────────────────┐
         │                │                  │
    用户支付成功      超时/主动关闭        支付失败
  (上游回调/查询)
         │                │                  │
    ┌────▼────┐      ┌────▼────┐       ┌─────▼────┐
    │  PAID   │      │ CLOSED  │       │  FAILED  │
    └────┬────┘      └─────────┘       └──────────┘
         │
    ┌────┼──────────────────────┐
    │    │                      │
  全额退款  分润触发          部分退款
    │    │                      │
┌───▼──┐ ▼                 ┌────▼──────┐
│REFUND│SHARING_PENDING → │PART_REFUND│
│      │SHARING_SUCCESS    └───────────┘
└──────┘

状态枚举

状态 说明 可流转至
CREATING 创建中(调用渠道前) PAYING / CREATE_FAILED
PAYING 支付中(等待用户付款) PAID / CLOSED / FAILED
PAID 已支付 REFUNDED / CLOSED退款完成后
CLOSED 已关闭 -
FAILED 支付失败 -
CREATE_FAILED 下单失败 PAYING重试下单时
REFUNDED 已全额退款 -

3.2 退款状态机

PENDING → PROCESSING → SUCCESS
                    └→ FAILED可重试

3.3 分润状态机

PENDING → PROCESSING → SUCCESS
                    └→ FAILED重试补偿
         SUCCESS → ROLLBACK退款触发回退

3.4 通知状态机

PENDING → SUCCESS
       └→ RETRY重试中retry_count < 8
       └→ GIVEUP超过 8 次,人工介入)

4. 关键流程时序图

4.1 统一下单流程

sequenceDiagram
    participant Client as 下游系统
    participant API as API 层
    participant Trade as 交易服务
    participant Seq as 订单编码服务
    participant Redis as Redis
    participant DB as MySQL
    participant Channel as 渠道适配器
    participant HePay as 汇元支付

    Client->>API: POST /api/v1/pay/unified-order
    API->>API: 鉴权appId+appSecret 签名验证)
    API->>API: 参数校验
    API->>Trade: CreateOrder(req)
    Trade->>Redis: SET NX idempotent:{appId}:{merchantOrderNo}
    alt 幂等 key 已存在
        Redis-->>Trade: false已存在
        Trade->>DB: GetByMerchantOrderNo
        DB-->>Trade: 已有订单
        Trade-->>API: 返回已有凭证
    else 首次请求
        Redis-->>Trade: true创建成功
        Trade->>Seq: NextTradeNo(appId)
        Seq->>DB: UPDATE order_sequence ... RETURNING current_value行锁
        DB-->>Seq: 序号
        Seq-->>Trade: trade_no
        Trade->>DB: INSERT trade_orderstatus=CREATING
        Trade->>Channel: CreateOrder(channelReq)
        Channel->>HePay: 调用汇元下单 API3DES 加密 + RSA 签名)
        HePay-->>Channel: 支付凭证(解密+验签)
        Channel-->>Trade: CreateOrderResp
        Trade->>DB: UPDATE trade_order SET status=PAYING, channel_extra=...
        Trade-->>API: 统一格式支付凭证
    end
    API-->>Client: HTTP 200 支付凭证

4.2 异步通知处理流程

sequenceDiagram
    participant HePay as 汇元支付
    participant NotifyAPI as 回调接口
    participant Trade as 交易服务
    participant Notify as 通知服务
    participant DB as MySQL
    participant Redis as Redis
    participant Downstream as 下游系统

    HePay->>NotifyAPI: POST /api/v1/notify/payment/HEPAY
    NotifyAPI->>NotifyAPI: 渠道验签RSA
    NotifyAPI->>Trade: HandleUpstreamNotify(data)
    Trade->>DB: UpdateStatus(PAYING→PAID)(乐观锁)
    Note over Trade,DB: WHERE status='PAYING' AND trade_no=?
    Trade->>DB: INSERT/UPDATE notify_log
    Trade->>Notify: SendNotify(tradeNo, PAYMENT)
    Notify->>Downstream: POST {notify_url} 通知
    alt 下游返回成功
        Downstream-->>Notify: HTTP 200 / "success"
        Notify->>DB: UPDATE notify_log SET status=SUCCESS
    else 下游返回失败或超时
        Downstream-->>Notify: 失败
        Notify->>DB: UPDATE notify_log SET retry_count++, next_retry_time=now+15s
        Notify->>Redis: ZADD notify_retry {score=next_retry_ts} {trade_no}
    end
    NotifyAPI-->>HePay: "success"(必须快速响应,异步处理通知)

    loop Poller goroutine每 5s 扫描)
        Redis->>Redis: ZRANGEBYSCORE notify_retry 0 now
        Redis-->>Notify: 到期任务列表
        Notify->>Downstream: POST {notify_url} 重试
        alt 成功
            Notify->>DB: status=SUCCESS
            Notify->>Redis: ZREM
        else 失败且 retry_count < 8
            Notify->>DB: UPDATE next_retry_time按退避策略
            Notify->>Redis: ZADD更新 score
        else retry_count >= 8
            Notify->>DB: status=GIVEUP
            Notify->>Redis: ZREM
        end
    end

4.3 固定账户收款匹配流程

sequenceDiagram
    participant HePay as 汇元支付
    participant MatchAPI as 入账回调接口
    participant Match as 收款匹配服务
    participant DB as MySQL
    participant Notify as 通知服务

    HePay->>MatchAPI: POST /api/v1/notify/account-payment/HEPAY
    MatchAPI->>MatchAPI: 验签
    MatchAPI->>Match: HandleIncomingPayment(incoming)
    Match->>DB: 查询 sub_merchant_accountby account_no
    DB-->>Match: 账户信息app_id, sub_merchant_id

    Note over Match: 开始三维度匹配
    Match->>Match: Step1: 解析 incoming_remark 提取订单号(正则)
    alt 备注含有效订单号
        Match->>DB: 查询 trade_order by merchant_order_no / trade_nostatus=PAYING
        DB-->>Match: 候选订单
        Match->>Match: Step2: 校验金额是否一致
        alt 金额一致
            Match->>Match: Step3: 比对 payer_name 与订单 invoice_name
            alt 名称一致
                Match->>DB: 更新订单状态 PAYING→PAIDname_diff=0
                Match->>DB: INSERT payment_match_logstatus=MATCHED
                Match->>Notify: SendNotify(tradeNo, PAYMENT)
            else 名称不一致
                Match->>DB: 更新订单状态 PAYING→PAID标记 name_diff
                Match->>DB: INSERT payment_match_logstatus=NAME_DIFF
                Match->>Notify: SendNotify含名称差异标记
            end
        else 金额不一致
            Match->>DB: INSERT payment_match_logstatus=PENDING_MANUAL
        end
    else 备注无效/为空
        Match->>DB: 按金额在 app 下的 PAYING 订单中查找
        alt 唯一匹配
            Match->>DB: 更新为 MATCHED同名称逻辑
        else 多个匹配/无匹配
            Match->>DB: INSERT payment_match_logstatus=PENDING_MANUAL
        end
    end
    MatchAPI-->>HePay: "success"

5. 错误码设计

采用分段式错误码,格式:{类别}{4位数字}HTTP 状态码与业务错误码分离。

// package errcode

const (
    // 成功
    OK = "0"

    // 1xxxx 参数/请求错误HTTP 400
    ErrInvalidParam        = "10001" // 参数校验失败
    ErrMissingParam        = "10002" // 缺少必填参数
    ErrInvalidPayMethod    = "10003" // 不支持的支付方式
    ErrInvalidAmount       = "10004" // 金额非法

    // 2xxxx 鉴权错误HTTP 401/403
    ErrUnauthorized        = "20001" // 签名验证失败
    ErrAppNotFound         = "20002" // 应用不存在或已禁用
    ErrPermissionDenied    = "20003" // 无权操作该资源

    // 3xxxx 业务规则错误HTTP 422
    ErrOrderNotFound       = "30001" // 订单不存在
    ErrOrderAlreadyPaid    = "30002" // 订单已支付
    ErrOrderClosed         = "30003" // 订单已关闭
    ErrRefundAmountExceed  = "30004" // 退款金额超限
    ErrSharingAmountExceed = "30005" // 分润金额超过最大比例
    ErrSharingNotConfig    = "30006" // 未配置分润接收方
    ErrSharingFeeExceed    = "30007" // 分润+服务费超过订单金额
    ErrOrderIdempotent     = "30008" // 幂等:返回已有订单

    // 4xxxx 渠道错误HTTP 502
    ErrChannelCreateFail   = "40001" // 渠道下单失败
    ErrChannelRefundFail   = "40002" // 渠道退款失败
    ErrChannelTimeout      = "40003" // 渠道调用超时
    ErrChannelNotSupport   = "40004" // 渠道不支持该功能
    ErrChannelVerifyFail   = "40005" // 回调验签失败

    // 5xxxx 系统内部错误HTTP 500
    ErrInternalDB          = "50001" // 数据库错误
    ErrInternalRedis       = "50002" // Redis 错误
    ErrInternalSystem      = "50099" // 系统内部错误
)

// APIError 统一错误响应结构
type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    TraceID string `json:"trace_id,omitempty"`
}

统一响应格式

{
  "code": "0",
  "message": "success",
  "data": { ... },
  "trace_id": "abc123"
}

错误时:

{
  "code": "30004",
  "message": "退款金额超过可退金额",
  "trace_id": "abc123"
}

6. 关键算法

6.1 订单号生成算法

格式:{prefix}{yyMMdd}{8位序号补零},例如 PAY26022700000001

func (s *sequenceService) NextTradeNo(ctx context.Context, appID string) (string, error) {
    // 使用 MySQL 行锁保证原子性
    // UPDATE order_sequence SET current_value = current_value + 1
    //   WHERE app_id = ? AND seq_type = 'TRADE'
    // 读取更新后的 current_value
    val, err := s.repo.IncrAndGet(ctx, appID, SeqTypeTrade)
    if err != nil {
        return "", err
    }
    prefix := "PAY"
    date := time.Now().Format("060102") // yyMMdd
    seq := fmt.Sprintf("%08d", val%100000000) // 8位溢出归零支持每日1亿笔
    return prefix + date + seq, nil
}

对于高并发场景QPS > 1000可改为批量预取step > 1每次取 100 个序号在内存消费,减少 DB 压力。

6.2 通知重试间隔计算

var retryIntervals = []time.Duration{
    0,             // 第 1 次(立即)
    15 * time.Second,
    30 * time.Second,
    1 * time.Minute,
    5 * time.Minute,
    30 * time.Minute,
    1 * time.Hour,
    6 * time.Hour,
    12 * time.Hour, // 第 9 次(最后一次)
}

// NextRetryTime 计算下次重试时间
// retryCount 为当前已重试次数0 = 尚未重试)
func NextRetryTime(retryCount int) (time.Time, bool) {
    if retryCount >= len(retryIntervals)-1 {
        return time.Time{}, false // 已超过最大重试次数,放弃
    }
    interval := retryIntervals[retryCount+1]
    return time.Now().Add(interval), true
}

6.3 固定账户收款匹配算法

// MatchIncomingPayment 核心匹配逻辑
func (s *paymentMatchService) match(ctx context.Context, incoming *IncomingPayment, accountID int64, appID string) (*MatchResult, error) {
    // Step 1: 从备注中提取订单号(正则优先匹配 trade_no/merchant_order_no
    orderNoPatterns := []string{
        `PAY\d{14}`,        // pay-bridge 交易号格式
        `[A-Z0-9]{16,32}`,  // 通用订单号格式
    }
    candidates := extractOrderNos(incoming.Remark, orderNoPatterns)

    var matched *TradeOrder
    for _, orderNo := range candidates {
        order, err := s.tradeRepo.GetByTradeOrMerchantNo(ctx, appID, orderNo)
        if err != nil || order == nil || order.Status != StatusPaying {
            continue
        }
        // Step 2: 金额精确匹配
        if order.Amount != incoming.Amount {
            continue
        }
        matched = order
        break
    }

    // 备注无法匹配时,降级为金额匹配
    if matched == nil {
        orders, _ := s.tradeRepo.ListPayingByAmount(ctx, appID, incoming.Amount, 7*24*time.Hour)
        if len(orders) == 1 {
            matched = orders[0]
        } else if len(orders) > 1 {
            // Step 3辅助: 用付款方名称缩小范围
            matched = filterByPayerName(orders, incoming.PayerName)
            if matched == nil {
                return &MatchResult{Status: StatusPendingManual}, nil
            }
        } else {
            return &MatchResult{Status: StatusPendingManual}, nil
        }
    }

    // Step 3: 付款方名称一致性检查
    nameDiff := false
    if matched.InvoiceName != "" && incoming.PayerName != "" {
        nameDiff = matched.InvoiceName != incoming.PayerName
    }

    return &MatchResult{
        TradeNo:  matched.TradeNo,
        Status:   StatusMatched,
        NameDiff: nameDiff,
    }, nil
}

6.4 服务费计算

// CalculateServiceFee 服务费计算(四舍五入到分)
func CalculateServiceFee(amount int64, rate decimal.Decimal) int64 {
    amountDec := decimal.NewFromInt(amount)
    fee := amountDec.Mul(rate).Round(0) // 四舍五入
    return fee.IntPart()
}

// ValidateSharingAndFee 校验分润+服务费不超过订单金额
func ValidateSharingAndFee(orderAmount, sharingAmount, serviceFee int64) error {
    if sharingAmount+serviceFee > orderAmount {
        return errors.New(errcode.ErrSharingFeeExceed, "分润与服务费之和超过订单金额")
    }
    return nil
}

7. 配置结构

7.1 应用配置config.yaml

server:
  port: 8080
  read_timeout: 30s
  write_timeout: 30s

database:
  dsn: "${DB_DSN}"          # 从环境变量注入,格式: user:pass@tcp(host:port)/db?charset=utf8mb4&parseTime=true
  max_open_conns: 50
  max_idle_conns: 10
  conn_max_lifetime: 1h

redis:
  addr: "${REDIS_ADDR}"     # host:port
  password: "${REDIS_PASS}"
  db: 0
  pool_size: 20

security:
  field_encrypt_key: "${FIELD_ENCRYPT_KEY}"  # AES-256 密钥,用于加密数据库敏感字段
  app_secret_salt: "${APP_SECRET_SALT}"       # appSecret 哈希盐

notify:
  poller_interval: 5s       # 重试队列扫描间隔
  poller_batch: 100         # 每次扫描处理的任务数
  http_timeout: 10s         # 向下游发送通知的超时

reconciliation:
  cron: "0 2 * * *"         # 每日 02:00 触发对账
  bill_retry_times: 3       # 账单下载失败重试次数

merchant:
  status_check_cron: "0 9 * * *"  # 每日 09:00 检测商户状态

wechat:
  app_id: "${WECHAT_APP_ID}"
  app_secret: "${WECHAT_APP_SECRET}"
  template_ids:
    payment_success: "template_id_1"
    refund_success:  "template_id_2"
    anomaly_alert:   "template_id_3"

log:
  level: "info"   # debug/info/warn/error
  format: "json"  # json/text

7.2 Go 配置结构体

// package config

type Config struct {
    Server         ServerConfig
    Database       DatabaseConfig
    Redis          RedisConfig
    Security       SecurityConfig
    Notify         NotifyConfig
    Reconciliation ReconciliationConfig
    Wechat         WechatConfig
    Log            LogConfig
}

type ServerConfig struct {
    Port         int           `yaml:"port"`
    ReadTimeout  time.Duration `yaml:"read_timeout"`
    WriteTimeout time.Duration `yaml:"write_timeout"`
}

type DatabaseConfig struct {
    DSN             string        `yaml:"dsn"`
    MaxOpenConns    int           `yaml:"max_open_conns"`
    MaxIdleConns    int           `yaml:"max_idle_conns"`
    ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"`
}

type SecurityConfig struct {
    FieldEncryptKey string `yaml:"field_encrypt_key"` // 从环境变量读取
    AppSecretSalt   string `yaml:"app_secret_salt"`
}

type NotifyConfig struct {
    PollerInterval time.Duration `yaml:"poller_interval"`
    PollerBatch    int           `yaml:"poller_batch"`
    HTTPTimeout    time.Duration `yaml:"http_timeout"`
}

8. 目录结构

pay-bridge/
├── cmd/
│   └── server/
│       └── main.go              # 启动入口
├── internal/
│   ├── api/                     # API 层Handler + 中间件)
│   │   ├── handler/
│   │   │   ├── pay.go           # 支付相关 Handler
│   │   │   ├── notify.go        # 回调接收 Handler
│   │   │   └── admin.go         # 管理接口 Handler
│   │   └── middleware/
│   │       ├── auth.go          # 鉴权中间件
│   │       └── ratelimit.go     # 限流中间件
│   ├── service/                 # 业务逻辑层
│   │   ├── trade.go
│   │   ├── refund.go
│   │   ├── notify.go
│   │   ├── profit_sharing.go
│   │   ├── service_fee.go
│   │   ├── payment_match.go
│   │   ├── reconciliation.go
│   │   ├── merchant.go
│   │   └── wechat_notify.go
│   ├── channel/                 # 渠道适配层
│   │   ├── interface.go         # PaymentChannel interface
│   │   ├── factory.go           # 渠道工厂 + 注册表
│   │   └── hepay/              # 汇元支付适配器
│   │       ├── adapter.go
│   │       ├── sign.go          # RSA/MD5 签名
│   │       └── crypto.go        # RSA+3DES 加解密
│   ├── repository/              # 数据访问层
│   │   ├── trade_order.go
│   │   ├── refund_order.go
│   │   ├── notify_log.go
│   │   └── ...
│   ├── model/                   # 数据模型DB entity
│   ├── dto/                     # 数据传输对象Service 层进出)
│   └── errcode/                 # 错误码定义
├── pkg/
│   ├── config/                  # 配置加载
│   ├── crypto/                  # AES 加解密工具
│   ├── sequence/                # 订单号生成
│   └── logger/                  # 日志封装
├── docs/
│   ├── PRD.md
│   ├── approach-design.md
│   └── tech-design.md
├── go.mod
└── main.go

9. 汇元支付HePay适配器关键实现说明

9.1 签名方案

汇元支付支持 RSA 和 MD5 两种签名方式,根据渠道配置中 sign_type 字段决定。

// RSA 签名SHA256WithRSA
func signRSA(params map[string]string, privateKey *rsa.PrivateKey) (string, error) {
    // 1. 将参数按字典序排列,拼接为 key=value&... 格式(排除 sign 字段)
    // 2. SHA256 哈希
    // 3. RSA PKCS1v15 签名
    // 4. Base64 编码
    sortedStr := sortAndJoin(params)
    hash := sha256.Sum256([]byte(sortedStr))
    sig, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hash[:])
    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(sig), nil
}

9.2 请求体加密

// EncryptRequest 使用 RSA+3DES 加密请求体
// 1. 随机生成 3DES 密钥24 字节)
// 2. 用 3DES 加密请求 JSON
// 3. 用汇元公钥 RSA 加密 3DES 密钥
// 4. 将加密后的数据和密钥一起发送
func EncryptRequest(plaintext []byte, hePayPublicKey *rsa.PublicKey) (*EncryptedPayload, error) {
    desKey := make([]byte, 24)
    _, _ = rand.Read(desKey)

    ciphertext, err := tripleDesEncrypt(plaintext, desKey)
    if err != nil {
        return nil, err
    }

    encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, hePayPublicKey, desKey)
    if err != nil {
        return nil, err
    }

    return &EncryptedPayload{
        Data:         base64.StdEncoding.EncodeToString(ciphertext),
        EncryptedKey: base64.StdEncoding.EncodeToString(encryptedKey),
    }, nil
}

10. 监控与可观测性

10.1 关键指标Prometheus

指标名 类型 标签 说明
paybridge_order_total Counter app_id, pay_method, status 订单总数
paybridge_order_amount_total Counter app_id, pay_method 订单总金额
paybridge_channel_request_duration_seconds Histogram channel_code, method 渠道调用耗时
paybridge_channel_request_errors_total Counter channel_code, method, error_code 渠道调用错误数
paybridge_notify_retry_total Counter app_id, notify_type 通知重试总数
paybridge_notify_giveup_total Counter app_id 放弃通知总数(需告警)
paybridge_match_pending_manual_total Gauge app_id 待人工确认的收款匹配数

10.2 告警规则

告警名 条件 严重级别
渠道调用错误率过高 channel_error_rate > 5%5 分钟窗口) P1
通知放弃量激增 giveup_total 5 分钟内增长 > 10 P2
订单幂等冲突激增 30002 错误码 5 分钟 > 50 次 P2
对账差异发现 recon_diff_count > 0 P1
收款未匹配积压 pending_manual_total > 20 P2