更新项目配置,调整 .next 目录的清理脚本以支持多个子目录的删除,优化提现功能的环境变量配置,确保与现有支付配置一致。同时,增强 API 日志记录以便于调试,更新文档以反映新的环境变量使用方式,提升系统的可维护性和用户体验。
This commit is contained in:
@@ -43,7 +43,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
// 简化菜单:按功能归类,保留核心功能
|
||||
// PDF需求:分账管理、分销管理、订单管理三合一 → 交易中心
|
||||
const menuItems = [
|
||||
{ icon: LayoutDashboard, label: "数据概览147", href: "/admin" },
|
||||
{ icon: LayoutDashboard, label: "数据概览1888", href: "/admin" },
|
||||
{ icon: BookOpen, label: "内容管理", href: "/admin/content" },
|
||||
{ icon: Users, label: "用户管理", href: "/admin/users" },
|
||||
{ icon: Wallet, label: "交易中心", href: "/admin/distribution" }, // 合并:分销+订单+提现
|
||||
|
||||
@@ -103,6 +103,7 @@ export async function PUT(request: Request) {
|
||||
|
||||
if (action === 'approve') {
|
||||
console.log(STEP, '3. 发起转账(用户确认模式)')
|
||||
console.log(STEP, '审核传入: id=', id, 'userId=', userId, 'amount=', amount, 'openid=', openid ? `${openid.slice(0, 8)}...` : '(空)')
|
||||
if (!openid) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
@@ -110,6 +111,7 @@ export async function PUT(request: Request) {
|
||||
}, { status: 400 })
|
||||
}
|
||||
const amountFen = Math.round(amount * 100)
|
||||
console.log(STEP, '调用 createTransferUserConfirm: outBillNo=', id, 'amountFen=', amountFen, 'openid 长度=', openid.length)
|
||||
const transferResult = await createTransferUserConfirm({
|
||||
openid,
|
||||
amountFen,
|
||||
|
||||
@@ -18,13 +18,34 @@ export interface WechatTransferConfig {
|
||||
certSerialNo: string
|
||||
}
|
||||
|
||||
// 与小程序支付、lib/payment/config 保持一致,复用同一套 env
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
|
||||
/** 从 apiclient_cert.pem 读取证书序列号(与 lib/payment 的 WECHAT_CERT_PATH 复用) */
|
||||
function getCertSerialNoFromPath(certPath: string): string {
|
||||
const p = path.isAbsolute(certPath) ? certPath : path.join(process.cwd(), certPath)
|
||||
const pem = fs.readFileSync(p, 'utf8')
|
||||
const cert = new crypto.X509Certificate(pem)
|
||||
const raw = (cert.serialNumber || '').replace(/:/g, '').replace(/^0+/, '') || ''
|
||||
return raw.toUpperCase()
|
||||
}
|
||||
|
||||
function getConfig(): WechatTransferConfig {
|
||||
const mchId = process.env.WECHAT_MCH_ID || process.env.WECHAT_MCHID || ''
|
||||
const appId = process.env.WECHAT_APP_ID || process.env.WECHAT_APPID || 'wxb8bbb2b10dec74aa'
|
||||
const mchId = process.env.WECHAT_MCH_ID || process.env.WECHAT_MCHID || DEFAULT_MCH_ID
|
||||
const appId = process.env.WECHAT_APP_ID || process.env.WECHAT_APPID || DEFAULT_APP_ID
|
||||
const apiV3Key = process.env.WECHAT_API_V3_KEY || process.env.WECHAT_MCH_KEY || ''
|
||||
const keyPath = process.env.WECHAT_KEY_PATH || process.env.WECHAT_MCH_PRIVATE_KEY_PATH || ''
|
||||
const keyContent = process.env.WECHAT_MCH_PRIVATE_KEY || ''
|
||||
const certSerialNo = process.env.WECHAT_MCH_CERT_SERIAL_NO || ''
|
||||
let certSerialNo = process.env.WECHAT_MCH_CERT_SERIAL_NO || ''
|
||||
const certPath = process.env.WECHAT_CERT_PATH || ''
|
||||
if (!certSerialNo && certPath) {
|
||||
try {
|
||||
certSerialNo = getCertSerialNoFromPath(certPath)
|
||||
} catch (e) {
|
||||
console.warn('[wechat-transfer] 从证书文件读取序列号失败:', (e as Error).message)
|
||||
}
|
||||
}
|
||||
return {
|
||||
mchId,
|
||||
appId,
|
||||
@@ -193,8 +214,24 @@ export async function createTransferUserConfirm(
|
||||
params: CreateTransferUserConfirmParams
|
||||
): Promise<CreateTransferUserConfirmResult> {
|
||||
const cfg = getConfig()
|
||||
if (!cfg.mchId || !cfg.appId || !cfg.certSerialNo) {
|
||||
return { success: false, errorCode: 'CONFIG_ERROR', errorMessage: '微信转账配置不完整' }
|
||||
if (!cfg.mchId || !cfg.appId) {
|
||||
return { success: false, errorCode: 'CONFIG_ERROR', errorMessage: '微信转账配置不完整:缺少商户号或 AppID' }
|
||||
}
|
||||
if (!cfg.certSerialNo) {
|
||||
const certPath = process.env.WECHAT_CERT_PATH || ''
|
||||
const hint = certPath
|
||||
? `已配置 WECHAT_CERT_PATH=${certPath} 但读取序列号失败,请检查文件存在且为有效 PEM。或直接在 .env 配置 WECHAT_MCH_CERT_SERIAL_NO(openssl x509 -in apiclient_cert.pem -noout -serial 获取)`
|
||||
: '请在 .env 中配置 WECHAT_CERT_PATH(apiclient_cert.pem 路径),或配置 WECHAT_MCH_CERT_SERIAL_NO(证书序列号)'
|
||||
return { success: false, errorCode: 'CONFIG_ERROR', errorMessage: `微信转账配置不完整:缺少证书序列号。${hint}` }
|
||||
}
|
||||
try {
|
||||
getPrivateKey()
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'CONFIG_ERROR',
|
||||
errorMessage: `微信转账配置不完整:商户私钥未配置。请在 .env 中配置 WECHAT_KEY_PATH(apiclient_key.pem 路径)或 WECHAT_MCH_PRIVATE_KEY`,
|
||||
}
|
||||
}
|
||||
|
||||
const urlPath = '/v3/fund-app/mch-transfer/transfer-bills'
|
||||
@@ -218,6 +255,14 @@ export async function createTransferUserConfirm(
|
||||
const signature = buildSignature('POST', urlPath, timestamp, nonce, bodyStr)
|
||||
const authorization = buildAuthorization(timestamp, nonce, signature)
|
||||
|
||||
// 发起请求前打印:便于区分是请求微信缺参,还是管理端审核传参问题
|
||||
console.log('[wechat-transfer] ========== 请求微信支付(用户确认模式)==========')
|
||||
console.log('[wechat-transfer] URL:', `${BASE_URL}${urlPath}`)
|
||||
console.log('[wechat-transfer] 请求体 body:', JSON.stringify(body, null, 2))
|
||||
console.log('[wechat-transfer] 当前配置: mchId=', cfg.mchId, 'appId=', cfg.appId, 'certSerialNo=', cfg.certSerialNo ? `${cfg.certSerialNo.slice(0, 8)}...` : '(空)')
|
||||
console.log('[wechat-transfer] notify_url:', body.notify_url)
|
||||
console.log('[wechat-transfer] ========================================')
|
||||
|
||||
const res = await fetch(`${BASE_URL}${urlPath}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -229,6 +274,7 @@ export async function createTransferUserConfirm(
|
||||
body: bodyStr,
|
||||
})
|
||||
const data = (await res.json()) as Record<string, unknown>
|
||||
console.log('[wechat-transfer] 微信响应 status=', res.status, 'body=', JSON.stringify(data, null, 2))
|
||||
if (res.ok && res.status >= 200 && res.status < 300) {
|
||||
const state = (data.state as string) || ''
|
||||
return {
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "node scripts/clean-standalone.js && next build && node scripts/write-standalone-warning.js",
|
||||
"postbuild": "node scripts/prepare-standalone.js",
|
||||
"clean": "node -e \"const fs=require('fs'),p=require('path').join(__dirname,'.next');fs.existsSync(p)&&fs.rmSync(p,{recursive:true,force:true})&&console.log('已删除 .next')\"",
|
||||
"dev": "next dev -p 3006",
|
||||
"lint": "eslint .",
|
||||
"start": "node scripts/start-standalone.js"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 构建前清理 .next/standalone,避免 Next.js build 时 EBUSY(目录被占用)
|
||||
* 构建前清理 .next/standalone、.next/static、.next/cache
|
||||
* - 避免 Next.js build 时 EBUSY(目录被占用)
|
||||
* - 避免混入 next dev 的 Turbopack 产物,导致打包后加载 chunk 500 / Failed to load chunk
|
||||
* 若曾运行 pnpm start,请先 Ctrl+C 停掉再 build
|
||||
*/
|
||||
|
||||
@@ -8,7 +10,12 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const standaloneDir = path.join(rootDir, '.next', 'standalone');
|
||||
const nextDir = path.join(rootDir, '.next');
|
||||
const dirsToClean = [
|
||||
path.join(nextDir, 'standalone'),
|
||||
path.join(nextDir, 'static'),
|
||||
path.join(nextDir, 'cache'),
|
||||
];
|
||||
|
||||
const RETRIES = 5;
|
||||
const DELAY_MS = 2000;
|
||||
@@ -17,29 +24,39 @@ function sleep(ms) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(standaloneDir)) {
|
||||
return;
|
||||
function removeDir(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) return true;
|
||||
try {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3 });
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= RETRIES; attempt++) {
|
||||
try {
|
||||
fs.rmSync(standaloneDir, { recursive: true, force: true, maxRetries: 3 });
|
||||
console.log('[clean-standalone] 已删除 .next/standalone');
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'ENOTEMPTY') {
|
||||
if (attempt < RETRIES) {
|
||||
console.warn('[clean-standalone] 目录被占用,%ds 后重试 (%d/%d)...', DELAY_MS / 1000, attempt, RETRIES);
|
||||
await sleep(DELAY_MS);
|
||||
async function main() {
|
||||
for (const dirPath of dirsToClean) {
|
||||
if (!fs.existsSync(dirPath)) continue;
|
||||
for (let attempt = 1; attempt <= RETRIES; attempt++) {
|
||||
try {
|
||||
removeDir(dirPath);
|
||||
const name = path.relative(nextDir, dirPath) || dirPath;
|
||||
console.log('[clean-standalone] 已删除 .next/' + name);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'ENOTEMPTY') {
|
||||
if (attempt < RETRIES) {
|
||||
console.warn('[clean-standalone] 目录被占用,%ds 后重试 (%d/%d)...', DELAY_MS / 1000, attempt, RETRIES);
|
||||
await sleep(DELAY_MS);
|
||||
} else {
|
||||
console.error('[clean-standalone] 无法删除:', dirPath);
|
||||
console.error(' 请先关闭:pnpm start / next dev、或占用该目录的其它程序');
|
||||
console.error(' 然后重新执行:pnpm build');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error('[clean-standalone] 无法删除 .next/standalone:目录被占用');
|
||||
console.error(' 请先关闭:pnpm start、或占用该目录的其它程序(如资源管理器、杀毒)');
|
||||
console.error(' 然后重新执行:pnpm build');
|
||||
process.exit(1);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,17 @@ if (!fs.existsSync(chunksDir)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 禁止把开发态 Turbopack 产物打进 standalone,否则线上会 500 / Failed to load chunk
|
||||
const chunkFiles = fs.readdirSync(chunksDir);
|
||||
const turbopackChunk = chunkFiles.find((f) => f.startsWith('turbopack-') && f.endsWith('.js'));
|
||||
if (turbopackChunk) {
|
||||
console.error('❌ 错误:检测到开发态产物 ' + turbopackChunk);
|
||||
console.error(' 当前 .next/static 来自 next dev,不能用于线上。请在本机执行:');
|
||||
console.error(' pnpm run clean && pnpm build');
|
||||
console.error(' 然后重新部署 .next/standalone 整个目录。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(' public → .next/standalone/public');
|
||||
copyDir(publicSrc, publicDst);
|
||||
|
||||
|
||||
@@ -98,7 +98,28 @@
|
||||
| **平台证书(wechat_pay_pub_key)** | 用于验证回调签名 | 下载或通过API获取 |
|
||||
| **小程序AppId** | 小程序标识 | 小程序管理后台 |
|
||||
|
||||
### 2. 提取证书序列号
|
||||
### 2. 项目环境变量(.env)— 复用现有支付配置
|
||||
|
||||
**商家转账到零钱**(`lib/wechat-transfer.ts`)已与 **lib/payment/config**、小程序支付共用同一套环境变量:
|
||||
|
||||
| 环境变量 | 说明 | 复用说明 |
|
||||
|---------|------|----------|
|
||||
| `WECHAT_MCH_ID` / `WECHAT_MCHID` | 商户号 | 不填则用默认 1318592501(与 pay 一致) |
|
||||
| `WECHAT_APP_ID` / `WECHAT_APPID` | 小程序 AppID | 不填则用默认 wxb8bbb2b10dec74aa |
|
||||
| **`WECHAT_CERT_PATH`** | 商户证书路径(apiclient_cert.pem) | **与 payment 共用**;配置后证书序列号会**自动从该文件读取**,无需再配 WECHAT_MCH_CERT_SERIAL_NO |
|
||||
| **`WECHAT_KEY_PATH`** | 商户私钥路径(apiclient_key.pem) | **与 payment 共用** |
|
||||
| `WECHAT_API_V3_KEY` / `WECHAT_MCH_KEY` | APIv3 密钥(回调解密) | 与 payment 的 mchKey 共用 |
|
||||
|
||||
**只需在 .env 中配置与现有支付相同的证书路径即可**(若 payment 已配 `WECHAT_CERT_PATH`、`WECHAT_KEY_PATH`,转账会直接复用):
|
||||
|
||||
```env
|
||||
WECHAT_CERT_PATH=./certs/apiclient_cert.pem
|
||||
WECHAT_KEY_PATH=./certs/apiclient_key.pem
|
||||
```
|
||||
|
||||
若未配置证书路径,也可单独配置:`WECHAT_MCH_CERT_SERIAL_NO`(证书序列号)、`WECHAT_MCH_PRIVATE_KEY` 或 `WECHAT_KEY_PATH`(私钥)。
|
||||
|
||||
### 3. 提取证书序列号
|
||||
|
||||
**使用OpenSSL命令**:
|
||||
|
||||
@@ -121,7 +142,7 @@ echo strtoupper(dechex($certData['serialNumber']));
|
||||
?>
|
||||
```
|
||||
|
||||
### 3. 配置IP白名单
|
||||
### 4. 配置IP白名单
|
||||
|
||||
路径:微信商户平台 → 账户中心 → API安全 → IP配置
|
||||
|
||||
@@ -133,7 +154,7 @@ echo strtoupper(dechex($certData['serialNumber']));
|
||||
curl ifconfig.me
|
||||
```
|
||||
|
||||
### 4. 配置转账场景
|
||||
### 5. 配置转账场景
|
||||
|
||||
路径:微信商户平台 → 产品中心 → 商家转账到零钱 → 前往功能
|
||||
|
||||
@@ -141,7 +162,7 @@ curl ifconfig.me
|
||||
- **1000**:现金营销
|
||||
- **1005**:营销活动
|
||||
|
||||
### 5. 环境要求
|
||||
### 6. 环境要求
|
||||
|
||||
- PHP >= 7.0
|
||||
- OpenSSL 扩展(必须)
|
||||
|
||||
Reference in New Issue
Block a user