# newapi-epay-adapter **Repository Path**: waynelee/newapi-epay-adapter ## Basic Information - **Project Name**: newapi-epay-adapter - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-29 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [English](README.md) | 简体中文 # EPay Adapter for NewAPI(中文说明) 这是一个为 NewAPI 提供的「易支付」协议适配层。NewAPI 按照易支付协议向本服务发起请求,本服务再分别调用支付宝官方 SDK 和微信支付 v3 接口(基于 [`wechatpay-node-v3`](https://github.com/klover2/wechatpay-node-v3-ts))完成真实下单,并把支付结果以易支付回调格式转发给 NewAPI。 ## 当前支持 - ✅ 支付宝(PC 网页跳转 `submit.php`,扫码 `mapi.php`) - ✅ 微信支付 v3(NATIVE 扫码支付,`submit.php` 跳转到内置二维码页,`mapi.php` 返回二维码链接) - 订单在内存里维护并**同步落盘**到 `data/orders.json`(`DATA_DIR` 可改),**服务重启后查单接口依然查得到**;订单默认保留 24 小时、最多 10000 条,可通过 `ORDER_TTL_MS` / `ORDER_MAX_ENTRIES` 调整 - 日志同时输出到控制台和文件,每行带时间戳、级别与来源标签;按天切分文件,保存目录、单文件大小上限、保留天数都可配置(见「日志」一节) ## 项目结构 ``` index.js 入口:装配 + 启动(只剩十几行) start.sh / start.bat Linux / Windows 启动脚本 deploy/ systemd 单元模板(后台常驻用) src/ config.js 环境变量读取与启动告警 app.js Express 应用与中间件 routes/ pay.js 下单/查单、notify.js 回调、qrcode.js 二维码页 gateways/ alipay.js / wxpay.js,各自封装渠道 SDK 与证书 protocol/ epay.js 签名、notify.js 回调转发与重试 store/ orderStore.js 订单存储(内存 + 落盘,含 TTL 清理) views/ 二维码页模板与安全渲染 lib/ 日志、超时等通用工具 ``` ⚠️ 在配置好 HTTPS、正式域名和真实支付商户密钥之前,不要把本服务暴露在公网。 ## 环境变量配置 复制 `.env.example` 为 `.env` 并填写: ```env PORT=3000 # 公网可访问地址,支付宝/微信回调要用,不要带末尾斜杠 PUBLIC_BASE=https://your-domain.example.com # 易支付协议侧(NewAPI 那边填这两个) EPAY_PID=1000 EPAY_KEY=replace-with-a-long-random-secret # 支付宝当面付(开放平台 -> 应用 -> 密钥) # 填 true 走支付宝沙箱网关,留空/false 用正式网关 ALIPAY_SANDBOX= ALIPAY_APP_ID= ALIPAY_PRIVATE_KEY= ALIPAY_PUBLIC_KEY= # 微信支付 v3(商户平台 -> 账户中心 -> API安全 / APIv3密钥) WX_APP_ID= WX_MCH_ID= # APIv3 密钥(32 位),回调解密和拉取平台证书都要用 WX_APIV3_KEY= # 商户 API 证书 apiclient_cert.pem:可直接粘贴内容(换行写成 \n),或留空改用 WX_MCH_CERT_PATH WX_MCH_CERT= WX_MCH_CERT_PATH= # 商户 API 私钥 apiclient_key.pem:同上 WX_PRIVATE_KEY= WX_PRIVATE_KEY_PATH= # 可选,留空则自动从商户证书解析 WX_MCH_SERIAL_NO= # 可选(微信支付公钥模式):公钥ID PUB_KEY_ID_xxx 与公钥 PEM 内容, # 填了之后回调验签不需要联网下载平台证书 WX_PLATFORM_SERIAL_NO= WX_PLATFORM_PUBLIC_KEY= WX_PLATFORM_PUBLIC_KEY_PATH= # 也可以直接给平台证书文件(会自动取出里面的公钥) WX_PLATFORM_CERT= WX_PLATFORM_CERT_PATH= # 订单存储(可选,留空用默认值) # 数据目录,绝对路径或相对启动目录,默认 data(即 data/orders.json) DATA_DIR= # 订单文件名,默认 orders.json ORDER_FILE= # 是否落盘,默认 true;填 false 则退回纯内存、重启即丢 ORDER_PERSIST= # 订单保留时长(毫秒),默认 24 小时 ORDER_TTL_MS= # 最多保留多少条,超出后从最旧的开始清理,默认 10000 ORDER_MAX_ENTRIES= # 后台清理任务的执行间隔(毫秒),默认 10 分钟 ORDER_SWEEP_INTERVAL_MS= # 日志(可选,留空用默认值) # 日志级别:debug / info / warn / error,默认 info LOG_LEVEL= # 是否同时输出到控制台,默认 true LOG_CONSOLE= # 是否写日志文件,默认 true LOG_FILE_ENABLED= # 日志保存目录,默认启动目录下的 logs LOG_DIR= # 日志文件名前缀,默认 app LOG_FILE_PREFIX= # 单个日志文件大小上限(MB),默认 10 LOG_MAX_SIZE_MB= # 日志保留天数(含今天),默认 7 LOG_RETENTION_DAYS= ``` 字段说明: - `PORT`:服务监听端口。 - `PUBLIC_BASE`:本服务的公网访问地址,用于生成支付宝/微信的异步通知地址,**末尾不要带 `/`**。 - `EPAY_PID` / `EPAY_KEY`:易支付商户号 / 商户密钥,NewAPI 支付设置中要填同样的值。 - `ALIPAY_APP_ID` / `ALIPAY_PRIVATE_KEY` / `ALIPAY_PUBLIC_KEY`:支付宝开放平台应用的 AppID、应用私钥、支付宝公钥。 - `ALIPAY_SANDBOX`:填 `true` 时走支付宝沙箱网关(`https://openapi-sandbox.dl.alipaydev.com/gateway.do`),留空或 `false` 时不配置网关,用 SDK 默认的正式环境。 - `WX_APP_ID`:微信支付绑定的公众号/应用 AppID。 - `WX_MCH_ID`:微信支付商户号。 - `WX_APIV3_KEY`:商户平台 -> 账户中心 -> API安全 -> APIv3密钥(32 位),用于回调解密和下载平台证书。 - `WX_MCH_CERT` / `WX_MCH_CERT_PATH`:商户 API 证书 `apiclient_cert.pem`,可把内容粘贴到 `WX_MCH_CERT`(换行写成 `\n`),或留空并把路径写到 `WX_MCH_CERT_PATH`。 - `WX_PRIVATE_KEY` / `WX_PRIVATE_KEY_PATH`:商户 API 私钥 `apiclient_key.pem`,同样支持两种填法。 - `WX_MCH_SERIAL_NO`:商户证书序列号,留空会自动从 `apiclient_cert.pem` 解析。 - `WX_PLATFORM_SERIAL_NO` / `WX_PLATFORM_PUBLIC_KEY`(或 `WX_PLATFORM_CERT`):可选。若商户号使用的是**微信支付公钥模式**(公钥 ID 形如 `PUB_KEY_ID_xxx`,在商户平台 -> 账户中心 -> API安全 里能看到),填上这两项后回调验签就无需联网下载平台证书。公钥/证书既可以粘贴内容(换行写成 `\n`),也可以填文件路径。 - `ORDER_TTL_MS` / `ORDER_MAX_ENTRIES` / `ORDER_SWEEP_INTERVAL_MS`:可选。订单的保留时长(默认 24 小时)、最多保留条数(默认 10000,超出后从最旧的开始清理)以及后台清理任务的执行间隔(默认 10 分钟)。 - `DATA_DIR` / `ORDER_FILE` / `ORDER_PERSIST`:可选。订单落盘目录(默认 `data`)、文件名(默认 `orders.json`)以及是否落盘(默认 `true`,填 `false` 退回纯内存)。详见下面的「订单持久化」一节。 - `LOG_LEVEL` / `LOG_DIR` / `LOG_MAX_SIZE_MB` / `LOG_RETENTION_DAYS` 等:可选,见下面的「日志」一节。 ## 日志 日志同时输出到控制台和文件,每行都带时间戳、级别和来源标签: ```text 2026-09-29 19:50:10.586 [INFO] [pay] submit.php type=alipay trade_no=E20260929001 ``` 文件默认保存在启动目录下的 `logs/`,一天一个文件(`app-2026-09-29.log`)。当天单个文件写满后会自动切分片(`app-2026-09-29.1.log`、`app-2026-09-29.2.log` …),超过保留天数的旧文件会在服务启动和跨天时自动删除(只删本程序生成的文件)。 | 环境变量 | 默认值 | 说明 | | --- | --- | --- | | `LOG_LEVEL` | `info` | 日志级别:`debug` / `info` / `warn` / `error`,低于该级别的不记录 | | `LOG_CONSOLE` | `true` | 是否同时输出到控制台(用 Docker/systemd 收集日志时可以改成 `false`,只留文件) | | `LOG_FILE_ENABLED` | `true` | 是否写日志文件 | | `LOG_DIR` | `logs` | 日志保存目录,绝对路径或相对启动目录 | | `LOG_FILE_PREFIX` | `app` | 日志文件名前缀 | | `LOG_MAX_SIZE_MB` | `10` | 单个日志文件大小上限(MB),超出后当天再切分片 | | `LOG_RETENTION_DAYS` | `7` | 日志保留天数(含今天),超期的旧文件自动删除 | 服务启动时会打印当前生效的日志策略(级别、目录、单文件上限、保留天数),方便确认日志写到了哪里。文件写不进去(例如目录没权限)时只会降级成只打控制台,不影响支付业务。 Docker 部署时 `docker-compose.yml` 已经把 `./logs` 挂到容器内的 `/app/logs`,日志会留在宿主机上。 ## 订单持久化 订单在内存里维护,同时每次状态变更(下单、标记支付成功、回调转发结果、过期清理)都会**同步落盘**到 `data/orders.json`,所以**服务重启后 `/api.php` 依然查得到订单**,二维码页也能继续显示商品名和金额。 - 目录用 `DATA_DIR` 配置(默认 `data`,相对路径按进程工作目录解析),文件名用 `ORDER_FILE`(默认 `orders.json`);想退回纯内存就设 `ORDER_PERSIST=false`。 - 写盘是「先写 `orders.json.tmp`,再 rename 覆盖」。rename 在同一文件系统上是原子操作,断电最多丢掉最后一次写入,不会留下半个坏文件。文件按 `0600` 创建,因为里面含 `notify_url` 这类内部地址。 - **写盘失败不影响支付**:磁盘满或目录没权限时只降级成「仅内存」并打一条告警日志,下单、回调、查单照常工作(和日志写入失败的处理原则一致)。 - `data/orders.json` 被写坏时,启动会先把它改名成 `orders.json.corrupt-<时间戳>` 留档,再按空列表继续启动,不会覆盖现场。 - 启动时会过滤掉过期订单,并把重启前卡在「回调重试中」(`pending`)的订单标记为 `failed` 并打日志——这些订单可能没通知到 NewAPI,需要人工确认。重试链是进程内的,重启后不会自动补发。 - 备份就是拷这一个文件。存储是「单实例」语义,不要让多个实例指向同一个数据目录。 ## 本地运行 ```bash npm install npm start ``` 健康检查: ```text GET / ``` 返回: ```text epay adapter ok ``` ### Linux / macOS 项目自带 `start.sh`: ```bash chmod +x start.sh # 首次执行需要;如果文件是从 Windows 拷到 Linux 的,也要补这一步 ./start.sh ``` 没有执行权限时也可以 `bash start.sh`。脚本会切到项目目录、检查 Node 版本(需要 18 或更高)和 `node_modules`,然后前台启动服务,Ctrl+C 停止。想常驻后台建议用 systemd 或下面的 Docker 方式,不要用 `nohup` 硬扛。 > 用 git 提交本文件时,可以执行 `git add --chmod=+x start.sh`,把「可执行」这个权限位一起记进仓库,别人 clone 下来就能直接 `./start.sh`。 ### Windows 双击 `start.bat`(或在命令行里执行 `start.bat`)即可。 ## Docker 运行 项目自带 `Dockerfile` 和 `docker-compose.yml`,默认会加入和 NewAPI 相同的 Docker 网络,方便容器间互相访问。 ```bash docker compose up -d --build ``` 如果和 NewAPI 不在同一个 compose 项目中,请根据实际情况修改 `docker-compose.yml` 里的外部网络名称(用 `docker network ls` 查看)。 服务名和容器名是 `epay-adapter`(早期版本叫 `epay-shim`)。如果 nginx、反向代理或别的 compose 文件里按容器名引用了旧名字,记得同步改成 `epay-adapter`,或者在 `.env`/compose 里改回旧名字。 ## 后台运行 服务本身是前台进程,想常驻后台按场景选一种。 ### systemd(Linux 服务器推荐) 仓库里带了单元模板 `deploy/epay-adapter.service`: ```bash sudo cp deploy/epay-adapter.service /etc/systemd/system/epay-adapter.service sudo nano /etc/systemd/system/epay-adapter.service # 改 WorkingDirectory / ExecStart / User sudo chmod +x /opt/newapi-epay-adapter/start.sh sudo systemctl daemon-reload sudo systemctl enable --now epay-adapter systemctl status epay-adapter journalctl -u epay-adapter -f ``` 几个容易踩的点: - `WorkingDirectory` 必须是项目根目录:`.env` 里的 `pay/*.pem` 是相对路径,代码按进程工作目录解析,`LOG_DIR` 的相对路径也一样。 - 运行用户要能读 `.env` 和 `pay/*.pem`、能写 `logs/` 和 `data/`,例如 `sudo chown -R www-data:www-data /opt/newapi-epay-adapter`,或者把 `LOG_DIR` / `DATA_DIR` 指到该用户可写的目录。 - `start.sh` 里是 `exec node index.js`,systemd 能直接拿到 node 作为主进程,停止/重启信号直达服务,不会被 shell 吞掉。 - 日志文件服务自己按天写、按天清理,**不需要配 logrotate**;但控制台那份额外输出会进 journal,建议顺手 `sudo journalctl --vacuum-time=7d`,或调 `/etc/systemd/journald.conf` 的 `SystemMaxUse=`。只想留文件日志就在 unit 里加 `Environment=LOG_CONSOLE=false`。 - unit 里的 `Environment=` 优先级高于 `.env`(dotenv 不覆盖已存在的环境变量),临时改端口或日志目录不用动 `.env`。 - 订单会落盘到 `data/orders.json`,服务重启后查单接口依然查得到(见「订单持久化」一节);`Restart=always` 保证进程意外退出后会被拉起来。 - Node 如果是用 nvm 装的,systemd 看不到它(systemd 不读 `.bashrc`),服务会以「没找到 node」退出 1,处理办法见下面「systemd 找不到 node」小节。 #### systemd 找不到 node(Node 装在 nvm 里) 手动执行 `./start.sh` 能正常启动,交给 systemd 却以 `[start.sh] 没找到 node,请先安装 Node.js 18 或更高版本` 退出 1,日志后面还跟一句 `Start request repeated too quickly` —— 这两条一起出现,基本可以确定是 Node 用 nvm 装的。 原因:交互式 shell 会加载 `.bashrc` 里的 nvm 初始化,把 `~/.nvm/versions/node/vXX/bin` 加进 `PATH`;systemd 不读这些文件,用的是一套精简 PATH(`/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`),看不到 nvm 目录,所以 `start.sh` 里的 `command -v node` 直接判定「没找到 node」。最后那句 `started too quickly` 是 unit 里 `StartLimitIntervalSec` / `StartLimitBurst` 触发的启动熔断,不是新的故障。 改法一,装一份全局 Node(推荐,unit 一个字都不用改): ```bash curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs which node # 期望输出 /usr/bin/node ``` 改法二,在 unit 的 `[Service]` 段补一行 PATH,具体路径用 `command -v node` 查: ```ini Environment=PATH=/root/.nvm/versions/node/v22.23.3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ``` 这条路径绑死了版本号,将来 `nvm install` 升级 Node 后服务会以同样的方式再挂一次,记得同步改。 改法三,让 `ExecStart` 直接指向 Node 绝对路径,绕过 `start.sh` 的检查: ```ini ExecStart=/root/.nvm/versions/node/v22.23.3/bin/node index.js ``` `WorkingDirectory` 已经是项目根目录,所以 `.env` 和 `pay/*.pem` 的相对路径仍然解析正确。 改完 unit 后: ```bash sudo systemctl daemon-reload sudo systemctl reset-failed epay-adapter # 清掉熔断计数,这一步不能省 sudo systemctl start epay-adapter systemctl status epay-adapter journalctl -u epay-adapter -n 20 --no-pager ``` 日志里出现 `epay adapter listening on :3400` 就算成功。改法二和改法三都要求 `/root/.nvm` 对服务用户可读,unit 里的 `User=` 一旦改成非 root 就会失效,那种情况下只能用改法一。 ### Docker `docker compose up -d` 即可,容器带 `restart: always`(开机自启),日志通过 `./logs` 挂在宿主机上。 ### 临时/快速后台运行 ```bash cd /opt/newapi-epay-adapter nohup ./start.sh >/dev/null 2>&1 & echo $! > epay-adapter.pid kill "$(cat epay-adapter.pid)" # 停止 ``` 日志本来就带时间戳写进文件,所以 stdout 可以直接丢掉;想保留就写成 `>> epay-adapter.out 2>&1`(记得自己清理),或者在 `.env` 里设 `LOG_CONSOLE=false`。想退出终端还留着进程记得加 `disown`,更省事的是用 tmux:`tmux new -s epay 'cd /opt/newapi-epay-adapter && ./start.sh'`。 ## NewAPI 配置 在 NewAPI 后台 -> 系统设置 -> 支付设置中开启「易支付」,填写: ```text 易支付地址:https://your-domain.example.com (即 PUBLIC_BASE) 商户 ID:和 EPAY_PID 保持一致 商户密钥:和 EPAY_KEY 保持一致 ``` 支付方式列表(同时支持支付宝和微信): ```json [ { "color": "rgba(var(--semi-blue-5), 1)", "name": "支付宝", "type": "alipay" }, { "color": "rgba(var(--semi-green-5), 1)", "name": "微信支付", "type": "wxpay" } ] ``` NewAPI 生成订单后会提交到: ```text https://your-domain.example.com/submit.php ``` 或调用扫码接口: ```text https://your-domain.example.com/mapi.php ``` ## 支付流程说明 - **支付宝**:`submit.php` 直接跳转到支付宝收银台页面;`mapi.php` 调用 `alipay.trade.precreate` 返回二维码链接。 - **微信支付**:`submit.php` 调用 v3 Native 下单接口(`POST /v3/pay/transactions/native`),成功后跳转到本服务内置的二维码展示页 `/wxpay/qrcode`;该页面会每 2 秒轮询一次订单状态,支付成功后自动跳转回 `return_url`。`mapi.php` 同样调用下单接口并直接返回二维码链接(`code_url`)。 二维码由服务端用 npm 包 [`qrcode`](https://www.npmjs.com/package/qrcode) 生成成 PNG 内嵌在页面里,页面不加载任何外部脚本或 CDN,内网/无外网环境也能正常显示;页面上会一并展示该订单的商品名称与金额(取自本地订单记录,不通过 URL 传递)。 ## 异步通知 - 支付宝:`POST /alipay/notify`,验签通过且交易状态为 `TRADE_SUCCESS`/`TRADE_FINISHED` 时,标记订单成功并转发给 NewAPI 的 `notify_url`。 - 微信支付:`POST /wxpay/notify`,先用 `Wechatpay-*` 请求头对原始报文验签,再用 APIv3 密钥解密 `resource`,当 `event_type=TRANSACTION.SUCCESS` 且 `trade_state=SUCCESS` 时标记订单成功并转发给 NewAPI 的 `notify_url`。 `GET /api.php?act=order` 还兼做兜底:微信订单本地还没标记成功时,会主动调用 `GET /v3/pay/transactions/out-trade-no/{out_trade_no}` 查单,若微信返回 `trade_state=SUCCESS` 则直接完成订单。回调延迟或丢失时二维码页面不会一直卡住。 转发到 NewAPI 失败时会自动重试,最多 5 次,每次间隔递增(30s、60s、90s...)。 ## 常见问题 - **微信报错 "ISV权限不足"**:检查微信支付商户号是否已经完成 `Native支付` 产品的签约,以及 `WX_APP_ID` 是否和商户号正确绑定。 - **微信报错「缺少配置...」/ 微信支付不可用**:启动日志里出现 `[wxpay] v3 初始化失败` 时,说明 `WX_APP_ID`、`WX_MCH_ID`、`WX_APIV3_KEY`、`WX_MCH_CERT(_PATH)`、`WX_PRIVATE_KEY(_PATH)` 中有缺失或文件读不到;这种情况下支付宝仍可正常使用。 - **回调报 `拉取平台证书失败` / `无法获取验签公钥`**:服务需要与 `Wechatpay-Serial` 对应的微信公钥。没有预置公钥时它会去调 `GET /v3/certificates` 下载平台证书,这一步被微信拒绝了。现在日志会打印真实的 HTTP 状态和响应体(例如 `GET .../v3/certificates 返回 HTTP 403 {"code":"NO_AUTH",...}`)以及本次回调的 `Wechatpay-Serial`。如果你的商户号是**微信支付公钥模式**,微信已经不再签发可下载的平台证书:请到商户平台 -> 账户中心 -> API安全 复制**公钥ID**和**公钥**,分别填到 `WX_PLATFORM_SERIAL_NO`、`WX_PLATFORM_PUBLIC_KEY`(也可以把下载到的证书文件路径填到 `WX_PLATFORM_CERT_PATH`)。查单不受影响,回调失败期间订单仍会通过 `/api.php` 的主动查单兜底完成。 - **回调返回 `401 sign error`**:签名与配置的公钥不匹配。请核对 `WX_PLATFORM_SERIAL_NO` 是否就是商户平台上的公钥ID,以及该公钥是否属于同一个商户号。 - **混合内容警告(Mixed Content)**:如果 NewAPI 是 HTTPS,但本服务是 HTTP,浏览器跳转/表单提交时会被拦截,请给本服务也配置 HTTPS(推荐 nginx + certbot)。 - **systemd 起来就挂,日志报 `[start.sh] 没找到 node`,但手动 `./start.sh` 一切正常**:Node 装在了 nvm 的目录里,而 systemd 的 PATH 里没有这个目录。处理办法见「后台运行 -> systemd 找不到 node」一节。