From 70497d3047857b9c4497c2d49917ea4a6a81d33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=98=E9=A3=8E?= Date: Sat, 31 Jan 2026 17:39:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0.gitignore=E4=BB=A5=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E9=83=A8=E7=BD=B2=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E4=B8=80=E9=94=AE=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=B0=8F=E7=A8=8B=E5=BA=8F=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=B5=81=E7=A8=8B=EF=BC=8C=E5=A2=9E=E5=BC=BA=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E8=AF=B4=E6=98=8E=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 21 + .gitignore | 4 + DEPLOYMENT.md | 113 ++ app/api/admin/withdrawals/route.ts | 35 +- app/api/orders/route.ts | 49 +- .../payment/wechat/transfer/notify/route.ts | 65 + app/api/withdraw/route.ts | 26 +- ecosystem.config.cjs | 19 + lib/wechat-transfer.ts | 212 ++ miniprogram/app.json | 3 +- package.json | 2 +- requirements-deploy.txt | 3 + scripts/deploy_baota.py | 370 ++++ scripts/deploy_config.example.json | 12 + 一键部署小程序.bat | 22 - 一键部署小程序.py | 225 --- 开发文档/8、部署/宝塔配置检查说明.md | 77 + 开发文档/8、部署/当前项目部署到线上.md | 85 + 开发文档/小程序管理/SKILL.md | 1797 +++++++++++++++++ .../小程序管理/references/API接口速查表.md | 176 ++ .../小程序管理/references/企业认证完整指南.md | 307 +++ 开发文档/小程序管理/references/审核规范.md | 276 +++ .../小程序管理/references/隐私协议填写指南.md | 242 +++ .../scripts/__pycache__/mp_api.cpython-314.pyc | Bin 0 -> 30696 bytes 开发文档/小程序管理/scripts/apps_config.json | 40 + 开发文档/小程序管理/scripts/env_template.txt | 30 + 开发文档/小程序管理/scripts/mp_api.py | 635 ++++++ 开发文档/小程序管理/scripts/mp_deploy.py | 725 +++++++ 开发文档/小程序管理/scripts/mp_full.py | 555 +++++ 开发文档/小程序管理/scripts/mp_manager.py | 558 +++++ .../reports/report_soul-party_20260125_113301.json | 76 + .../reports/report_soul-party_20260125_113423.json | 76 + .../reports/report_soul-party_20260125_113434.json | 76 + .../scripts/reports/summary_20260125_113255.json | 88 + 开发文档/小程序管理/scripts/requirements.txt | 7 + 开发文档/提现功能完整技术文档.md | 1033 ++++++++++ 开发文档/服务器管理/SKILL.md | 314 +++ .../服务器管理/references/宝塔api接口文档.md | 142 ++ .../服务器管理/references/常见问题手册.md | 184 ++ 开发文档/服务器管理/references/端口配置表.md | 64 + .../服务器管理/references/系统架构说明.md | 310 +++ .../服务器管理/references/部署配置模板.md | 154 ++ 开发文档/服务器管理/scripts/ssl证书检查.py | 168 ++ 开发文档/服务器管理/scripts/一键部署.py | 138 ++ 开发文档/服务器管理/scripts/快速检查服务器.py | 104 + 45 files changed, 9346 insertions(+), 272 deletions(-) create mode 100644 .dockerignore create mode 100644 app/api/payment/wechat/transfer/notify/route.ts create mode 100644 ecosystem.config.cjs create mode 100644 lib/wechat-transfer.ts create mode 100644 requirements-deploy.txt create mode 100644 scripts/deploy_baota.py create mode 100644 scripts/deploy_config.example.json delete mode 100644 一键部署小程序.bat delete mode 100644 一键部署小程序.py create mode 100644 开发文档/8、部署/宝塔配置检查说明.md create mode 100644 开发文档/8、部署/当前项目部署到线上.md create mode 100644 开发文档/小程序管理/SKILL.md create mode 100644 开发文档/小程序管理/references/API接口速查表.md create mode 100644 开发文档/小程序管理/references/企业认证完整指南.md create mode 100644 开发文档/小程序管理/references/审核规范.md create mode 100644 开发文档/小程序管理/references/隐私协议填写指南.md create mode 100644 开发文档/小程序管理/scripts/__pycache__/mp_api.cpython-314.pyc create mode 100644 开发文档/小程序管理/scripts/apps_config.json create mode 100644 开发文档/小程序管理/scripts/env_template.txt create mode 100644 开发文档/小程序管理/scripts/mp_api.py create mode 100644 开发文档/小程序管理/scripts/mp_deploy.py create mode 100644 开发文档/小程序管理/scripts/mp_full.py create mode 100644 开发文档/小程序管理/scripts/mp_manager.py create mode 100644 开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113301.json create mode 100644 开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113423.json create mode 100644 开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113434.json create mode 100644 开发文档/小程序管理/scripts/reports/summary_20260125_113255.json create mode 100644 开发文档/小程序管理/scripts/requirements.txt create mode 100644 开发文档/提现功能完整技术文档.md create mode 100644 开发文档/服务器管理/SKILL.md create mode 100644 开发文档/服务器管理/references/宝塔api接口文档.md create mode 100644 开发文档/服务器管理/references/常见问题手册.md create mode 100644 开发文档/服务器管理/references/端口配置表.md create mode 100644 开发文档/服务器管理/references/系统架构说明.md create mode 100644 开发文档/服务器管理/references/部署配置模板.md create mode 100644 开发文档/服务器管理/scripts/ssl证书检查.py create mode 100644 开发文档/服务器管理/scripts/一键部署.py create mode 100644 开发文档/服务器管理/scripts/快速检查服务器.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5ec3979 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# 构建/运行不需要的目录,不打包进镜像 +node_modules +.next +.git +.gitignore +*.md +.env* +.DS_Store + +# 部署与开发脚本不打包 +scripts +*.sh +deploy_config.json +deploy_config.example.json +requirements-deploy.txt + +# 小程序、文档、附加模块 +miniprogram +开发文档 +addons +book diff --git a/.gitignore b/.gitignore index 529f285..cd6c6f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ node_modules/ .trae/ *.log node_modules + +# 部署配置(含服务器信息,勿提交) +deploy_config.json +scripts/deploy_config.json diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ab229c6..07150b5 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,5 +1,93 @@ # 部署指南 +## 整站与后台管理端 + +本项目是**一个 Next.js 应用**,前台 H5、后台管理、API 都在同一套代码里: + +- **前台**:`/`、`/chapters`、`/read/*`、`/my`、`/match` 等 +- **后台管理端**:`/admin`、`/admin/login`、`/admin/settings`、`/admin/users` 等 +- **API**:`/api/*`(含 `/api/admin/*`) + +**部署一次 = 前台 + 后台 + API 一起上线。** 后台无需单独部署,上线后访问: + +- 后台首页:`https://你的域名/admin` +- 后台登录:`https://你的域名/admin/login`(账号见项目文档,如 `admin` / `key123456`) + +--- + +## 项目内已有的部署配置 + +| 类型 | 文件/目录 | 说明 | +|------|------------|------| +| 总览文档 | `DEPLOYMENT.md`(本文件) | 部署步骤、环境变量、支付回调 | +| Docker | `Dockerfile` | Next.js 独立构建(`output: 'standalone'`) | +| Docker 编排 | `docker-compose.yml` | 整站容器、端口 3000、支付/基础环境变量 | +| Next 配置 | `next.config.mjs` | `output: 'standalone'` 供 Docker 使用 | +| 宝塔一键部署 | `scripts/deploy-to-server.sh` | SSH 到宝塔服务器,拉代码、安装依赖、构建、PM2 重启 | +| 宝塔自动化 | `开发文档/8、部署/Next.js自动化部署流程.md` | GitHub Webhook + 宝塔,推送即自动部署 | +| NAS 部署 | `deploy_to_nas.sh`、`redeploy.sh`、`quick_deploy.sh` | 部署到 NAS / 内网环境 | +| **宝塔部署(跨平台)** | **`scripts/deploy_baota.py`** | **Python 脚本,Windows/Mac/Linux 通用,不依赖 .sh 或 sshpass** | + +无 `vercel.json` 时,Vercel 会按默认规则部署本仓库;若需自定义路由或头信息,可再加 `vercel.json`。 + +--- + +## 宝塔部署(Python 跨平台) + +本项目在 Mac 上开发,原有一键部署脚本为 `scripts/deploy-to-server.sh`(依赖 sshpass,仅 Linux/Mac)。为在 **Windows / Mac / Linux** 上都能部署到宝塔,提供了 **Python 脚本**,不依赖 shell 或 sshpass。 + +### 1. 安装依赖 + +\`\`\`bash +pip install paramiko +\`\`\` + +### 2. 配置服务器信息 + +复制示例配置并填写真实信息(**不要提交到 Git**): + +\`\`\`bash +cp scripts/deploy_config.example.json deploy_config.json +# 编辑 deploy_config.json,填写 server_host、server_user、project_path、branch、pm2_app_name 等 +\`\`\` + +或使用环境变量(不写配置文件时,脚本会提示输入密码): + +- `DEPLOY_HOST`:服务器 IP +- `DEPLOY_USER`:SSH 用户名(如 root) +- `DEPLOY_PROJECT_PATH`:服务器上项目路径(如 /www/wwwroot/soul) +- `DEPLOY_BRANCH`:要部署的分支(如 soul-content) +- `DEPLOY_PM2_APP`:PM2 应用名(如 soul) +- `DEPLOY_SSH_KEY`:SSH 私钥路径(可选,不填则用密码) + +### 3. 执行部署 + +在**项目根目录**执行: + +\`\`\`bash +python scripts/deploy_baota.py +# 或指定配置 +python scripts/deploy_baota.py --config scripts/deploy_config.json +# 仅查看将要执行的步骤(不连接) +python scripts/deploy_baota.py --dry-run +\`\`\` + +脚本会依次执行:SSH 连接 → 拉取代码 → 安装依赖 → 构建 → PM2 重启。部署完成后访问: + +- 前台:`https://soul.quwanzhi.com` +- 后台:`https://soul.quwanzhi.com/admin` + +### 4. 首次在宝塔上准备 + +若服务器上尚未有代码,需先在宝塔上: + +1. 在网站目录(如 `/www/wwwroot/soul`)执行 `git clone <你的仓库> .`,或从本地上传代码。 +2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node_modules/next/dist/bin/next` 或 `node server.js`(若使用 standalone 输出),启动参数为 `start -p 3006`(与 `package.json` 里 `start` 端口一致)。 +3. 配置 Nginx 反向代理到该端口,并绑定域名。 +4. 之后即可用 `python scripts/deploy_baota.py` 做日常拉代码、构建、重启。 + +--- + ## 生产环境部署步骤 ### 1. Vercel部署 @@ -54,6 +142,14 @@ vercel --prod 2. 在产品中心配置支付回调URL:`https://your-domain.com/api/payment/wechat/notify` 3. 添加支付授权域名:`your-domain.com` +**提现(商家转账到零钱):** 详见 `开发文档/提现功能完整技术文档.md`。需配置: +- `WECHAT_MCH_ID`:商户号 +- `WECHAT_APP_ID`:小程序/公众号 AppID(如 `wxb8bbb2b10dec74aa`) +- `WECHAT_API_V3_KEY` 或 `WECHAT_MCH_KEY`:APIv3 密钥(32 字节,用于回调解密) +- `WECHAT_KEY_PATH` 或 `WECHAT_MCH_PRIVATE_KEY_PATH`:商户私钥文件路径(apiclient_key.pem) +- `WECHAT_MCH_CERT_SERIAL_NO`:商户证书序列号(OpenSSL 从 apiclient_cert.pem 提取) +- 商户平台需配置:商家转账到零钱、转账结果通知 URL:`https://你的域名/api/payment/wechat/transfer/notify` + ### 5. 测试流程 1. 创建测试订单 @@ -81,6 +177,23 @@ npm run dev # 访问 http://localhost:3000 \`\`\` +### Windows 本地执行 `pnpm build` 报 EPERM symlink + +本项目使用 `output: 'standalone'`,构建时 Next.js 会创建符号链接。**Windows 默认不允许普通用户创建符号链接**,会报错: + +- `EPERM: operation not permitted, symlink ... -> .next\standalone\node_modules\...` + +**可选做法(任选其一):** + +1. **开启 Windows 开发者模式(推荐,一劳永逸)** + - 设置 → 隐私和安全性 → 针对开发人员 → **开发人员模式** 打开 + - 开启后无需管理员即可创建符号链接,本地 `pnpm build` 可正常完成。 + +2. **以管理员身份运行终端再执行构建** + - 右键 Cursor/终端 → “以管理员身份运行”,在项目根目录执行 `pnpm build`。 + +若只做部署、不在本机打 standalone 包,可直接用 `python scripts/deploy_baota.py`,构建会在**服务器(Linux)**上执行,不会遇到该问题。 + ## 注意事项 1. 生产环境必须使用HTTPS diff --git a/app/api/admin/withdrawals/route.ts b/app/api/admin/withdrawals/route.ts index 4d02a6e..867c569 100644 --- a/app/api/admin/withdrawals/route.ts +++ b/app/api/admin/withdrawals/route.ts @@ -1,9 +1,11 @@ /** * 后台提现管理API * 获取所有提现记录,处理提现审批 + * 批准时如已配置微信转账则调用「商家转账到零钱」,否则仅更新为成功(需线下打款) */ import { NextResponse } from 'next/server' import { query } from '@/lib/db' +import { createTransfer } from '@/lib/wechat-transfer' // 获取所有提现记录 export async function GET(request: Request) { @@ -112,24 +114,47 @@ export async function PUT(request: Request) { } if (action === 'approve') { - // 批准提现 - 更新状态为成功 + const openid = withdrawal.wechat_openid || '' + const amountFen = Math.round(parseFloat(withdrawal.amount) * 100) + if (openid && amountFen > 0) { + const result = await createTransfer({ + openid, + amountFen, + outDetailNo: id, + transferRemark: 'Soul创业派对-提现', + }) + if (result.success) { + await query(` + UPDATE withdrawals + SET status = 'processing', transaction_id = ? + WHERE id = ? + `, [result.batchId || result.outBatchNo || '', id]) + return NextResponse.json({ + success: true, + message: '已发起微信转账,等待到账后自动更新状态', + batchId: result.batchId, + }) + } + return NextResponse.json({ + success: false, + error: result.errorMessage || '微信转账发起失败', + }, { status: 400 }) + } + // 无 openid 或金额为 0:仅标记为成功(线下打款) await query(` UPDATE withdrawals SET status = 'success', processed_at = NOW(), transaction_id = ? WHERE id = ? `, [`manual_${Date.now()}`, id]) - - // 更新用户已提现金额 await query(` UPDATE users SET withdrawn_earnings = withdrawn_earnings + ?, pending_earnings = pending_earnings - ? WHERE id = ? `, [withdrawal.amount, withdrawal.amount, withdrawal.user_id]) - return NextResponse.json({ success: true, - message: '提现已批准' + message: '提现已批准(线下打款)', }) } else if (action === 'reject') { diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts index c3661ae..99fa16d 100644 --- a/app/api/orders/route.ts +++ b/app/api/orders/route.ts @@ -2,28 +2,63 @@ * 订单管理接口 * 开发: 卡若 * 技术支持: 存客宝 + * + * GET /api/orders - 管理后台:返回全部订单(无 userId) + * GET /api/orders?userId= - 按用户返回订单 */ import { type NextRequest, NextResponse } from "next/server" +import { query } from "@/lib/db" + +function rowToOrder(row: Record) { + return { + id: row.id, + orderSn: row.order_sn, + userId: row.user_id, + openId: row.open_id, + productType: row.product_type, + productId: row.product_id, + amount: row.amount, + description: row.description, + status: row.status, + transactionId: row.transaction_id, + payTime: row.pay_time, + createdAt: row.created_at, + updatedAt: row.updated_at, + } +} export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const userId = searchParams.get("userId") - if (!userId) { - return NextResponse.json({ code: 400, message: "缺少用户ID" }, { status: 400 }) + let rows: Record[] = [] + try { + if (userId) { + rows = (await query( + "SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC", + [userId] + )) as Record[] + } else { + // 管理后台:无 userId 时返回全部订单 + rows = (await query( + "SELECT * FROM orders ORDER BY created_at DESC" + )) as Record[] + } + } catch (e) { + console.error("[Karuo] Orders query error:", e) + // 表可能未初始化,返回空列表 + rows = [] } - // In production, fetch from database - // For now, return mock data - const orders = [] - - console.log("[Karuo] Fetching orders for user:", userId) + const orders = rows.map(rowToOrder) return NextResponse.json({ code: 0, message: "获取成功", data: orders, + success: true, + orders, }) } catch (error) { console.error("[Karuo] Get orders error:", error) diff --git a/app/api/payment/wechat/transfer/notify/route.ts b/app/api/payment/wechat/transfer/notify/route.ts new file mode 100644 index 0000000..c32cfa4 --- /dev/null +++ b/app/api/payment/wechat/transfer/notify/route.ts @@ -0,0 +1,65 @@ +/** + * 微信支付 - 商家转账到零钱 结果通知 + * 文档: 开发文档/提现功能完整技术文档.md + */ + +import { NextRequest, NextResponse } from 'next/server' +import { decryptResource } from '@/lib/wechat-transfer' +import { query } from '@/lib/db' + +const cfg = { + apiV3Key: process.env.WECHAT_API_V3_KEY || process.env.WECHAT_MCH_KEY || '', +} + +export async function POST(request: NextRequest) { + try { + const rawBody = await request.text() + const data = JSON.parse(rawBody) as { + event_type?: string + resource?: { ciphertext: string; nonce: string; associated_data: string } + } + if (data.event_type !== 'MCHTRANSFER.BILL.FINISHED' || !data.resource) { + return NextResponse.json({ code: 'SUCCESS' }) + } + const { ciphertext, nonce, associated_data } = data.resource + const decrypted = decryptResource( + ciphertext, + nonce, + associated_data, + cfg.apiV3Key + ) as { out_bill_no?: string; state?: string; transfer_bill_no?: string } + const outBillNo = decrypted.out_bill_no + const state = decrypted.state + const transferBillNo = decrypted.transfer_bill_no || '' + if (!outBillNo) { + return NextResponse.json({ code: 'SUCCESS' }) + } + const rows = await query('SELECT id, user_id, amount, status FROM withdrawals WHERE id = ?', [outBillNo]) as any[] + if (rows.length === 0) { + return NextResponse.json({ code: 'SUCCESS' }) + } + const w = rows[0] + if (w.status !== 'processing') { + return NextResponse.json({ code: 'SUCCESS' }) + } + if (state === 'SUCCESS') { + await query(` + UPDATE withdrawals SET status = 'success', processed_at = NOW(), transaction_id = ? WHERE id = ? + `, [transferBillNo, outBillNo]) + await query(` + UPDATE users SET withdrawn_earnings = withdrawn_earnings + ?, pending_earnings = GREATEST(0, pending_earnings - ?) WHERE id = ? + `, [w.amount, w.amount, w.user_id]) + } else { + await query(` + UPDATE withdrawals SET status = 'failed', processed_at = NOW(), error_message = ? WHERE id = ? + `, [state || '转账失败', outBillNo]) + await query(` + UPDATE users SET pending_earnings = pending_earnings + ? WHERE id = ? + `, [w.amount, w.user_id]) + } + return NextResponse.json({ code: 'SUCCESS' }) + } catch (e) { + console.error('[WechatTransferNotify]', e) + return NextResponse.json({ code: 'FAIL', message: '处理失败' }, { status: 500 }) + } +} diff --git a/app/api/withdraw/route.ts b/app/api/withdraw/route.ts index 1669024..3cda38e 100644 --- a/app/api/withdraw/route.ts +++ b/app/api/withdraw/route.ts @@ -52,15 +52,15 @@ export async function POST(request: NextRequest) { const user = users[0] - // 检查是否绑定支付方式(微信号或支付宝) - // 如果没有绑定,提示用户先绑定 + // 微信零钱提现需要 open_id(小程序/公众号登录获得) + const openId = user.open_id || '' const wechatId = user.wechat || user.wechat_id || '' const alipayId = user.alipay || '' - if (!wechatId && !alipayId) { + if (!openId && !alipayId) { return NextResponse.json({ success: false, - message: '请先在设置中绑定微信号或支付宝', + message: '提现到微信零钱需先使用微信登录;或绑定支付宝后提现到支付宝', needBind: true }) } @@ -101,20 +101,24 @@ export async function POST(request: NextRequest) { }) } - // 创建提现记录 + // 创建提现记录(微信零钱需保存 wechat_openid 供后台批准时调用商家转账到零钱) const withdrawId = `W${Date.now()}` const accountType = alipayId ? 'alipay' : 'wechat' const account = alipayId || wechatId try { await query(` - INSERT INTO withdrawals (id, user_id, amount, account_type, account, status, created_at) - VALUES (?, ?, ?, ?, ?, 'pending', NOW()) - `, [withdrawId, userId, amount, accountType, account]) + INSERT INTO withdrawals (id, user_id, amount, status, wechat_openid, created_at) + VALUES (?, ?, ?, 'pending', ?, NOW()) + `, [withdrawId, userId, amount, accountType === 'wechat' ? openId : null]) - // TODO: 实际调用微信企业付款或支付宝转账API - // 这里先模拟成功 - await query(`UPDATE withdrawals SET status = 'completed', completed_at = NOW() WHERE id = ?`, [withdrawId]) + // 微信零钱由后台批准时调用「商家转账到零钱」;支付宝/无 openid 时仅标记成功(需线下打款) + if (accountType !== 'wechat' || !openId) { + await query(`UPDATE withdrawals SET status = 'success', processed_at = NOW() WHERE id = ?`, [withdrawId]) + await query(` + UPDATE users SET withdrawn_earnings = withdrawn_earnings + ?, pending_earnings = GREATEST(0, pending_earnings - ?) WHERE id = ? + `, [amount, amount, userId]) + } } catch (e) { console.log('[Withdraw] 创建提现记录失败:', e) } diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..267254b --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,19 @@ +/** + * PM2 配置:用于 standalone 部署的服务器 + * 启动方式:node server.js(不要用 npm start / next start,standalone 无 next 命令) + * 使用:pm2 start ecosystem.config.cjs 或 PORT=3006 pm2 start server.js --name soul + */ +module.exports = { + apps: [ + { + name: 'soul', + script: 'server.js', + interpreter: 'node', + env: { + NODE_ENV: 'production', + PORT: 3006, + }, + cwd: undefined, // 以当前目录为准,部署时在 /www/wwwroot/soul + }, + ], +}; diff --git a/lib/wechat-transfer.ts b/lib/wechat-transfer.ts new file mode 100644 index 0000000..b8f6eff --- /dev/null +++ b/lib/wechat-transfer.ts @@ -0,0 +1,212 @@ +/** + * 微信支付 V3 - 商家转账到零钱 + * 文档: 开发文档/提现功能完整技术文档.md + */ + +import crypto from 'crypto' +import fs from 'fs' +import path from 'path' + +const BASE_URL = 'https://api.mch.weixin.qq.com' + +export interface WechatTransferConfig { + mchId: string + appId: string + apiV3Key: string + privateKeyPath?: string + privateKeyContent?: string + certSerialNo: string +} + +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 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 || '' + return { + mchId, + appId, + apiV3Key, + privateKeyPath: keyPath, + privateKeyContent: keyContent, + certSerialNo, + } +} + +function getPrivateKey(): string { + const cfg = getConfig() + if (cfg.privateKeyContent) { + const key = cfg.privateKeyContent.replace(/\\n/g, '\n') + if (!key.includes('BEGIN')) { + return `-----BEGIN PRIVATE KEY-----\n${key}\n-----END PRIVATE KEY-----` + } + return key + } + if (cfg.privateKeyPath) { + const p = path.isAbsolute(cfg.privateKeyPath) ? cfg.privateKeyPath : path.join(process.cwd(), cfg.privateKeyPath) + return fs.readFileSync(p, 'utf8') + } + throw new Error('微信商户私钥未配置: WECHAT_MCH_PRIVATE_KEY 或 WECHAT_KEY_PATH') +} + +function generateNonce(length = 32): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let s = '' + for (let i = 0; i < length; i++) { + s += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return s +} + +/** 生成请求签名 */ +function buildSignature(method: string, urlPath: string, timestamp: string, nonce: string, body: string): string { + const message = `${method}\n${urlPath}\n${timestamp}\n${nonce}\n${body}\n` + const key = getPrivateKey() + const sign = crypto.createSign('RSA-SHA256') + sign.update(message) + return sign.sign(key, 'base64') +} + +/** 构建 Authorization 头 */ +function buildAuthorization(timestamp: string, nonce: string, signature: string): string { + const cfg = getConfig() + return `WECHATPAY2-SHA256-RSA2048 mchid="${cfg.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${cfg.certSerialNo}"` +} + +export interface CreateTransferParams { + openid: string + amountFen: number + outDetailNo: string + outBatchNo?: string + transferRemark?: string +} + +export interface CreateTransferResult { + success: boolean + outBatchNo?: string + batchId?: string + createTime?: string + batchStatus?: string + errorCode?: string + errorMessage?: string +} + +/** + * 发起商家转账到零钱 + */ +export async function createTransfer(params: CreateTransferParams): Promise { + const cfg = getConfig() + if (!cfg.mchId || !cfg.appId || !cfg.apiV3Key || !cfg.certSerialNo) { + return { success: false, errorCode: 'CONFIG_ERROR', errorMessage: '微信转账配置不完整' } + } + + const urlPath = '/v3/transfer/batches' + const outBatchNo = params.outBatchNo || `B${Date.now()}${Math.random().toString(36).slice(2, 8)}` + const body = { + appid: cfg.appId, + out_batch_no: outBatchNo, + batch_name: '提现', + batch_remark: params.transferRemark || '用户提现', + total_amount: params.amountFen, + total_num: 1, + transfer_detail_list: [ + { + out_detail_no: params.outDetailNo, + transfer_amount: params.amountFen, + transfer_remark: params.transferRemark || '提现', + openid: params.openid, + }, + ], + transfer_scene_id: '1005', + transfer_scene_report_infos: [ + { info_type: '岗位类型', info_content: '兼职人员' }, + { info_type: '报酬说明', info_content: '当日兼职费' }, + ], + } + const bodyStr = JSON.stringify(body) + const timestamp = Math.floor(Date.now() / 1000).toString() + const nonce = generateNonce() + const signature = buildSignature('POST', urlPath, timestamp, nonce, bodyStr) + const authorization = buildAuthorization(timestamp, nonce, signature) + + const res = await fetch(`${BASE_URL}${urlPath}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: authorization, + 'User-Agent': 'Soul-Withdraw/1.0', + }, + body: bodyStr, + }) + const data = (await res.json()) as Record + if (res.ok && res.status >= 200 && res.status < 300) { + return { + success: true, + outBatchNo: data.out_batch_no as string, + batchId: data.batch_id as string, + createTime: data.create_time as string, + batchStatus: data.batch_status as string, + } + } + return { + success: false, + errorCode: (data.code as string) || 'UNKNOWN', + errorMessage: (data.message as string) || (data.error as string) as string || '请求失败', + } +} + +/** + * 解密回调 resource(AEAD_AES_256_GCM) + */ +export function decryptResource( + ciphertext: string, + nonce: string, + associatedData: string, + apiV3Key: string +): Record { + if (apiV3Key.length !== 32) { + throw new Error('APIv3密钥必须为32字节') + } + const key = Buffer.from(apiV3Key, 'utf8') + const ct = Buffer.from(ciphertext, 'base64') + const authTag = ct.subarray(ct.length - 16) + const data = ct.subarray(0, ct.length - 16) + const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(nonce, 'utf8')) + decipher.setAuthTag(authTag) + decipher.setAAD(Buffer.from(associatedData, 'utf8')) + const dec = decipher.update(data) as Buffer + const final = decipher.final() as Buffer + const json = Buffer.concat([dec, final]).toString('utf8') + return JSON.parse(json) as Record +} + +/** + * 验证回调签名(需平台公钥,可选) + */ +export function verifyCallbackSignature( + timestamp: string, + nonce: string, + body: string, + signature: string, + publicKeyPem: string +): boolean { + const message = `${timestamp}\n${nonce}\n${body}\n` + const sigBuf = Buffer.from(signature, 'base64') + const verify = crypto.createVerify('RSA-SHA256') + verify.update(message) + return verify.verify(publicKeyPem, sigBuf) +} + +export interface TransferNotifyDecrypted { + mch_id: string + out_bill_no: string + transfer_bill_no?: string + transfer_amount?: number + state: string + openid?: string + create_time?: string + update_time?: string +} diff --git a/miniprogram/app.json b/miniprogram/app.json index fe03e7b..55da589 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -52,7 +52,8 @@ } }, "requiredPrivateInfos": [ - "getLocation" + "getLocation", + "chooseAddress" ], "lazyCodeLoading": "requiredComponents", "style": "v2", diff --git a/package.json b/package.json index 077b460..6fb0699 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "next build", "dev": "next dev", "lint": "eslint .", - "start": "next start -p 3006" + "start": "npx next start -p 3006" }, "dependencies": { "@emotion/is-prop-valid": "latest", diff --git a/requirements-deploy.txt b/requirements-deploy.txt new file mode 100644 index 0000000..1206e91 --- /dev/null +++ b/requirements-deploy.txt @@ -0,0 +1,3 @@ +# 仅用于「部署到宝塔」脚本,非项目运行依赖 +# 使用: pip install -r requirements-deploy.txt +paramiko>=2.9.0 diff --git a/scripts/deploy_baota.py b/scripts/deploy_baota.py new file mode 100644 index 0000000..f71cf9b --- /dev/null +++ b/scripts/deploy_baota.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Soul 创业派对 - 宝塔一键部署(跨平台) + +一键执行: python scripts/deploy_baota.py +依赖: pip install paramiko + +流程:本地 pnpm build -> 打包 .next/standalone -> 上传 -> 服务器解压 -> PM2 运行 node server.js +(不从 git 拉取,不在服务器安装依赖或构建。) +""" + +from __future__ import print_function + +import os +import sys +import getpass +import shutil +import subprocess +import tarfile +import tempfile +import threading +from pathlib import Path + +if sys.platform == 'win32': + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + + +def log(msg, step=None): + """输出并立即刷新,便于看到进度""" + if step is not None: + print('[步骤 %s] %s' % (step, msg)) + else: + print(msg) + sys.stdout.flush() + sys.stderr.flush() + + +def log_err(msg): + print('>>> 错误: %s' % msg, file=sys.stderr) + sys.stderr.flush() + + +try: + import paramiko +except ImportError: + log('请先安装: pip install paramiko') + sys.exit(1) + +# 默认配置(与 开发文档/服务器管理 一致) +# 应用端口须与 端口配置表 及 Nginx proxy_pass 一致(soul -> 3006) +CFG = { + 'host': os.environ.get('DEPLOY_HOST', '42.194.232.22'), + 'port': int(os.environ.get('DEPLOY_PORT', '22')), + 'app_port': int(os.environ.get('DEPLOY_APP_PORT', '3006')), + 'user': os.environ.get('DEPLOY_USER', 'root'), + 'pwd': os.environ.get('DEPLOY_PASSWORD', 'Zhiqun1984'), + 'path': os.environ.get('DEPLOY_PROJECT_PATH', '/www/wwwroot/soul'), + 'branch': os.environ.get('DEPLOY_BRANCH', 'soul-content'), + 'pm2': os.environ.get('DEPLOY_PM2_APP', 'soul'), + 'url': os.environ.get('DEPLOY_SITE_URL', 'https://soul.quwanzhi.com'), + 'key': os.environ.get('DEPLOY_SSH_KEY') or None, +} + +EXCLUDE = { + 'node_modules', '.next', '.git', '.gitignore', '.cursorrules', + 'scripts', 'miniprogram', '开发文档', 'addons', 'book', + '__pycache__', '.DS_Store', '*.log', 'deploy_config.json', + 'requirements-deploy.txt', '*.bat', '*.ps1', +} + + +def run(ssh, cmd, desc, step_label=None, ignore_err=False): + """执行远程命令,打印完整输出,失败时明确标出错误和退出码""" + if step_label: + log(desc, step_label) + else: + log(desc) + print(' $ %s' % (cmd[:100] + '...' if len(cmd) > 100 else cmd)) + sys.stdout.flush() + stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + out = stdout.read().decode('utf-8', errors='replace') + err = stderr.read().decode('utf-8', errors='replace') + code = stdout.channel.recv_exit_status() + if out: + print(out) + sys.stdout.flush() + if err: + print(err, file=sys.stderr) + sys.stderr.flush() + if code != 0: + log_err('退出码: %s | %s' % (code, desc)) + if err and len(err.strip()) > 0: + for line in err.strip().split('\n')[-5:]: + print(' stderr: %s' % line, file=sys.stderr) + sys.stderr.flush() + return ignore_err + return True + + +def _read_and_print(stream, prefix=' ', is_stderr=False): + """后台线程:不断读 stream 并打印,用于实时输出""" + import threading + out = sys.stderr if is_stderr else sys.stdout + try: + while True: + line = stream.readline() + if not line: + break + s = line.decode('utf-8', errors='replace').rstrip() + if s: + print('%s%s' % (prefix, s), file=out) + out.flush() + except Exception: + pass + + +def run_stream(ssh, cmd, desc, step_label=None, ignore_err=False): + """执行远程命令并实时输出(npm install / build 不卡住、能看到进度)""" + if step_label: + log(desc, step_label) + else: + log(desc) + print(' $ %s' % (cmd[:100] + '...' if len(cmd) > 100 else cmd)) + sys.stdout.flush() + stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) + t1 = threading.Thread(target=_read_and_print, args=(stdout, ' ', False)) + t2 = threading.Thread(target=_read_and_print, args=(stderr, ' [stderr] ', True)) + t1.daemon = True + t2.daemon = True + t1.start() + t2.start() + t1.join() + t2.join() + code = stdout.channel.recv_exit_status() + if code != 0: + log_err('退出码: %s | %s' % (code, desc)) + return ignore_err + return True + + +def _tar_filter(ti): + n = ti.name.replace('\\', '/') + if 'node_modules' in n or '.next' in n or '.git' in n: + return None + if '/scripts/' in n or n.startswith('scripts/'): + return None + if '/miniprogram/' in n or n.startswith('miniprogram/'): + return None + if '/开发文档/' in n or '开发文档/' in n: + return None + if '/addons/' in n or '/book/' in n: + return None + return ti + + +def make_tarball(root_dir): + root = Path(root_dir).resolve() + tmp = tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) + tmp.close() + with tarfile.open(tmp.name, 'w:gz') as tar: + for item in root.iterdir(): + name = item.name + if name in EXCLUDE or name.endswith('.md') or (name.startswith('.') and name != '.cursorrules'): + continue + if name.startswith('deploy_config') or name.endswith('.bat') or name.endswith('.ps1'): + continue + arcname = name + tar.add(str(item), arcname=arcname, filter=_tar_filter) + return tmp.name + + +def run_local_build(local_root, step_label=None): + """本地执行 pnpm build,实时输出""" + root = Path(local_root).resolve() + if step_label: + log('本地构建 pnpm build(standalone)', step_label) + else: + log('本地构建 pnpm build(standalone)') + cmd_str = 'pnpm build' + print(' $ %s' % cmd_str) + sys.stdout.flush() + try: + # Windows 下用 shell=True,否则子进程 PATH 里可能没有 pnpm + use_shell = sys.platform == 'win32' + p = subprocess.Popen( + cmd_str if use_shell else ['pnpm', 'build'], + cwd=str(root), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + universal_newlines=True, + encoding='utf-8', + errors='replace', + shell=use_shell, + ) + for line in p.stdout: + print(' %s' % line.rstrip()) + sys.stdout.flush() + code = p.wait() + if code != 0: + log_err('本地构建失败,退出码 %s' % code) + return False + return True + except Exception as e: + log_err('本地构建异常: %s' % e) + return False + + +def make_standalone_tarball(local_root): + """ + 在 next.config 已设置 output: 'standalone' 且已执行 pnpm build 的前提下, + 将 .next/static 和 public 复制进 .next/standalone,再打包 .next/standalone 目录内容。 + 返回生成的 tar.gz 路径。 + """ + root = Path(local_root).resolve() + standalone_dir = root / '.next' / 'standalone' + static_src = root / '.next' / 'static' + public_src = root / 'public' + if not standalone_dir.is_dir(): + raise FileNotFoundError('.next/standalone 不存在,请先执行 pnpm build') + # Next 要求将 .next/static 和 public 复制进 standalone + standalone_next = standalone_dir / '.next' + standalone_next.mkdir(parents=True, exist_ok=True) + if static_src.is_dir(): + dest_static = standalone_next / 'static' + if dest_static.exists(): + shutil.rmtree(dest_static) + shutil.copytree(static_src, dest_static) + if public_src.is_dir(): + dest_public = standalone_dir / 'public' + if dest_public.exists(): + shutil.rmtree(dest_public) + shutil.copytree(public_src, dest_public) + # 复制 PM2 配置到 standalone,便于服务器上用 pm2 start ecosystem.config.cjs + ecosystem_src = root / 'ecosystem.config.cjs' + if ecosystem_src.is_file(): + shutil.copy2(ecosystem_src, standalone_dir / 'ecosystem.config.cjs') + # 打包 standalone 目录「内容」,使解压到服务器项目目录后根目录即为 server.js + tmp = tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) + tmp.close() + with tarfile.open(tmp.name, 'w:gz') as tar: + for item in standalone_dir.iterdir(): + arcname = item.name + tar.add(str(item), arcname=arcname, recursive=True) + return tmp.name + + +def deploy_by_upload_standalone(ssh, sftp, local_root, remote_path, pm2_name, step_start, app_port=None): + """本地 standalone 构建 -> 打包 -> 上传 -> 解压 -> PM2 用 node server.js 启动(PORT 与 Nginx 一致)""" + step = step_start + root = Path(local_root).resolve() + + # 步骤 1: 本地构建 + log('本地执行 pnpm build(standalone)', step) + step += 1 + if not run_local_build(str(root), step_label=None): + return False + sys.stdout.flush() + + # 步骤 2: 打包 standalone + log('打包 .next/standalone(含 static、public)', step) + step += 1 + try: + tarball = make_standalone_tarball(str(root)) + size_mb = os.path.getsize(tarball) / 1024 / 1024 + log('打包完成,约 %.2f MB' % size_mb) + except FileNotFoundError as e: + log_err(str(e)) + return False + except Exception as e: + log_err('打包失败: %s' % e) + return False + sys.stdout.flush() + + # 步骤 3: 上传 + log('上传到服务器 /tmp/soul_standalone.tar.gz', step) + step += 1 + remote_tar = '/tmp/soul_standalone.tar.gz' + try: + sftp.put(tarball, remote_tar) + log('上传完成') + except Exception as e: + log_err('上传失败: %s' % e) + os.unlink(tarball) + return False + os.unlink(tarball) + sys.stdout.flush() + + # 步骤 4: 清理并解压(保留 .env 等隐藏配置) + log('清理旧文件并解压 standalone', step) + step += 1 + run(ssh, 'cd %s && rm -rf app components lib public styles .next *.json *.js *.ts *.mjs *.css *.d.ts server.js node_modules 2>/dev/null; ls -la' % remote_path, '清理', step_label=None, ignore_err=True) + if not run(ssh, 'cd %s && tar -xzf %s' % (remote_path, remote_tar), '解压'): + log_err('解压失败,请检查服务器磁盘或路径') + return False + run(ssh, 'rm -f %s' % remote_tar, '删除临时包', ignore_err=True) + sys.stdout.flush() + + # 步骤 5: PM2 用 node server.js 启动,PORT 须与 Nginx proxy_pass 一致(默认 3006) + # 宝塔服务器上 pm2 可能不在默认 PATH,先注入常见路径 + port = app_port if app_port is not None else 3006 + log('PM2 启动 node server.js(PORT=%s)' % port, step) + pm2_cmd = ( + 'export PATH=/www/server/nodejs/v22.14.0/bin:/www/server/nvm/versions/node/*/bin:$PATH 2>/dev/null; ' + 'cd %s && (pm2 delete %s 2>/dev/null; PORT=%s pm2 start server.js --name %s)' + ) % (remote_path, pm2_name, port, pm2_name) + run(ssh, pm2_cmd, 'PM2 启动', ignore_err=True) + return True + + +def main(): + print('=' * 60) + print(' Soul 创业派对 - 宝塔一键部署') + print('=' * 60) + print(' %s@%s -> %s' % (CFG['user'], CFG['host'], CFG['path'])) + print('=' * 60) + sys.stdout.flush() + + # 步骤 1: 连接 + log('连接服务器 %s:%s' % (CFG['host'], CFG['port']), '1/6') + password = CFG.get('pwd') + if not CFG['key'] and not password: + password = getpass.getpass('请输入 SSH 密码: ') + sys.stdout.flush() + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + kw = {'hostname': CFG['host'], 'port': CFG['port'], 'username': CFG['user']} + if CFG['key']: + kw['key_filename'] = CFG['key'] + else: + kw['password'] = password + ssh.connect(**kw) + log('连接成功') + except Exception as e: + log_err('连接失败: %s' % e) + return 1 + sys.stdout.flush() + + p, pm = CFG['path'], CFG['pm2'] + sftp = ssh.open_sftp() + + # 步骤 2~6: 本地 build -> 打包 -> 上传 -> 解压 -> PM2 启动 + log('本地打包上传部署(不从 git 拉取)', '2/6') + local_root = Path(__file__).resolve().parent.parent + if not deploy_by_upload_standalone(ssh, sftp, str(local_root), p, pm, step_start=2, app_port=CFG.get('app_port')): + sftp.close() + ssh.close() + log_err('部署中断,请根据上方错误信息排查') + return 1 + + sftp.close() + ssh.close() + + print('') + print('=' * 60) + print(' 部署完成') + print(' 前台: %s' % CFG['url']) + print(' 后台: %s/admin' % CFG['url']) + print('=' * 60) + sys.stdout.flush() + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/deploy_config.example.json b/scripts/deploy_config.example.json new file mode 100644 index 0000000..38406b9 --- /dev/null +++ b/scripts/deploy_config.example.json @@ -0,0 +1,12 @@ +{ + "server_host": "42.194.232.22", + "server_port": 22, + "server_user": "root", + "project_path": "/www/wwwroot/soul", + "branch": "soul-content", + "pm2_app_name": "soul", + "site_url": "https://soul.quwanzhi.com", + "ssh_key_path": null, + "use_pnpm": true, + "_comment": "复制本文件为 deploy_config.json,填写真实信息。不要将 deploy_config.json 提交到 Git。ssh_key_path 填私钥路径则用密钥登录,否则用密码。" +} diff --git a/一键部署小程序.bat b/一键部署小程序.bat deleted file mode 100644 index 4f48a06..0000000 --- a/一键部署小程序.bat +++ /dev/null @@ -1,22 +0,0 @@ -@echo off -chcp 65001 >nul -title Soul创业派对 - 小程序一键部署 - -echo. -echo ======================================== -echo Soul创业派对 - 小程序一键部署 -echo ======================================== -echo. - -python "一键部署小程序.py" - -if errorlevel 1 ( - echo. - echo [错误] 部署失败 - pause - exit /b 1 -) - -echo. -echo [成功] 部署完成 -pause diff --git a/一键部署小程序.py b/一键部署小程序.py deleted file mode 100644 index 5dc3791..0000000 --- a/一键部署小程序.py +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Soul创业派对 - 小程序一键部署脚本 -功能: -1. 打开微信开发者工具 -2. 自动编译小程序 -3. 上传到微信平台 -4. 显示审核指引 -""" - -import os -import sys -import time -import subprocess -from pathlib import Path - -# 修复Windows控制台编码问题 -if sys.platform == 'win32': - import io - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') - sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') - -# 配置信息 -CONFIG = { - 'appid': 'wxb8bbb2b10dec74aa', - 'project_path': Path(__file__).parent / 'miniprogram', - 'version': '1.0.1', - 'desc': 'Soul创业派对 - 1:1完整还原Web功能' -} - -# 微信开发者工具可能的路径 -DEVTOOLS_PATHS = [ - r"D:\微信web开发者工具\微信开发者工具.exe", - r"C:\Program Files (x86)\Tencent\微信web开发者工具\微信开发者工具.exe", - r"C:\Program Files\Tencent\微信web开发者工具\微信开发者工具.exe", -] - - -def print_banner(): - """打印横幅""" - print("\n" + "=" * 70) - print(" 🚀 Soul创业派对 - 小程序一键部署") - print("=" * 70 + "\n") - - -def find_devtools(): - """查找微信开发者工具""" - print("🔍 正在查找微信开发者工具...") - - for devtools_path in DEVTOOLS_PATHS: - if os.path.exists(devtools_path): - print(f"✅ 找到微信开发者工具: {devtools_path}\n") - return devtools_path - - print("❌ 未找到微信开发者工具") - print("\n请确保已安装微信开发者工具") - print("下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html\n") - return None - - -def open_devtools(devtools_path): - """打开微信开发者工具""" - print("📱 正在打开微信开发者工具...") - - try: - # 使用项目路径打开开发者工具 - subprocess.Popen([devtools_path, str(CONFIG['project_path'])]) - print("✅ 微信开发者工具已打开\n") - print("⏳ 等待开发者工具启动(10秒)...") - time.sleep(10) - return True - except Exception as e: - print(f"❌ 打开失败: {e}") - return False - - -def check_private_key(): - """检查上传密钥""" - key_path = CONFIG['project_path'] / 'private.key' - - if not key_path.exists(): - print("\n" + "⚠" * 35) - print("\n❌ 未找到上传密钥文件 private.key\n") - print("📥 获取密钥步骤:") - print(" 1. 访问 https://mp.weixin.qq.com/") - print(" 2. 登录小程序后台") - print(" 3. 开发管理 → 开发设置 → 小程序代码上传密钥") - print(" 4. 点击「生成」,下载密钥文件") - print(" 5. 将下载的 private.*.key 重命名为 private.key") - print(f" 6. 放到目录: {CONFIG['project_path']}") - print("\n💡 温馨提示:") - print(" - 密钥只能生成一次,请妥善保管") - print(" - 如需重新生成,需要到后台重置密钥") - print("\n" + "⚠" * 35 + "\n") - return False - - print(f"✅ 找到密钥文件: private.key\n") - return True - - -def upload_miniprogram(): - """上传小程序""" - print("\n" + "-" * 70) - print("📦 准备上传小程序到微信平台...") - print("-" * 70 + "\n") - - print(f"📂 项目路径: {CONFIG['project_path']}") - print(f"🆔 AppID: {CONFIG['appid']}") - print(f"📌 版本号: {CONFIG['version']}") - print(f"📝 描述: {CONFIG['desc']}\n") - - # 检查密钥 - if not check_private_key(): - return False - - # 切换到miniprogram目录执行上传脚本 - upload_script = CONFIG['project_path'] / '上传小程序.py' - - if not upload_script.exists(): - print(f"❌ 未找到上传脚本: {upload_script}") - return False - - print("⏳ 正在执行上传脚本...\n") - - try: - result = subprocess.run( - [sys.executable, str(upload_script)], - cwd=CONFIG['project_path'], - capture_output=False, # 直接显示输出 - text=True - ) - - return result.returncode == 0 - except Exception as e: - print(f"❌ 上传出错: {e}") - return False - - -def show_next_steps(): - """显示后续步骤""" - print("\n" + "=" * 70) - print("✅ 部署完成!") - print("=" * 70 + "\n") - - print("📱 后续操作:") - print("\n1️⃣ 在微信开发者工具中:") - print(" - 查看编译结果") - print(" - 使用模拟器或真机预览测试") - print(" - 确认所有功能正常") - - print("\n2️⃣ 提交审核:") - print(" - 访问 https://mp.weixin.qq.com/") - print(" - 登录小程序后台") - print(" - 版本管理 → 开发版本") - print(" - 选择刚上传的版本 → 提交审核") - - print("\n3️⃣ 审核材料准备:") - print(" - 小程序演示视频(可选)") - print(" - 测试账号(如有登录功能)") - print(" - 功能说明(突出核心功能)") - - print("\n4️⃣ 审核通过后:") - print(" - 在后台点击「发布」") - print(" - 用户即可在微信中搜索使用") - - print("\n" + "=" * 70 + "\n") - - -def main(): - """主函数""" - print_banner() - - # 1. 查找微信开发者工具 - devtools_path = find_devtools() - if not devtools_path: - print("💡 请先安装微信开发者工具,然后重新运行本脚本") - return False - - # 2. 打开微信开发者工具 - if not open_devtools(devtools_path): - print("❌ 无法打开微信开发者工具") - return False - - print("\n✅ 微信开发者工具已打开,项目已加载") - print("\n💡 现在你可以:") - print(" 1. 在开发者工具中查看和测试小程序") - print(" 2. 使用模拟器或扫码真机预览") - print(" 3. 确认功能正常后,准备上传\n") - - # 3. 询问是否立即上传 - print("-" * 70) - user_input = input("\n是否立即上传到微信平台?(y/n,默认n): ").strip().lower() - - if user_input == 'y': - if upload_miniprogram(): - show_next_steps() - return True - else: - print("\n❌ 上传失败") - print("\n💡 你可以:") - print(" 1. 检查 private.key 是否正确") - print(" 2. 确保已开启开发者工具的「服务端口」") - print(" 3. 或在开发者工具中手动点击「上传」按钮\n") - return False - else: - print("\n✅ 开发者工具已就绪,你可以:") - print(" 1. 在开发者工具中测试小程序") - print(" 2. 准备好后,运行本脚本并选择上传") - print(" 3. 或直接在开发者工具中点击「上传」按钮\n") - return True - - -if __name__ == '__main__': - try: - success = main() - sys.exit(0 if success else 1) - except KeyboardInterrupt: - print("\n\n⚠️ 用户取消操作") - sys.exit(1) - except Exception as e: - print(f"\n❌ 发生错误: {e}") - import traceback - traceback.print_exc() - sys.exit(1) diff --git a/开发文档/8、部署/宝塔配置检查说明.md b/开发文档/8、部署/宝塔配置检查说明.md new file mode 100644 index 0000000..946e647 --- /dev/null +++ b/开发文档/8、部署/宝塔配置检查说明.md @@ -0,0 +1,77 @@ +# Soul 项目宝塔配置检查说明 + +> 用于排查 soul.quwanzhi.com 在宝塔上的 Nginx / PM2 / 端口 配置问题。 + +--- + +## 一、已发现并修复的问题 + +### 1. 应用端口与 Nginx 不一致(已修复) + +- **现象**:部署脚本用 `pm2 start server.js --name soul` 启动,未指定端口。Next.js standalone 默认监听 **3000**。 +- **宝塔约定**:根据 `开发文档/服务器管理/references/端口配置表.md`,soul 使用端口 **3006**,Nginx 反代到 `127.0.0.1:3006`。 +- **结果**:应用实际在 3000 监听,Nginx 请求 3006 → 无进程 → **502 Bad Gateway**。 +- **修复**:`scripts/deploy_baota.py` 已改为启动时设置 `PORT=3006`(可通过环境变量 `DEPLOY_APP_PORT` 覆盖),保证与 Nginx 一致。 + +--- + +## 二、宝塔侧需自检的配置 + +### 1. Nginx 反向代理 + +- **域名**:soul.quwanzhi.com +- **要求**:`proxy_pass http://127.0.0.1:3006;`(与端口配置表一致) +- **检查**:宝塔 → 网站 → soul.quwanzhi.com → 设置 → 反向代理 / 配置文件,确认 `proxy_pass` 指向 `127.0.0.1:3006`。 +- **SSL**:若走 HTTPS,确认已配置 443 与证书(端口配置表注明使用通配符证书)。 + +### 2. PM2 与部署脚本一致 + +- **项目名**:soul(与 `deploy_baota.py` 中 `DEPLOY_PM2_APP` 一致) +- **启动方式**:**必须用 `node server.js`**,工作目录 `/www/wwwroot/soul`,环境变量 `PORT=3006`。 +- **不要用**:`npm start` / `next start`。standalone 部署后没有完整 node_modules,也没有 `next` 命令,会报 `next: command not found`。 +- **宝塔 PM2 管理器**:启动文件填 `server.js`,启动命令填 `node server.js`(或选「Node 项目」后只填 `server.js`),环境变量添加 `PORT=3006`。也可用 `pm2 start ecosystem.config.cjs`(项目根目录已提供该文件)。 +- **注意**:若同时在宝塔「PM2 管理器」里添加了同名项目,可能产生 root 与 www 用户冲突,建议只保留一种方式(要么只用脚本部署 + 命令行 PM2,要么只用宝塔 PM2 界面)。 + +### 3. 项目目录与端口 + +- **项目路径**:`/www/wwwroot/soul`(与 `DEPLOY_PROJECT_PATH` 一致) +- **应用端口**:3006(与端口配置表、Nginx、部署脚本中的 `PORT` 一致) + +--- + +## 三、快速检查命令(SSH 到服务器后执行) + +```bash +# 1. 应用是否在 3006 监听 +ss -tlnp | grep 3006 + +# 2. PM2 列表(是否有 soul,状态 online) +pm2 list + +# 3. Nginx 配置是否包含 soul 且 proxy_pass 为 3006 +grep -r "soul\|3006" /www/server/panel/vhost/nginx/ + +# 4. Nginx 语法 +nginx -t +``` + +--- + +## 四、环境变量(可选) + +部署时若需改端口,可在本机执行脚本前设置: + +```bash +set DEPLOY_APP_PORT=3006 +python scripts/deploy_baota.py +``` + +或修改 `scripts/deploy_baota.py` 中 `CFG['app_port']` 的默认值(当前为 3006)。 + +--- + +## 五、参考文档 + +- 端口与域名:`开发文档/服务器管理/references/端口配置表.md` +- 常见问题:`开发文档/服务器管理/references/常见问题手册.md` +- 部署步骤:`DEPLOYMENT.md`(宝塔部署章节) diff --git a/开发文档/8、部署/当前项目部署到线上.md b/开发文档/8、部署/当前项目部署到线上.md new file mode 100644 index 0000000..e15ebde --- /dev/null +++ b/开发文档/8、部署/当前项目部署到线上.md @@ -0,0 +1,85 @@ +# 当前项目部署到线上 + +用 **开发文档/服务器管理** 和 **开发文档/小程序管理** 把本仓库(Soul 创业派对)部署到线上。 + +--- + +## 一、Web 与后台(Next.js) + +**服务器**:与 开发文档/服务器管理 一致 +- 小型宝塔:`42.194.232.22` +- 项目路径:`/www/wwwroot/soul` +- 端口:3006,域名:https://soul.quwanzhi.com + +**凭证**:与 服务器管理/SKILL.md 一致(root / Zhiqun1984),已写在项目部署脚本里。 + +### 操作(任选其一) + +**方式 A:用本仓库脚本(推荐,Windows 可用)** + +```bash +cd E:\Gongsi\Mycontent +python scripts/deploy_baota.py +``` + +- 脚本里已使用 服务器管理 的 root / Zhiqun1984,无需再输入密码。 +- 流程:SSH → 拉代码 → 安装依赖 → 构建 → PM2 重启。 + +**方式 B:用 服务器管理 的一键部署** + +```bash +cd 开发文档/服务器管理/scripts +python 一键部署.py soul E:\Gongsi\Mycontent +``` + +- 需要本机有 `sshpass`(Linux/Mac 常见,Windows 需单独装)。 +- 流程:本地打包 → scp 上传 → 服务器解压、安装、构建、重启。 + +--- + +## 二、小程序 + +**AppID**:`wxb8bbb2b10dec74aa`(与 开发文档/小程序管理/apps_config.json 中 soul-party 一致) + +### 方式 A:用本仓库脚本(最简单) + +1. 在微信公众平台下载「小程序代码上传密钥」,重命名为 `private.key`,放到 `miniprogram/` 目录。 +2. 在项目根目录执行: + +```bash +cd E:\Gongsi\Mycontent\miniprogram +python 上传小程序.py +``` + +### 方式 B:用 小程序管理(多小程序、提审、发布) + +1. 打开 `开发文档/小程序管理/scripts/apps_config.json`,把 soul-party 的 `project_path` 改成你本机路径,例如: + - Windows:`E:/Gongsi/Mycontent/miniprogram` + - Mac:`/Users/你的用户名/Gongsi/Mycontent/miniprogram` +2. 若有上传密钥,把 `private_key_path` 填成密钥文件路径(或把 `private.key` 放在 miniprogram 下,脚本里一般会默认找)。 +3. 在 小程序管理 的 scripts 目录执行: + +```bash +cd 开发文档/小程序管理/scripts +python mp_deploy.py upload soul-party +# 或一键部署(上传+提审) +python mp_deploy.py deploy soul-party +``` + +- 需要已在微信开放平台配置第三方平台并填好 `apps_config.json` 里 `third_party_platform`。 + +--- + +## 三、总结 + +| 要部署的 | 推荐做法 | 命令/位置 | +|----------|----------|-----------| +| Web + 后台 | 用本仓库脚本(已对接 服务器管理 凭证) | `python scripts/deploy_baota.py` | +| 小程序上传 | 用本仓库 miniprogram 脚本 | `cd miniprogram` → `python 上传小程序.py` | +| 小程序多项目/提审/发布 | 用 小程序管理 | `开发文档/小程序管理/scripts/mp_deploy.py` | +| 服务器状态/SSL/多机 | 用 服务器管理 | `开发文档/服务器管理/scripts/` 下对应脚本 | + +上线后访问: + +- 前台:https://soul.quwanzhi.com +- 后台:https://soul.quwanzhi.com/admin diff --git a/开发文档/小程序管理/SKILL.md b/开发文档/小程序管理/SKILL.md new file mode 100644 index 0000000..4645ec8 --- /dev/null +++ b/开发文档/小程序管理/SKILL.md @@ -0,0 +1,1797 @@ +# 小程序管理技能 v3.0 + +> 📅 创建日期:2026-01-25 +> 📅 更新日期:2026-01-25(v3.0 全能版) +> 📋 通过微信开放平台API完整管理小程序的全生命周期:申请、认证、开发、审核、发布、运营 +> 🚀 支持多小程序管理、一键部署、自动认证检查、汇总报告 + +--- + +## 🎯 能力全景图 + +``` +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 小程序管理技能 v3.0 能力全景图 ║ +╠══════════════════════════════════════════════════════════════════════════════╣ +║ ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ 🔧 工具整合层 │ ║ +║ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │ ║ +║ │ │ 微信开发者 │ │ miniprogram │ │ 开放平台 │ │ GitHub │ │ ║ +║ │ │ 工具 CLI │ │ -ci (npm) │ │ API │ │ Actions │ │ ║ +║ │ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ │ ║ +║ ▼ ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ 📦 核心功能层 │ ║ +║ │ │ ║ +║ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ ║ +║ │ │ 多小程序 │ │ 项目 │ │ 一键 │ │ 汇总 │ │ ║ +║ │ │ 管理 │ │ 检查 │ │ 部署 │ │ 报告 │ │ ║ +║ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ ║ +║ │ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ │ ║ +║ ▼ ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ 🚀 部署流程 │ ║ +║ │ │ ║ +║ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ ║ +║ │ │ 检查 │ → │ 编译 │ → │ 上传 │ → │ 提审 │ → │ 审核 │ → │ 发布 │ │ ║ +║ │ │check │ │build │ │upload│ │audit │ │wait │ │release│ │ ║ +║ │ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ ║ +║ │ │ │ │ │ │ │ │ ║ +║ │ ▼ ▼ ▼ ▼ ▼ ▼ │ ║ +║ │ 自动化 自动化 自动化 自动化 等待 手动/自动 │ ║ +║ │ 1-7天 │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +``` + +--- + +## 📊 命令速查表 + +| 命令 | 说明 | 示例 | +|------|------|------| +| `mp_full.py report` | 生成所有小程序汇总报告 | `python3 mp_full.py report` | +| `mp_full.py check` | 检查项目问题 | `python3 mp_full.py check soul-party` | +| `mp_full.py auto` | 全自动部署(上传+提审) | `python3 mp_full.py auto soul-party` | +| `mp_deploy.py list` | 列出所有小程序 | `python3 mp_deploy.py list` | +| `mp_deploy.py add` | 添加新小程序 | `python3 mp_deploy.py add` | +| `mp_deploy.py cert-status` | 查询认证状态 | `python3 mp_deploy.py cert-status soul-party` | +| `mp_deploy.py cert-done` | 标记认证完成 | `python3 mp_deploy.py cert-done soul-party` | +| `mp_deploy.py release` | 发布上线 | `python3 mp_deploy.py release soul-party` | + +--- + +## 🎯 技能概述 + +本技能用于通过API实现微信小程序的完整管理,包括: + +### 核心能力 + +| 阶段 | 能力 | 说明 | +|------|------|------| +| **申请** | 快速注册小程序 | 复用公众号主体资质,无需300元认证费 | +| **认证** | 企业认证管理 | 自动检查认证状态、提醒过期、材料管理 | +| **配置** | 基础信息设置 | 名称、头像、介绍、类目管理 | +| **开发** | 代码管理 | 上传代码、生成体验版 | +| **审核** | 提审发布 | 提交审核、查询状态、撤回、发布 | +| **运营** | 接口管理 | 域名配置、隐私协议、接口权限 | +| **推广** | 小程序码 | 生成无限量小程序码 | +| **数据** | 数据分析 | 访问数据、用户画像 | + +### v2.0 新增能力 + +| 能力 | 命令 | 说明 | +|------|------|------| +| **多小程序管理** | `mp_deploy.py list` | 统一管理多个小程序 | +| **一键部署** | `mp_deploy.py deploy ` | 编译→上传→提审一步完成 | +| **认证管理** | `mp_deploy.py cert ` | 认证状态检查、材料管理 | +| **快速上传** | `mp_deploy.py upload ` | 快速上传代码到开发版 | + +### 开源工具集成 + +| 工具 | 用途 | 安装方式 | +|------|------|----------| +| **miniprogram-ci** | 微信官方CI工具 | `npm install miniprogram-ci -g` | +| **微信开发者工具CLI** | 本地编译上传 | 安装开发者工具自带 | +| **multi-mini-ci** | 多平台小程序上传 | GitHub开源 | + +--- + +## 🚀 触发词 + +- 管理小程序 +- 小程序申请 +- 小程序审核 +- 小程序发布 +- 上传小程序代码 +- 配置小程序域名 +- 生成小程序码 +- 查看小程序状态 + +--- + +## 🏗️ 技术架构 + +### 管理方式对比 + +| 方式 | 优点 | 缺点 | 适用场景 | +|------|------|------|----------| +| **微信后台手动操作** | 无需开发 | 效率低,无法批量 | 单个小程序 | +| **微信开发者工具CLI** | 简单易用 | 功能有限 | 开发测试 | +| **开放平台API(本方案)** | 功能完整、可自动化 | 需要开发 | 批量管理、自动化 | + +### 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 小程序管理引擎 v1.0 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 第一层:认证层 │ │ +│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ 第三方平台 │ │ component_ │ │ authorizer_ │ │ │ +│ │ │ 认证信息 │ → │ access_token │ → │ access_token │ │ │ +│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 第二层:管理API层 │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ 注册 │ │ 配置 │ │ 代码 │ │ 审核 │ │ 运营 │ │ │ +│ │ │ 管理 │ │ 管理 │ │ 管理 │ │ 管理 │ │ 管理 │ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 第三层:本地CLI层 │ │ +│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ mp_manager.py │ │ 微信开发者 │ │ 自动化脚本 │ │ │ +│ │ │ Python CLI │ │ 工具 CLI │ │ Shell │ │ │ +│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 📋 前置条件 + +### 1. 微信开放平台账号 + +- 注册地址:https://open.weixin.qq.com/ +- 完成开发者资质认证(需企业资质) + +### 2. 创建第三方平台 + +登录开放平台后台 → 管理中心 → 第三方平台 → 创建 + +**必填信息**: +- 平台名称 +- 服务类型(选择"平台型"可管理多个小程序) +- 授权发起页域名 +- 消息与事件接收URL + +### 3. 获取关键凭证 + +| 凭证 | 说明 | 获取方式 | +|------|------|----------| +| `component_appid` | 第三方平台AppID | 开放平台后台 | +| `component_appsecret` | 第三方平台密钥 | 开放平台后台 | +| `component_verify_ticket` | 验证票据 | 微信每10分钟推送 | +| `component_access_token` | 平台调用凭证 | API获取,2小时有效 | +| `authorizer_access_token` | 授权方调用凭证 | API获取,2小时有效 | + +--- + +## 🔑 认证流程 + +### 获取 component_access_token + +```python +# POST https://api.weixin.qq.com/cgi-bin/component/api_component_token +{ + "component_appid": "你的第三方平台AppID", + "component_appsecret": "你的第三方平台密钥", + "component_verify_ticket": "微信推送的验证票据" +} + +# 返回 +{ + "component_access_token": "xxxx", + "expires_in": 7200 +} +``` + +### 获取预授权码 + +```python +# POST https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode +{ + "component_appid": "你的第三方平台AppID" +} + +# 返回 +{ + "pre_auth_code": "xxxx", + "expires_in": 600 +} +``` + +### 引导用户授权 + +``` +https://mp.weixin.qq.com/cgi-bin/componentloginpage? +component_appid=第三方平台AppID& +pre_auth_code=预授权码& +redirect_uri=授权回调地址& +auth_type=1 # 1=仅小程序,2=仅公众号,3=两者都可 +``` + +### 获取 authorizer_access_token + +```python +# POST https://api.weixin.qq.com/cgi-bin/component/api_query_auth +{ + "component_appid": "第三方平台AppID", + "authorization_code": "授权回调返回的code" +} + +# 返回 +{ + "authorization_info": { + "authorizer_appid": "授权方AppID", + "authorizer_access_token": "授权方调用凭证", + "expires_in": 7200, + "authorizer_refresh_token": "刷新令牌" + } +} +``` + +--- + +## 📱 核心API接口 + +### 一、小程序注册 + +#### 1.1 复用公众号资质快速注册 + +**前提**:已有认证的公众号 + +```python +# POST https://api.weixin.qq.com/cgi-bin/account/fastregister?access_token=ACCESS_TOKEN +{ + "ticket": "公众号扫码授权的ticket" +} + +# 返回 +{ + "errcode": 0, + "errmsg": "ok", + "appid": "新注册的小程序AppID", + "authorization_code": "授权码" +} +``` + +**优势**: +- 无需重新提交主体材料 +- 无需对公打款 +- 无需支付300元认证费 + +**限制**: +- 非个体户:每月可注册5个 +- 个体户:每月可注册1个 + +#### 1.2 快速注册企业小程序 + +**前提**:有企业营业执照 + +```python +# POST https://api.weixin.qq.com/cgi-bin/component/fastregisterminiprogram +{ + "name": "小程序名称", + "code": "统一社会信用代码", + "code_type": 1, # 1=营业执照 + "legal_persona_wechat": "法人微信号", + "legal_persona_name": "法人姓名", + "component_phone": "联系电话" +} +``` + +--- + +### 二、基础信息配置 + +#### 2.1 获取基础信息 + +```python +# POST https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo +# 无请求参数 + +# 返回 +{ + "appid": "小程序AppID", + "account_type": 2, # 2=小程序 + "principal_type": 1, # 1=企业 + "principal_name": "主体名称", + "realname_status": 1, # 1=已认证 + "nickname": "小程序名称", + "head_image_url": "头像URL", + "signature": "简介" +} +``` + +#### 2.2 修改名称 + +```python +# POST https://api.weixin.qq.com/wxa/setnickname?access_token=ACCESS_TOKEN +{ + "nick_name": "新名称", + "id_card": "管理员身份证号", + "license": "营业执照media_id", # 需要先上传 + "naming_other_stuff_1": "其他证明material_id" # 可选 +} +``` + +#### 2.3 修改头像 + +```python +# POST https://api.weixin.qq.com/cgi-bin/account/modifyheadimage?access_token=ACCESS_TOKEN +{ + "head_img_media_id": "头像图片media_id", # 需要先上传 + "x1": 0, "y1": 0, "x2": 1, "y2": 1 # 裁剪区域 +} +``` + +#### 2.4 修改简介 + +```python +# POST https://api.weixin.qq.com/cgi-bin/account/modifysignature?access_token=ACCESS_TOKEN +{ + "signature": "新的简介内容" # 4-120字 +} +``` + +--- + +### 三、类目管理 + +#### 3.1 获取可选类目 + +```python +# GET https://api.weixin.qq.com/cgi-bin/wxopen/getallcategories?access_token=ACCESS_TOKEN +``` + +#### 3.2 获取已设置类目 + +```python +# GET https://api.weixin.qq.com/cgi-bin/wxopen/getcategory?access_token=ACCESS_TOKEN +``` + +#### 3.3 添加类目 + +```python +# POST https://api.weixin.qq.com/cgi-bin/wxopen/addcategory?access_token=ACCESS_TOKEN +{ + "categories": [ + { + "first": 1, # 一级类目ID + "second": 2, # 二级类目ID + "certicates": [ # 资质证明 + {"key": "资质名称", "value": "media_id"} + ] + } + ] +} +``` + +#### 3.4 删除类目 + +```python +# POST https://api.weixin.qq.com/cgi-bin/wxopen/deletecategory?access_token=ACCESS_TOKEN +{ + "first": 1, + "second": 2 +} +``` + +--- + +### 四、域名配置 + +#### 4.1 设置服务器域名 + +```python +# POST https://api.weixin.qq.com/wxa/modify_domain?access_token=ACCESS_TOKEN +{ + "action": "set", # add/delete/set/get + "requestdomain": ["https://api.example.com"], + "wsrequestdomain": ["wss://ws.example.com"], + "uploaddomain": ["https://upload.example.com"], + "downloaddomain": ["https://download.example.com"], + "udpdomain": ["udp://udp.example.com"], + "tcpdomain": ["tcp://tcp.example.com"] +} +``` + +#### 4.2 设置业务域名 + +```python +# POST https://api.weixin.qq.com/wxa/setwebviewdomain?access_token=ACCESS_TOKEN +{ + "action": "set", # add/delete/set/get + "webviewdomain": ["https://web.example.com"] +} +``` + +--- + +### 五、隐私协议配置 + +#### 5.1 获取隐私协议设置 + +```python +# POST https://api.weixin.qq.com/cgi-bin/component/getprivacysetting?access_token=ACCESS_TOKEN +{ + "privacy_ver": 2 # 1=当前版本,2=开发版本 +} +``` + +#### 5.2 设置隐私协议 + +```python +# POST https://api.weixin.qq.com/cgi-bin/component/setprivacysetting?access_token=ACCESS_TOKEN +{ + "privacy_ver": 2, + "setting_list": [ + { + "privacy_key": "UserInfo", + "privacy_text": "用于展示用户头像和昵称" + }, + { + "privacy_key": "Location", + "privacy_text": "用于获取您的位置信息以推荐附近服务" + }, + { + "privacy_key": "PhoneNumber", + "privacy_text": "用于登录验证和订单通知" + } + ], + "owner_setting": { + "contact_email": "contact@example.com", + "contact_phone": "15880802661", + "notice_method": "弹窗提示" + } +} +``` + +**常用隐私字段**: + +| privacy_key | 说明 | +|-------------|------| +| `UserInfo` | 用户信息(头像、昵称) | +| `Location` | 地理位置 | +| `PhoneNumber` | 手机号 | +| `Album` | 相册 | +| `Camera` | 相机 | +| `Record` | 麦克风 | +| `Clipboard` | 剪切板 | +| `MessageFile` | 微信消息中的文件 | +| `ChooseAddress` | 收货地址 | +| `BluetoothInfo` | 蓝牙 | + +--- + +### 六、代码管理 + +#### 6.1 上传代码 + +```python +# POST https://api.weixin.qq.com/wxa/commit?access_token=ACCESS_TOKEN +{ + "template_id": 1, # 代码模板ID + "ext_json": "{\"extAppid\":\"授权方AppID\",\"ext\":{}}", + "user_version": "1.0.0", + "user_desc": "版本描述" +} +``` + +**注意**:需要先在第三方平台创建代码模板 + +#### 6.2 获取体验版二维码 + +```python +# GET https://api.weixin.qq.com/wxa/get_qrcode?access_token=ACCESS_TOKEN&path=pages/index/index +# 返回二维码图片 +``` + +#### 6.3 获取已上传的代码页面列表 + +```python +# GET https://api.weixin.qq.com/wxa/get_page?access_token=ACCESS_TOKEN + +# 返回 +{ + "errcode": 0, + "page_list": ["pages/index/index", "pages/my/my"] +} +``` + +--- + +### 七、审核管理 + +#### 7.1 提交审核 + +```python +# POST https://api.weixin.qq.com/wxa/submit_audit?access_token=ACCESS_TOKEN +{ + "item_list": [ + { + "address": "pages/index/index", + "tag": "电子书 阅读 创业", + "first_class": "教育", + "second_class": "在线教育", + "first_id": 1, + "second_id": 2, + "title": "首页" + } + ], + "preview_info": { + "video_id_list": [], + "pic_id_list": [] + }, + "version_desc": "版本说明", + "feedback_info": "反馈内容", + "feedback_stuff": "media_id" # 反馈附件 +} +``` + +#### 7.2 查询审核状态 + +```python +# POST https://api.weixin.qq.com/wxa/get_auditstatus?access_token=ACCESS_TOKEN +{ + "auditid": 1234567890 +} + +# 返回 +{ + "errcode": 0, + "status": 0, # 0=审核成功,1=审核被拒,2=审核中,3=已撤回,4=审核延后 + "reason": "拒绝原因", # status=1时返回 + "screenshot": "截图" +} +``` + +#### 7.3 查询最新审核状态 + +```python +# GET https://api.weixin.qq.com/wxa/get_latest_auditstatus?access_token=ACCESS_TOKEN +``` + +#### 7.4 撤回审核 + +```python +# GET https://api.weixin.qq.com/wxa/undocodeaudit?access_token=ACCESS_TOKEN +``` + +**限制**:单个小程序每天只能撤回1次 + +--- + +### 八、发布管理 + +#### 8.1 发布已审核通过的版本 + +```python +# POST https://api.weixin.qq.com/wxa/release?access_token=ACCESS_TOKEN +{} # 无请求参数 + +# 返回 +{ + "errcode": 0, + "errmsg": "ok" +} +``` + +#### 8.2 版本回退 + +```python +# GET https://api.weixin.qq.com/wxa/revertcoderelease?access_token=ACCESS_TOKEN +``` + +**限制**:只能回退到上一个版本,且只能回退一次 + +#### 8.3 获取可回退版本历史 + +```python +# GET https://api.weixin.qq.com/wxa/revertcoderelease?access_token=ACCESS_TOKEN&action=get_history_version +``` + +#### 8.4 分阶段发布 + +```python +# POST https://api.weixin.qq.com/wxa/grayrelease?access_token=ACCESS_TOKEN +{ + "gray_percentage": 10 # 灰度比例 1-100 +} +``` + +--- + +### 九、小程序码生成 + +#### 9.1 获取小程序码(有限制) + +```python +# POST https://api.weixin.qq.com/wxa/getwxacode?access_token=ACCESS_TOKEN +{ + "path": "pages/index/index?scene=123", + "width": 430, + "auto_color": false, + "line_color": {"r": 0, "g": 0, "b": 0}, + "is_hyaline": false # 是否透明背景 +} +# 返回图片二进制 +``` + +**限制**:每个path只能生成10万个 + +#### 9.2 获取无限小程序码(推荐) + +```python +# POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN +{ + "scene": "user_id=123&from=share", # 最长32字符 + "page": "pages/index/index", # 必须是已发布的页面 + "width": 430, + "auto_color": false, + "line_color": {"r": 0, "g": 0, "b": 0}, + "is_hyaline": false +} +# 返回图片二进制 +``` + +**注意**:scene参数需要在小程序中用`onLoad(options)`解析 + +#### 9.3 生成小程序短链接 + +```python +# POST https://api.weixin.qq.com/wxa/genwxashortlink?access_token=ACCESS_TOKEN +{ + "page_url": "pages/index/index?id=123", + "page_title": "页面标题", + "is_permanent": false # 是否永久有效 +} + +# 返回 +{ + "errcode": 0, + "link": "https://wxaurl.cn/xxxx" +} +``` + +--- + +### 十、接口权限管理 + +#### 10.1 查询接口调用额度 + +```python +# POST https://api.weixin.qq.com/cgi-bin/openapi/quota/get?access_token=ACCESS_TOKEN +{ + "cgi_path": "/wxa/getwxacode" +} + +# 返回 +{ + "quota": { + "daily_limit": 100000, + "used": 500, + "remain": 99500 + } +} +``` + +#### 10.2 重置接口调用次数 + +```python +# POST https://api.weixin.qq.com/cgi-bin/clear_quota?access_token=ACCESS_TOKEN +{ + "appid": "小程序AppID" +} +``` + +**限制**:每月只能重置10次 + +--- + +### 十一、数据分析 + +#### 11.1 获取访问趋势 + +```python +# POST https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend?access_token=ACCESS_TOKEN +{ + "begin_date": "20260101", + "end_date": "20260125" +} + +# 返回 +{ + "list": [ + { + "ref_date": "20260125", + "session_cnt": 1000, # 打开次数 + "visit_pv": 5000, # 访问次数 + "visit_uv": 800, # 访问人数 + "visit_uv_new": 100, # 新用户数 + "stay_time_uv": 120.5, # 人均停留时长(秒) + "stay_time_session": 60.2 # 次均停留时长(秒) + } + ] +} +``` + +#### 11.2 获取用户画像 + +```python +# POST https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait?access_token=ACCESS_TOKEN +{ + "begin_date": "20260101", + "end_date": "20260125" +} +``` + +--- + +## 🛠️ 快速使用 + +### 方式一:使用Python管理脚本(推荐) + +```bash +# 进入脚本目录 +cd /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/小程序管理/scripts + +# 安装依赖 +pip install httpx python-dotenv + +# 配置凭证 +cp .env.example .env +# 编辑 .env 填入你的凭证 + +# 使用命令行工具 +python mp_manager.py status # 查看小程序状态 +python mp_manager.py audit # 查看审核状态 +python mp_manager.py release # 发布上线 +python mp_manager.py qrcode # 生成小程序码 +``` + +### 方式二:使用微信开发者工具CLI + +```bash +# CLI路径 +CLI="/Applications/wechatwebdevtools.app/Contents/MacOS/cli" + +# 打开项目 +$CLI -o "/path/to/miniprogram" + +# 编译 +$CLI build-npm --project "/path/to/miniprogram" + +# 预览(生成二维码) +$CLI preview --project "/path/to/miniprogram" --qr-format image --qr-output preview.png + +# 上传代码 +$CLI upload --project "/path/to/miniprogram" --version "1.0.0" --desc "版本说明" + +# 提交审核 +$CLI submit-audit --project "/path/to/miniprogram" +``` + +### 方式三:直接调用API + +```python +import httpx + +# 配置 +ACCESS_TOKEN = "你的access_token" +BASE_URL = "https://api.weixin.qq.com" + +async def check_audit_status(auditid: int): + """查询审核状态""" + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{BASE_URL}/wxa/get_auditstatus?access_token={ACCESS_TOKEN}", + json={"auditid": auditid} + ) + return resp.json() + +# 使用 +result = await check_audit_status(1234567890) +print(result) +``` + +--- + +## 📁 文件结构 + +``` +小程序管理/ +├── SKILL.md # 技能说明文档(本文件) +├── scripts/ +│ ├── mp_manager.py # 小程序管理CLI工具 +│ ├── mp_api.py # API封装类 +│ ├── requirements.txt # Python依赖 +│ └── .env.example # 环境变量模板 +└── references/ + ├── API接口速查表.md # 常用API速查 + ├── 隐私协议填写指南.md # 隐私协议配置指南 + └── 审核规范.md # 审核常见问题 +``` + +--- + +## ⚠️ 常见问题 + +### Q1: 如何获取 component_verify_ticket? + +微信会每10分钟向你配置的"消息与事件接收URL"推送。你需要部署一个服务来接收并存储它。 + +```python +# 接收推送示例 +@app.post("/callback") +async def receive_ticket(request: Request): + xml_data = await request.body() + # 解密并解析XML + # 提取 ComponentVerifyTicket + # 存储到Redis或数据库 + return "success" +``` + +### Q2: 审核被拒常见原因? + +| 原因 | 解决方案 | +|------|----------| +| 类目不符 | 确保小程序功能与所选类目一致 | +| 隐私协议缺失 | 配置完整的隐私保护指引 | +| 诱导分享 | 移除"分享到群获取xx"等诱导文案 | +| 虚拟支付 | iOS不能使用虚拟支付,需走IAP | +| 内容违规 | 检查文案、图片是否合规 | +| 功能不完整 | 确保所有页面功能正常 | + +### Q3: 审核需要多长时间? + +- 首次提审:1-7个工作日 +- 非首次:1-3个工作日 +- 加急审核:付费服务,24小时内 + +### Q4: 如何提高审核通过率? + +1. **提交完整测试账号**:如需登录才能体验功能 +2. **录制操作视频**:复杂功能建议附上操作视频 +3. **详细的版本描述**:说明本次更新内容 +4. **完善隐私协议**:所有收集数据的接口都要说明用途 + +### Q5: 代码模板是什么? + +第三方平台需要先将小程序代码上传到"草稿箱",再添加到"代码模板库"。之后可以用这个模板批量部署到多个小程序。 + +流程:开发完成 → 上传到草稿箱 → 添加到模板库 → 使用模板部署 + +--- + +## 🔗 官方文档 + +- [微信开放平台文档](https://developers.weixin.qq.com/doc/oplatform/) +- [第三方平台开发指南](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/getting_started/how_to_read.html) +- [小程序管理接口](https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/) +- [代码管理接口](https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/code-management/commit.html) +- [隐私协议开发指南](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/) + +--- + +## 📊 当前项目配置 + +### Soul派对小程序 + +| 项目 | 配置值 | +|------|--------| +| **AppID** | `wxb8bbb2b10dec74aa` | +| **项目路径** | `/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram` | +| **API域名** | `https://soul.quwanzhi.com` | +| **当前版本** | `1.0.13` | +| **认证状态** | ⏳ 审核中(等待1-5工作日) | +| **检查结果** | ✅ 8项通过 / ⚠️ 2项警告 / ❌ 0项错误 | +| **可上传** | ✅ 是 | +| **可发布** | ❌ 需等待认证通过 | + +### 快速命令 + +```bash +# 打开项目 +/Applications/wechatwebdevtools.app/Contents/MacOS/cli -o "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram" + +# 预览 +/Applications/wechatwebdevtools.app/Contents/MacOS/cli preview --project "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram" --qr-format image --qr-output preview.png + +# 上传 +/Applications/wechatwebdevtools.app/Contents/MacOS/cli upload --project "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram" --version "1.0.0" --desc "版本说明" +``` + +--- + +## 🚀 全自动部署流程(v3.0) + +### 完整生命周期流程图 + +``` +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 小程序完整生命周期管理 ║ +╠══════════════════════════════════════════════════════════════════════════════╣ +║ ║ +║ 阶段一:准备阶段 ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ │ ║ +║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ║ +║ │ │ 注册小程序│ → │ 企业认证 │ → │ 配置项目 │ │ ║ +║ │ │ (微信后台)│ │ (300元/年)│ │ (添加到管理)│ │ ║ +║ │ └──────────┘ └──────────┘ └──────────┘ │ ║ +║ │ │ │ │ │ ║ +║ │ │ │ ▼ │ ║ +║ │ │ │ mp_deploy.py add │ ║ +║ │ │ ▼ │ ║ +║ │ │ 等待审核 1-5 天 │ ║ +║ │ │ mp_deploy.py cert-done │ ║ +║ │ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ │ ║ +║ ▼ ║ +║ 阶段二:开发阶段 ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ │ ║ +║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ║ +║ │ │ 编写代码 │ → │ 本地测试 │ → │ 项目检查 │ │ ║ +║ │ │ (开发者) │ │ (模拟器) │ │ mp_full │ │ ║ +║ │ └──────────┘ └──────────┘ └──────────┘ │ ║ +║ │ │ │ ║ +║ │ ▼ │ ║ +║ │ mp_full.py check │ ║ +║ │ 检查: 配置/域名/隐私/认证 │ ║ +║ │ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ │ ║ +║ ▼ ║ +║ 阶段三:部署阶段(全自动) ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ │ ║ +║ │ mp_full.py auto -v "1.0.0" -d "版本描述" │ ║ +║ │ │ │ ║ +║ │ ▼ │ ║ +║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ║ +║ │ │ ① 检查 │ → │ ② 上传 │ → │ ③ 提审 │ │ ║ +║ │ │ 项目问题 │ │ 代码到微信│ │ 提交审核 │ │ ║ +║ │ └──────────┘ └──────────┘ └──────────┘ │ ║ +║ │ │ │ │ │ ║ +║ │ ▼ ▼ ▼ │ ║ +║ │ 自动检测 使用CLI/npm 已认证→API提审 │ ║ +║ │ 配置/认证/域名 优先级选择 未认证→手动提审 │ ║ +║ │ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ │ ║ +║ ▼ ║ +║ 阶段四:发布阶段 ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ │ ║ +║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ║ +║ │ │ 等待审核 │ → │ 审核通过 │ → │ 发布上线 │ │ ║ +║ │ │ 1-7工作日 │ │ 通知 │ │ release │ │ ║ +║ │ └──────────┘ └──────────┘ └──────────┘ │ ║ +║ │ │ │ ║ +║ │ ▼ │ ║ +║ │ mp_deploy.py release │ ║ +║ │ 或 微信后台点击发布 │ ║ +║ │ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ │ ║ +║ ▼ ║ +║ 阶段五:运营阶段 ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ │ ║ +║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ║ +║ │ │ 数据分析 │ │ 汇总报告 │ │ 版本迭代 │ │ ║ +║ │ │ mp_manager│ │ mp_full │ │ 循环部署 │ │ ║ +║ │ └──────────┘ └──────────┘ └──────────┘ │ ║ +║ │ │ │ │ │ ║ +║ │ ▼ ▼ ▼ │ ║ +║ │ data命令 report命令 返回部署阶段 │ ║ +║ │ │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +``` + +### 工具整合架构图 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ 小程序管理工具整合架构 │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 用户命令层 │ │ +│ │ mp_full.py report │ mp_full.py check │ mp_full.py auto │ │ +│ │ mp_deploy.py list │ mp_deploy.py cert │ mp_manager.py status │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Python 管理层 │ │ +│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ mp_full.py │ │ mp_deploy.py │ │ mp_api.py │ │ │ +│ │ │ 全能管理器 │ │ 部署工具 │ │ API封装 │ │ │ +│ │ │ • 检查 │ │ • 多小程序 │ │ • 认证 │ │ │ +│ │ │ • 报告 │ │ • 认证管理 │ │ • 审核 │ │ │ +│ │ │ • 自动部署 │ │ • 发布 │ │ • 发布 │ │ │ +│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ 外部工具层 │ │ +│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ 微信开发者 │ │ miniprogram │ │ 微信开放平台 │ │ │ +│ │ │ 工具 CLI │ │ -ci (npm) │ │ API │ │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ │ • 打开项目 │ │ • 上传代码 │ │ • 提交审核 │ │ │ +│ │ │ • 预览 │ │ • 预览 │ │ • 发布 │ │ │ +│ │ │ • 上传 │ │ • 构建npm │ │ • 认证管理 │ │ │ +│ │ │ • 编译 │ │ • sourceMap │ │ • 数据分析 │ │ │ +│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ +│ │ │ │ │ │ │ +│ │ ▼ ▼ ▼ │ │ +│ │ 优先级: 1 优先级: 2 优先级: 3 │ │ +│ │ (无需密钥) (需要密钥) (需要token) │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### 快速开始 + +```bash +# 进入脚本目录 +cd /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/小程序管理/scripts + +# 1. 查看已配置的小程序 +python3 mp_deploy.py list + +# 2. 添加新小程序(交互式) +python3 mp_deploy.py add + +# 3. 检查认证状态 +python3 mp_deploy.py cert-status soul-party + +# 4. 一键部署 +python3 mp_deploy.py deploy soul-party + +# 5. 审核通过后发布 +python3 mp_deploy.py release soul-party +``` + +### 命令速查 + +| 命令 | 说明 | 示例 | +|------|------|------| +| `list` | 列出所有小程序 | `python3 mp_deploy.py list` | +| `add` | 添加新小程序 | `python3 mp_deploy.py add` | +| `deploy` | 一键部署 | `python3 mp_deploy.py deploy soul-party` | +| `upload` | 仅上传代码 | `python3 mp_deploy.py upload soul-party` | +| `cert` | 提交认证 | `python3 mp_deploy.py cert soul-party` | +| `cert-status` | 查询认证状态 | `python3 mp_deploy.py cert-status soul-party` | +| `cert-done` | 标记认证完成 | `python3 mp_deploy.py cert-done soul-party` | +| `release` | 发布上线 | `python3 mp_deploy.py release soul-party` | + +--- + +## 📋 企业认证详解 + +### 认证流程图 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 企业认证流程 │ +├──────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ 准备 │ → │ 提交 │ → │ 审核 │ │ +│ │ 材料 │ │ 认证 │ │ 等待 │ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +│ │ │ │ │ +│ ↓ ↓ ↓ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ • 营业执照 • 微信后台上传 • 1-5工作日 │ │ +│ │ • 法人身份证 • 法人扫码验证 • 审核结果 │ │ +│ │ • 法人微信号 • 支付300元 • 通知 │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ⚠️ 认证有效期:1年,到期需年审 │ +│ │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 认证材料清单 + +| 材料 | 必需 | 说明 | +|------|------|------| +| 企业营业执照 | ✅ | 扫描件或照片,信息清晰 | +| 法人身份证 | ✅ | 正反面照片 | +| 法人微信号 | ✅ | 需绑定银行卡,用于扫码验证 | +| 联系人手机号 | ✅ | 接收审核通知 | +| 认证费用 | ✅ | 300元/年 | +| 其他资质 | 可选 | 特殊行业需要(如医疗、金融) | + +### 认证状态说明 + +| 状态 | 说明 | 下一步操作 | +|------|------|------------| +| `unknown` | 未知/未检查 | 运行 `cert-status` 检查 | +| `pending` | 审核中 | 等待1-5个工作日 | +| `verified` | 已认证 | 可以正常发布 | +| `rejected` | 被拒绝 | 查看原因,修改后重新提交 | +| `expired` | 已过期 | 需要重新认证(年审) | + +### 认证API(第三方平台) + +如果你有第三方平台资质,可以通过API代商家提交认证: + +```python +# POST https://api.weixin.qq.com/wxa/sec/wxaauth?access_token=ACCESS_TOKEN +{ + "auth_type": 1, # 1=企业 + "auth_data": { + "name": "企业名称", + "code": "统一社会信用代码", + "legal_persona_wechat": "法人微信号", + "legal_persona_name": "法人姓名", + "legal_persona_idcard": "法人身份证号", + "component_phone": "联系电话" + } +} +``` + +--- + +## 🔧 GitHub开源工具推荐 + +### 1. miniprogram-ci(官方) + +微信官方提供的CI工具,支持Node.js环境使用。 + +**安装**: +```bash +npm install miniprogram-ci -g +``` + +**使用**: +```javascript +const ci = require('miniprogram-ci'); + +const project = new ci.Project({ + appid: 'wxb8bbb2b10dec74aa', + type: 'miniProgram', + projectPath: '/path/to/miniprogram', + privateKeyPath: '/path/to/private.key', + ignores: ['node_modules/**/*'] +}); + +// 上传 +await ci.upload({ + project, + version: '1.0.0', + desc: '版本描述', + setting: { + es6: true, + minify: true + } +}); +``` + +**获取私钥**: +1. 登录小程序后台 +2. 开发管理 → 开发设置 +3. 小程序代码上传密钥 → 下载 + +### 2. multi-mini-ci + +多平台小程序自动化上传工具。 + +**GitHub**: https://github.com/Ethan-zjc/multi-mini-ci + +**支持平台**: +- 微信小程序 +- 支付宝小程序 +- 百度小程序 +- 字节跳动小程序 + +### 3. GitHub Actions集成 + +在项目中创建 `.github/workflows/deploy.yml`: + +```yaml +name: Deploy MiniProgram + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run build + + - name: Upload to WeChat + env: + PRIVATE_KEY: ${{ secrets.MINIPROGRAM_PRIVATE_KEY }} + run: | + echo "$PRIVATE_KEY" > private.key + npx miniprogram-ci upload \ + --pp ./dist \ + --pkp ./private.key \ + --appid wxb8bbb2b10dec74aa \ + --uv "1.0.${{ github.run_number }}" \ + -r 1 \ + --desc "CI auto deploy" +``` + +--- + +## 📁 完整文件结构(v3.0) + +``` +小程序管理/ +├── SKILL.md # 技能说明文档(本文件) +├── scripts/ +│ ├── mp_full.py # 全能管理工具(推荐)⭐ +│ ├── mp_deploy.py # 部署工具(多小程序管理) +│ ├── mp_manager.py # API管理工具(基础版) +│ ├── mp_api.py # API封装类(Python SDK) +│ ├── apps_config.json # 多小程序配置文件 +│ ├── requirements.txt # Python依赖 +│ ├── env_template.txt # 环境变量模板 +│ └── reports/ # 检查报告存放目录 +│ ├── summary_*.json # 汇总报告 +│ └── report_*.json # 项目报告 +└── references/ + ├── API接口速查表.md # 常用API速查 + ├── 企业认证完整指南.md # 认证操作指南 + ├── 隐私协议填写指南.md # 隐私协议配置指南 + └── 审核规范.md # 审核常见问题 +``` + +### 脚本功能对比 + +| 脚本 | 功能 | 推荐场景 | +|------|------|----------| +| **mp_full.py** | 全能:检查+报告+自动部署 | 日常管理(推荐) | +| **mp_deploy.py** | 多小程序管理+认证 | 多项目管理 | +| **mp_manager.py** | API调用工具 | 高级操作 | +| **mp_api.py** | Python SDK | 二次开发 | + +--- + +## 📊 多小程序配置 + +配置文件位于 `scripts/apps_config.json`: + +```json +{ + "apps": [ + { + "id": "soul-party", + "name": "Soul派对", + "appid": "wxb8bbb2b10dec74aa", + "project_path": "/path/to/miniprogram", + "certification": { + "status": "verified", + "enterprise_name": "厦门智群网络科技有限公司" + } + }, + { + "id": "another-app", + "name": "另一个小程序", + "appid": "wx1234567890", + "project_path": "/path/to/another", + "certification": { + "status": "pending" + } + } + ], + "certification_materials": { + "enterprise_name": "厦门智群网络科技有限公司", + "license_number": "统一社会信用代码", + "legal_persona_name": "法人姓名", + "component_phone": "15880802661" + } +} +``` + +--- + +## ⚡ 常见场景速查 + +### 场景1:发布时提示"未完成微信认证" + +```bash +# 1. 检查认证状态 +python3 mp_deploy.py cert-status soul-party + +# 2. 如果未认证,查看认证指引 +python3 mp_deploy.py cert soul-party + +# 3. 在微信后台完成认证后,标记完成 +python3 mp_deploy.py cert-done soul-party + +# 4. 重新部署 +python3 mp_deploy.py deploy soul-party +``` + +### 场景2:新建小程序并快速上线 + +```bash +# 1. 添加小程序配置 +python3 mp_deploy.py add + +# 2. 提交认证 +python3 mp_deploy.py cert my-new-app + +# 3. 在微信后台完成认证(1-5天) + +# 4. 认证通过后标记 +python3 mp_deploy.py cert-done my-new-app + +# 5. 一键部署 +python3 mp_deploy.py deploy my-new-app + +# 6. 审核通过后发布 +python3 mp_deploy.py release my-new-app +``` + +### 场景3:仅更新代码(不提审) + +```bash +# 快速上传到开发版 +python3 mp_deploy.py upload soul-party -v "1.0.5" -d "修复xxx问题" +``` + +### 场景4:批量管理多个小程序 + +```bash +# 查看所有小程序 +python3 mp_deploy.py list + +# 检查所有认证状态 +for app in soul-party another-app third-app; do + echo "=== $app ===" + python3 mp_deploy.py cert-status $app +done +``` + +--- + +## 🔗 相关资源 + +### 官方文档 +- [微信开放平台](https://developers.weixin.qq.com/doc/oplatform/) +- [小程序CI工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html) +- [第三方平台代认证](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/product/weapp_wxverify.html) + +### 开源项目 +- [miniprogram-ci](https://www.npmjs.com/package/miniprogram-ci) - 官方CI工具 +- [multi-mini-ci](https://github.com/Ethan-zjc/multi-mini-ci) - 多平台上传 +- [uz-miniprogram-ci](https://github.com/uzhan/uz-miniprogram-ci) - 一键上传发布 + +### 相关文章 +- [GitHub Actions集成小程序CI](https://idayer.com/use-github-actions-and-mp-ci-for-wechat-miniprogram-ci/) + +--- + +## 🔥 实战经验库(持续更新) + +> 基于 Soul创业派对 项目开发过程中的真实问题和解决方案 + +### 一、数据库与后端问题 + +#### 1.1 后台初始化失败:Unknown column 'password' in 'field list' + +**问题现象**:后台用户管理显示"初始化失败" + +**根本原因**:数据库表结构缺少字段 + +**解决方案**:创建数据库初始化API自动修复 + +```typescript +// app/api/db/init/route.ts +// 自动检查并添加缺失字段 +const columnsToAdd = [ + { name: 'password', type: 'VARCHAR(100)' }, + { name: 'session_key', type: 'VARCHAR(100)' }, + { name: 'referred_by', type: 'VARCHAR(50)' }, + { name: 'is_admin', type: 'BOOLEAN DEFAULT FALSE' }, +] + +for (const col of columnsToAdd) { + // 检查列是否存在,不存在则添加 + await query(`ALTER TABLE users ADD COLUMN ${col.name} ${col.type}`) +} +``` + +**访问修复**:`curl https://your-domain.com/api/db/init` + +--- + +#### 1.2 Column 'open_id' cannot be null + +**问题现象**:后台添加用户失败 + +**根本原因**:数据库 `open_id` 字段设置为 NOT NULL,但后台添加用户时没有openId + +**解决方案**: +```sql +ALTER TABLE users MODIFY COLUMN open_id VARCHAR(100) NULL +``` + +**最佳实践**:openId允许为NULL,因为: +- 后台手动添加的用户没有openId +- 微信登录用户有openId +- 两种用户需要共存 + +--- + +#### 1.3 AppID配置不一致 + +**问题现象**:微信登录返回错误,或获取openId失败 + +**根本原因**:项目中多个文件使用了不同的AppID + +**检查清单**: + +| 文件 | 配置项 | 正确值 | +|:---|:---|:---| +| `miniprogram/project.config.json` | appid | wxb8bbb2b10dec74aa | +| `app/api/miniprogram/login/route.ts` | MINIPROGRAM_CONFIG.appId | wxb8bbb2b10dec74aa | +| `app/api/wechat/login/route.ts` | APPID | wxb8bbb2b10dec74aa | +| `app/api/withdraw/route.ts` | WECHAT_PAY_CONFIG.appId | wxb8bbb2b10dec74aa | + +**搜索命令**: +```bash +# 查找所有AppID配置 +rg "wx[a-f0-9]{16}" --type ts --type json +``` + +--- + +#### 1.4 用户ID设计最佳实践 + +**推荐方案**:使用 `openId` 作为用户主键 + +```typescript +// 微信登录创建用户 +const userId = openId // 直接使用openId作为用户ID +await query(` + INSERT INTO users (id, open_id, ...) VALUES (?, ?, ...) +`, [userId, openId, ...]) +``` + +**优势**: +- 与微信官方标识一致 +- 便于追踪和管理 +- 后台显示更直观 + +**兼容方案**:后台添加的用户使用 `user_` 前缀 + +--- + +### 二、前端与UI问题 + +#### 2.1 Next.js Hydration错误 + +**问题现象**:页面显示"哎呀,出错了",控制台报 hydration 错误 + +**根本原因**:服务端和客户端渲染结果不一致(如使用localStorage、zustand持久化) + +**解决方案**:添加mounted状态检查 + +```tsx +export default function AdminLayout({ children }) { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + // 等待客户端mount后再渲染 + if (!mounted) { + return
加载中...
+ } + + return
{children}
+} +``` + +--- + +#### 2.2 数据类型不匹配:toFixed() 报错 + +**问题现象**:显示金额时报错 `toFixed is not a function` + +**根本原因**:数据库返回的 `DECIMAL` 字段是字符串类型 + +**解决方案**: +```tsx +// ❌ 错误 +
{user.earnings.toFixed(2)}
+ +// ✅ 正确 +
{parseFloat(String(user.earnings || 0)).toFixed(2)}
+``` + +**通用处理函数**: +```typescript +const formatMoney = (value: any) => { + return parseFloat(String(value || 0)).toFixed(2) +} +``` + +--- + +### 三、小程序开发问题 + +#### 3.1 搜索功能:章节ID格式不一致 + +**问题现象**:搜索结果跳转到阅读页404 + +**根本原因**: +- `book-chapters.json` 使用 `chapter-2`, `chapter-3` 格式 +- 阅读页使用 `1.1`, `1.2` 格式 + +**解决方案**:从标题提取章节号 + +```typescript +// 从标题提取章节号(如 "1.1 荷包:..." → "1.1") +const sectionIdMatch = chapter.title?.match(/^(\d+\.\d+)\s/) +const sectionId = sectionIdMatch ? sectionIdMatch[1] : chapter.id +``` + +--- + +#### 3.2 搜索功能:敏感信息过滤 + +**需求**:搜索结果不能显示用户手机号、微信号等 + +**实现**: +```typescript +const cleanContent = content + .replace(/1[3-9]\d{9}/g, '***') // 手机号 + .replace(/微信[::]\s*\S+/g, '微信:***') // 微信号 + .replace(/QQ[::]\s*\d+/g, 'QQ:***') // QQ号 + .replace(/邮箱[::]\s*\S+@\S+/g, '邮箱:***') // 邮箱 +``` + +--- + +#### 3.3 上下章导航:付费内容也需要显示 + +**需求**:即使用户没有购买,也要显示上一篇/下一篇导航 + +**实现**:将导航组件移到付费墙之外 + +```html + + + + + + + + 上一篇 + 下一篇 + +``` + +--- + +#### 3.4 分销绑定:推广码捕获 + +**需求**:用户通过分享链接进入时,自动绑定推广者 + +**实现流程**: + +```javascript +// app.js - onLaunch/onShow +onLaunch(options) { + if (options.query && options.query.ref) { + wx.setStorageSync('referral_code', options.query.ref) + this.bindReferral(options.query.ref) + } +} + +// 小程序码scene参数解析 +const scene = decodeURIComponent(options.scene) +const params = new URLSearchParams(scene) +const ref = params.get('ref') +``` + +**后端绑定**: +```sql +UPDATE users SET referred_by = ? WHERE id = ? AND referred_by IS NULL +``` + +--- + +### 四、后台管理优化 + +#### 4.1 菜单精简原则 + +**优化前(9项)**: +- 数据概览、网站配置、内容管理、用户管理、匹配配置、分销管理、支付配置、分账提现、二维码、系统设置 + +**优化后(6项)**: +- 数据概览、内容管理、用户管理、分账管理、支付设置、系统设置 + +**精简原则**: +1. 合并相似功能(分销管理 + 分账提现 → 分账管理) +2. 移除低频功能(二维码、匹配配置 → 可在系统设置中配置) +3. 核心功能优先 + +--- + +#### 4.2 用户绑定关系展示 + +**需求**:查看用户的推广下线详情 + +**实现API**: +```typescript +// GET /api/db/users/referrals?userId=xxx +const referrals = await query(` + SELECT * FROM users WHERE referred_by = ? + ORDER BY created_at DESC +`, [referralCode]) +``` + +**展示信息**: +- 绑定总数、已付费人数、免费用户 +- 累计收益、待提现金额 +- 每个绑定用户的状态(VIP/已付费/未付费) + +--- + +### 五、分销与提现 + +#### 5.1 自动分账规则 + +| 配置项 | 值 | 说明 | +|:---|:---|:---| +| 分销比例 | 90% | 推广者获得订单金额的90% | +| 结算方式 | 自动 | 用户付款后立即计入推广者账户 | +| 提现方式 | 微信零钱 | 企业付款到零钱 | +| 提现门槛 | 1元 | 累计收益≥1元可提现 | + +#### 5.2 提现流程 + +``` +用户申请提现 + ↓ +扣除账户余额,增加待提现金额 + ↓ +管理员后台审核 + ↓ +批准 → 调用微信企业付款API → 到账 +拒绝 → 返还用户余额 +``` + +--- + +### 六、开发规范 + +#### 6.1 配置统一管理 + +```typescript +// lib/config.ts +export const WECHAT_CONFIG = { + appId: process.env.WECHAT_APPID || 'wxb8bbb2b10dec74aa', + appSecret: process.env.WECHAT_APPSECRET || '...', + mchId: '1318592501', + apiKey: '...' +} +``` + +**所有API文件统一引用此配置,避免硬编码** + +#### 6.2 数据库字段命名 + +| 前端字段 | 数据库字段 | 说明 | +|:---|:---|:---| +| openId | open_id | 微信openId | +| hasFullBook | has_full_book | 是否购买全书 | +| referralCode | referral_code | 推广码 | +| pendingEarnings | pending_earnings | 待提现收益 | + +**规则**:数据库使用snake_case,前端使用camelCase + +#### 6.3 错误处理模板 + +```typescript +export async function POST(request: Request) { + try { + const body = await request.json() + // 业务逻辑 + return NextResponse.json({ success: true, data: ... }) + } catch (error) { + console.error('[API名称] 错误:', error) + return NextResponse.json({ + success: false, + error: '用户友好的错误信息' + }, { status: 500 }) + } +} +``` + +--- + +## 📌 问题排查清单 + +### 小程序无法登录 + +- [ ] 检查AppID是否正确(project.config.json vs 后端) +- [ ] 检查AppSecret是否正确 +- [ ] 检查API域名是否已配置 +- [ ] 检查后端服务是否正常运行 +- [ ] 查看后端日志 `[MiniLogin]` + +### 后台显示异常 + +- [ ] 运行 `/api/db/init` 初始化数据库 +- [ ] 检查数据库连接是否正常 +- [ ] 清除浏览器缓存(Cmd+Shift+R) +- [ ] 查看浏览器控制台错误 + +### 搜索功能无结果 + +- [ ] 检查 `public/book-chapters.json` 是否存在 +- [ ] 检查章节文件路径是否正确(filePath字段) +- [ ] 检查关键词编码(中文需URL编码) + +### 提现失败 + +- [ ] 检查用户余额是否充足 +- [ ] 检查用户是否有openId +- [ ] 检查微信商户API证书配置 +- [ ] 查看后端日志 `[Withdraw]` + +--- + +**创建时间**:2026-01-25 +**更新时间**:2026-01-25 +**版本**:v3.1(新增实战经验库) +**维护者**:卡若 diff --git a/开发文档/小程序管理/references/API接口速查表.md b/开发文档/小程序管理/references/API接口速查表.md new file mode 100644 index 0000000..2d4e0c2 --- /dev/null +++ b/开发文档/小程序管理/references/API接口速查表.md @@ -0,0 +1,176 @@ +# 微信小程序管理API速查表 + +> 快速查找常用API接口 + +--- + +## 一、认证相关 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/cgi-bin/component/api_component_token` | POST | 获取第三方平台token | +| `/cgi-bin/component/api_create_preauthcode` | POST | 获取预授权码 | +| `/cgi-bin/component/api_query_auth` | POST | 获取授权信息 | +| `/cgi-bin/component/api_authorizer_token` | POST | 刷新授权方token | + +--- + +## 二、基础信息 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/cgi-bin/account/getaccountbasicinfo` | POST | 获取基础信息 | +| `/wxa/setnickname` | POST | 设置名称 | +| `/cgi-bin/account/modifyheadimage` | POST | 修改头像 | +| `/cgi-bin/account/modifysignature` | POST | 修改简介 | + +--- + +## 三、类目管理 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/cgi-bin/wxopen/getallcategories` | GET | 获取可选类目 | +| `/cgi-bin/wxopen/getcategory` | GET | 获取已设置类目 | +| `/cgi-bin/wxopen/addcategory` | POST | 添加类目 | +| `/cgi-bin/wxopen/deletecategory` | POST | 删除类目 | +| `/cgi-bin/wxopen/modifycategory` | POST | 修改类目 | + +--- + +## 四、域名配置 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/wxa/modify_domain` | POST | 设置服务器域名 | +| `/wxa/setwebviewdomain` | POST | 设置业务域名 | + +**action参数**: +- `get` - 获取 +- `set` - 覆盖设置 +- `add` - 添加 +- `delete` - 删除 + +--- + +## 五、隐私协议 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/cgi-bin/component/getprivacysetting` | POST | 获取隐私设置 | +| `/cgi-bin/component/setprivacysetting` | POST | 设置隐私协议 | + +**常用隐私字段**: +- `UserInfo` - 用户信息 +- `Location` - 地理位置 +- `PhoneNumber` - 手机号 +- `Album` - 相册 +- `Camera` - 相机 +- `Record` - 麦克风 +- `Clipboard` - 剪切板 + +--- + +## 六、代码管理 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/wxa/commit` | POST | 上传代码 | +| `/wxa/get_page` | GET | 获取页面列表 | +| `/wxa/get_qrcode` | GET | 获取体验版二维码 | + +--- + +## 七、审核管理 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/wxa/submit_audit` | POST | 提交审核 | +| `/wxa/get_auditstatus` | POST | 查询审核状态 | +| `/wxa/get_latest_auditstatus` | GET | 查询最新审核状态 | +| `/wxa/undocodeaudit` | GET | 撤回审核(每天1次) | +| `/wxa/speedupaudit` | POST | 加急审核 | + +**审核状态码**: +- `0` - 审核成功 +- `1` - 审核被拒 +- `2` - 审核中 +- `3` - 已撤回 +- `4` - 审核延后 + +--- + +## 八、发布管理 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/wxa/release` | POST | 发布上线 | +| `/wxa/revertcoderelease` | GET | 版本回退 | +| `/wxa/grayrelease` | POST | 分阶段发布 | +| `/wxa/getgrayreleaseplan` | GET | 查询灰度计划 | +| `/wxa/revertgrayrelease` | GET | 取消灰度 | + +--- + +## 九、小程序码 + +| 接口 | 方法 | 说明 | 限制 | +|------|------|------|------| +| `/wxa/getwxacode` | POST | 获取小程序码 | 每个path最多10万个 | +| `/wxa/getwxacodeunlimit` | POST | 获取无限小程序码 | 无限制(推荐) | +| `/cgi-bin/wxaapp/createwxaqrcode` | POST | 获取小程序二维码 | 每个path最多10万个 | +| `/wxa/genwxashortlink` | POST | 生成短链接 | - | + +--- + +## 十、数据分析 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/datacube/getweanalysisappiddailyvisittrend` | POST | 日访问趋势 | +| `/datacube/getweanalysisappidweeklyvisittrend` | POST | 周访问趋势 | +| `/datacube/getweanalysisappidmonthlyvisittrend` | POST | 月访问趋势 | +| `/datacube/getweanalysisappiduserportrait` | POST | 用户画像 | +| `/datacube/getweanalysisappidvisitpage` | POST | 访问页面 | + +--- + +## 十一、API配额 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/cgi-bin/openapi/quota/get` | POST | 查询接口配额 | +| `/cgi-bin/clear_quota` | POST | 重置调用次数(每月10次) | +| `/cgi-bin/openapi/rid/get` | POST | 查询rid信息 | + +--- + +## 十二、快速注册 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/cgi-bin/account/fastregister` | POST | 复用公众号资质注册 | +| `/cgi-bin/component/fastregisterminiprogram` | POST | 快速注册企业小程序 | + +--- + +## 常见错误码 + +| 错误码 | 说明 | 解决方案 | +|--------|------|----------| +| 40001 | access_token无效 | 重新获取token | +| 42001 | access_token过期 | 刷新token | +| 45009 | 调用超过限制 | 明天再试或重置 | +| 61039 | 代码检测未完成 | 等待几秒后重试 | +| 85009 | 已有审核版本 | 先撤回再提交 | +| 85086 | 未绑定类目 | 先添加类目 | +| 87013 | 每天只能撤回1次 | 明天再试 | +| 89248 | 隐私协议不完整 | 补充隐私配置 | + +--- + +## 官方文档链接 + +- [第三方平台开发指南](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/getting_started/how_to_read.html) +- [代码管理API](https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/code-management/commit.html) +- [隐私协议开发指南](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/) diff --git a/开发文档/小程序管理/references/企业认证完整指南.md b/开发文档/小程序管理/references/企业认证完整指南.md new file mode 100644 index 0000000..60ea498 --- /dev/null +++ b/开发文档/小程序管理/references/企业认证完整指南.md @@ -0,0 +1,307 @@ +# 小程序企业认证完整指南 + +> 从准备材料到认证完成的完整操作流程 + +--- + +## 一、认证必要性 + +### 未认证 vs 已认证 + +| 功能 | 未认证 | 已认证 | +|------|--------|--------| +| 上传代码 | ✅ 可以 | ✅ 可以 | +| 生成体验版 | ✅ 可以 | ✅ 可以 | +| 提交审核 | ❌ 不可以 | ✅ 可以 | +| 发布上线 | ❌ 不可以 | ✅ 可以 | +| 微信支付 | ❌ 不可以 | ✅ 可以 | +| 获取手机号 | ❌ 不可以 | ✅ 可以 | +| 申请接口权限 | ❌ 受限 | ✅ 全部 | + +**结论**:要让小程序上线,必须完成企业认证。 + +--- + +## 二、认证类型 + +### 1. 企业认证(推荐) + +**适用于**:公司、企业 +**费用**:300元/年 +**审核时间**:1-5个工作日 + +### 2. 个体工商户认证 + +**适用于**:个体工商户 +**费用**:300元/年 +**审核时间**:1-5个工作日 + +### 3. 政府/事业单位认证 + +**适用于**:政府机关、事业单位 +**费用**:免费 +**审核时间**:1-5个工作日 + +### 4. 复用公众号资质(快速) + +**适用于**:已有认证公众号 +**费用**:免费 +**审核时间**:即时生效 + +--- + +## 三、企业认证材料清单 + +### 必需材料 + +| 材料 | 要求 | 说明 | +|------|------|------| +| **企业营业执照** | 彩色扫描件/照片 | 信息清晰完整,未过期 | +| **法人身份证** | 正反面照片 | 与营业执照法人一致 | +| **法人微信号** | 已绑定银行卡 | 用于扫码验证身份 | +| **联系人手机号** | 能接收短信 | 接收审核通知 | +| **认证费用** | 300元 | 支持微信支付 | + +### 特殊行业额外材料 + +| 行业 | 额外材料 | +|------|----------| +| 医疗健康 | 医疗机构执业许可证 | +| 金融服务 | 金融业务许可证 | +| 教育培训 | 办学许可证 | +| 餐饮服务 | 食品经营许可证 | +| 直播 | 网络文化经营许可证 | + +--- + +## 四、认证操作步骤 + +### 步骤1:准备材料 + +``` +☐ 营业执照扫描件(清晰、完整) +☐ 法人身份证正面照片 +☐ 法人身份证反面照片 +☐ 确认法人微信已绑定银行卡 +☐ 准备300元认证费用 +``` + +### 步骤2:登录小程序后台 + +1. 打开 https://mp.weixin.qq.com/ +2. 使用小程序管理员微信扫码登录 + +### 步骤3:进入认证页面 + +1. 点击左侧菜单「设置」 +2. 点击「基本设置」 +3. 找到「微信认证」区域 +4. 点击「去认证」或「详情」 + +### 步骤4:选择认证类型 + +1. 选择「企业」类型 +2. 勾选同意协议 +3. 点击「下一步」 + +### 步骤5:填写企业信息 + +``` +企业名称:厦门智群网络科技有限公司 +统一社会信用代码:91350200... +企业类型:有限责任公司 +经营范围:(按营业执照填写) +注册地址:(按营业执照填写) +``` + +### 步骤6:上传营业执照 + +1. 上传营业执照扫描件 +2. 确保图片清晰、四角完整 +3. 信息与填写内容一致 + +### 步骤7:填写法人信息 + +``` +法人姓名:(与营业执照一致) +法人身份证号: +法人微信号: +``` + +### 步骤8:上传法人身份证 + +1. 上传身份证正面(人像面) +2. 上传身份证反面(国徽面) +3. 确保照片清晰 + +### 步骤9:法人扫码验证 + +1. 页面显示验证二维码 +2. 使用法人微信扫码 +3. 在手机上确认验证 + +### 步骤10:支付认证费用 + +1. 确认费用:300元 +2. 使用微信支付 +3. 支付成功后等待审核 + +### 步骤11:等待审核 + +- 审核时间:1-5个工作日 +- 审核结果会通过模板消息通知 +- 也可在后台查看审核进度 + +--- + +## 五、常见问题 + +### Q1: 法人不方便扫码怎么办? + +**解决方案**: +1. 远程发送验证二维码给法人 +2. 法人用微信扫码即可 +3. 不需要法人在场 + +### Q2: 营业执照即将过期? + +**解决方案**: +1. 先更新营业执照 +2. 再申请认证 +3. 过期的营业执照无法通过审核 + +### Q3: 法人微信未绑定银行卡? + +**解决方案**: +1. 法人先在微信中绑定银行卡 +2. 绑定后再进行扫码验证 +3. 这是实名验证的必要条件 + +### Q4: 审核被拒绝怎么办? + +**常见原因**: +- 营业执照信息与填写不一致 +- 图片模糊或不完整 +- 法人信息与营业执照不匹配 + +**解决方案**: +1. 查看拒绝原因 +2. 修正问题 +3. 重新提交(不需要再付费) + +### Q5: 认证到期怎么办? + +**年审流程**: +1. 提前30天会收到提醒 +2. 登录后台进行年审 +3. 更新材料并支付300元 +4. 1-5个工作日完成 + +--- + +## 六、认证后标记完成 + +认证通过后,运行以下命令更新状态: + +```bash +cd /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/小程序管理/scripts + +# 标记认证完成 +python3 mp_deploy.py cert-done soul-party + +# 确认状态 +python3 mp_deploy.py cert-status soul-party + +# 现在可以部署了 +python3 mp_deploy.py deploy soul-party +``` + +--- + +## 七、认证时间线 + +| 时间节点 | 操作 | +|----------|------| +| Day 0 | 准备材料,提交认证申请 | +| Day 0 | 法人扫码验证,支付费用 | +| Day 1-5 | 等待审核 | +| Day 5 | 审核通过/被拒 | +| 通过后 | 可以正常发布小程序 | +| 1年后 | 需要年审续费 | + +--- + +## 八、复用公众号资质(免费快速认证) + +如果你已有认证的公众号,可以免费快速认证小程序: + +### 条件 +- 公众号已完成微信认证 +- 公众号类型为企业/媒体/政府/其他组织 +- 公众号与小程序主体一致 + +### 操作步骤 +1. 登录公众号后台 +2. 小程序 → 小程序管理 → 添加 +3. 选择「快速注册并认证小程序」 +4. 同意协议 → 管理员扫码 +5. 选择复用资质 → 填写小程序信息 +6. 提交后即时生效 + +### 限制 +- 非个体户:每月可注册5个小程序 +- 个体户:每月可注册1个小程序 + +--- + +## 九、第三方平台代认证(高级) + +如果你有第三方平台资质,可以通过API代商家认证: + +### API接口 + +```python +# POST https://api.weixin.qq.com/wxa/sec/wxaauth +{ + "auth_type": 1, + "auth_data": { + "enterprise_name": "企业名称", + "code": "统一社会信用代码", + "code_type": 1, + "legal_persona_wechat": "法人微信号", + "legal_persona_name": "法人姓名", + "legal_persona_idcard": "法人身份证号", + "component_phone": "联系电话" + } +} +``` + +### 返回说明 + +| errcode | 说明 | +|---------|------| +| 0 | 提交成功 | +| 89247 | 认证信息校验失败 | +| 89248 | 法人信息不匹配 | +| 89249 | 需要法人扫码验证 | + +--- + +## 十、卡若公司认证信息 + +```json +{ + "enterprise_name": "厦门智群网络科技有限公司", + "license_number": "", + "legal_persona_name": "", + "component_phone": "15880802661", + "contact_email": "zhiqun@qq.com" +} +``` + +**注意**:敏感信息(营业执照号、法人姓名、身份证号)请填入配置文件,不要写在文档中。 + +--- + +**创建时间**:2026-01-25 +**适用于**:所有需要企业认证的小程序 diff --git a/开发文档/小程序管理/references/审核规范.md b/开发文档/小程序管理/references/审核规范.md new file mode 100644 index 0000000..09be9de --- /dev/null +++ b/开发文档/小程序管理/references/审核规范.md @@ -0,0 +1,276 @@ +# 小程序审核规范与常见问题 + +> 帮助你提高审核通过率 + +--- + +## 一、审核时间 + +| 类型 | 时间 | +|------|------| +| 首次提审 | 1-7个工作日 | +| 非首次提审 | 1-3个工作日 | +| 加急审核(付费) | 24小时内 | + +**注意**:节假日期间审核时间会延长 + +--- + +## 二、常见被拒原因及解决方案 + +### 1. 类目不符 + +**问题描述**: +小程序功能与所选类目不一致 + +**解决方案**: +- 检查小程序实际功能 +- 在后台选择正确的类目 +- 部分类目需要资质证明 + +**示例**: +- 电子书阅读 → 教育-在线教育 或 图书-电子书 +- 商城 → 商家自营-百货 +- 工具类 → 工具-效率办公 + +### 2. 隐私协议缺失 + +**问题描述**: +使用了隐私接口但未配置隐私保护指引 + +**解决方案**: +1. 在小程序后台配置《用户隐私保护指引》 +2. 在小程序代码中实现隐私授权弹窗 +3. 确保每个隐私接口都有对应说明 + +### 3. 诱导分享/关注 + +**问题描述**: +- "分享到群可获得xx" +- "关注公众号领取xx" +- "转发后才能继续使用" + +**解决方案**: +- 删除所有诱导性文案 +- 分享/关注不能作为获取功能的前提条件 +- 可以使用"邀请好友"等非强制性引导 + +### 4. 虚拟支付(iOS) + +**问题描述**: +iOS上使用了非IAP的虚拟商品支付 + +**解决方案**: +- iOS上虚拟商品必须使用苹果内购(IAP) +- 或者关闭iOS上的虚拟商品购买入口 +- 实物商品可以使用微信支付 + +**常见虚拟商品**: +- 会员/VIP +- 虚拟货币 +- 电子书内容 +- 课程/教程 + +### 5. 功能不完整 + +**问题描述**: +- 页面空白或无内容 +- 按钮无响应 +- 功能入口找不到 + +**解决方案**: +- 确保所有页面都有内容 +- 确保所有按钮都有响应 +- 提供完整的测试账号 +- 录制操作视频说明 + +### 6. 内容违规 + +**问题描述**: +- 涉及敏感词汇 +- 图片不合规 +- 用户生成内容(UGC)无审核机制 + +**解决方案**: +- 自查敏感词 +- 检查图片是否合规 +- UGC内容需要有审核/举报机制 + +### 7. 资质问题 + +**问题描述**: +某些功能需要特定资质 + +**常见需要资质的功能**: +| 功能 | 需要资质 | +|------|----------| +| 直播 | 《增值电信业务许可证》或《网络文化经营许可证》 | +| 金融 | 金融资质 | +| 医疗 | 医疗资质 | +| 地图 | 地图服务相关资质 | +| 发票 | 发票服务资质 | + +### 8. 测试账号问题 + +**问题描述**: +- 未提供测试账号 +- 测试账号无法登录 +- 测试账号权限不足 + +**解决方案**: +- 提供有效的测试账号和密码 +- 确保测试账号可以体验全部功能 +- 在审核备注中说明账号用途 + +--- + +## 三、提高审核通过率的技巧 + +### 1. 提交前自查清单 + +- [ ] 所有页面功能正常 +- [ ] 隐私协议已配置 +- [ ] 类目选择正确 +- [ ] 无诱导分享/关注 +- [ ] iOS虚拟支付问题已处理 +- [ ] 测试账号有效 +- [ ] 内容合规 + +### 2. 填写完整的审核信息 + +```python +# 提交审核时的item_list示例 +item_list = [ + { + "address": "pages/index/index", + "tag": "电子书 阅读 创业", + "first_class": "教育", + "second_class": "在线教育", + "first_id": 1, + "second_id": 2, + "title": "首页-电子书列表" + }, + { + "address": "pages/read/read", + "tag": "阅读 书籍", + "first_class": "教育", + "second_class": "在线教育", + "first_id": 1, + "second_id": 2, + "title": "阅读页-章节内容" + } +] +``` + +### 3. 录制操作视频 + +对于复杂功能,建议录制操作视频: +1. 展示所有核心功能 +2. 演示完整的用户流程 +3. 说明需要测试账号才能体验的功能 + +### 4. 写清楚版本说明 + +``` +版本说明示例: +1. 新增电子书阅读功能 +2. 新增用户登录功能(使用手机号登录) +3. 新增分销功能(邀请好友可获得佣金) + +测试账号: +手机号:13800138000 +验证码:123456 + +注意事项: +1. 分销佣金功能需要登录后才能使用 +2. 部分章节为付费内容,可使用测试账号体验 +``` + +--- + +## 四、审核被拒后的处理 + +### 1. 查看拒绝原因 + +```python +from mp_api import MiniProgramAPI + +api = MiniProgramAPI(access_token="你的token") +status = api.get_latest_audit_status() + +if status.status == 1: # 被拒 + print(f"拒绝原因: {status.reason}") + print(f"问题截图: {status.screenshot}") +``` + +### 2. 根据原因修改 + +- 仔细阅读拒绝原因 +- 查看问题截图 +- 针对性修改 + +### 3. 重新提交 + +修改完成后重新提交审核,建议在版本说明中说明修改内容: + +``` +v1.0.1 更新说明: +1. 修复上次审核指出的问题 +2. 删除了诱导分享的文案 +3. 完善了隐私协议配置 + +修改详情: +- 删除了"分享到群获得积分"的引导 +- 在设置页面增加了隐私协议入口 +``` + +--- + +## 五、特殊情况处理 + +### 1. 加急审核 + +微信提供付费加急审核服务,24小时内完成审核。 + +申请条件: +- 有紧急的业务需求 +- 非首次提审 + +### 2. 申诉 + +如果认为审核结果有误,可以申诉: +1. 登录小程序后台 +2. 版本管理 → 审核版本 +3. 点击"申诉" +4. 填写申诉理由 + +### 3. 撤回审核 + +如果发现问题需要撤回: +- 每天只能撤回1次 +- 撤回后需要重新提交 + +```python +# 撤回审核 +api.undo_code_audit() +``` + +--- + +## 六、审核状态码说明 + +| 状态码 | 说明 | 后续操作 | +|--------|------|----------| +| 0 | 审核成功 | 可以发布上线 | +| 1 | 审核被拒 | 根据原因修改后重新提交 | +| 2 | 审核中 | 等待(1-7个工作日) | +| 3 | 已撤回 | 修改后重新提交 | +| 4 | 审核延后 | 等待进一步通知 | + +--- + +## 七、相关链接 + +- [小程序审核规范](https://developers.weixin.qq.com/miniprogram/product/reject.html) +- [运营规范](https://developers.weixin.qq.com/miniprogram/product/#运营规范) +- [常见拒绝情形](https://developers.weixin.qq.com/miniprogram/product/reject.html) diff --git a/开发文档/小程序管理/references/隐私协议填写指南.md b/开发文档/小程序管理/references/隐私协议填写指南.md new file mode 100644 index 0000000..410481e --- /dev/null +++ b/开发文档/小程序管理/references/隐私协议填写指南.md @@ -0,0 +1,242 @@ +# 小程序隐私协议填写指南 + +> 2024年起,小程序需要配置隐私保护指引才能正常使用涉及用户隐私的接口 + +--- + +## 一、为什么需要配置? + +从2024年开始,微信要求所有小程序: +1. 在调用涉及用户隐私的接口前,需要让用户同意隐私协议 +2. 必须在小程序后台配置《用户隐私保护指引》 +3. 未配置的接口将无法正常调用 + +--- + +## 二、常用隐私字段说明 + +### 用户信息类 + +| 字段 | 涉及接口 | 填写示例 | +|------|----------|----------| +| `UserInfo` | wx.getUserProfile, wx.getUserInfo | 用于展示您的头像和昵称 | +| `Nickname` | wx.chooseNickname | 用于获取您设置的昵称 | + +### 位置信息类 + +| 字段 | 涉及接口 | 填写示例 | +|------|----------|----------| +| `Location` | wx.getLocation, wx.chooseLocation | 用于获取您的位置信息以推荐附近服务 | +| `ChooseLocation` | wx.chooseLocation | 用于在地图上选择位置 | + +### 通讯信息类 + +| 字段 | 涉及接口 | 填写示例 | +|------|----------|----------| +| `PhoneNumber` | button(open-type="getPhoneNumber") | 用于登录验证和订单通知 | +| `Contact` | wx.chooseContact | 用于获取通讯录联系人 | + +### 媒体信息类 + +| 字段 | 涉及接口 | 填写示例 | +|------|----------|----------| +| `Album` | wx.chooseImage, wx.chooseMedia | 用于上传图片或视频 | +| `Camera` | wx.openSetting, camera组件 | 用于拍摄照片或视频 | +| `Record` | wx.startRecord, RecorderManager | 用于录制语音消息 | + +### 其他信息类 + +| 字段 | 涉及接口 | 填写示例 | +|------|----------|----------| +| `Clipboard` | wx.setClipboardData, wx.getClipboardData | 用于复制分享链接或优惠码 | +| `ChooseAddress` | wx.chooseAddress | 用于获取收货地址以便配送 | +| `MessageFile` | wx.openDocument | 用于打开微信消息中的文件 | +| `BluetoothInfo` | wx.openBluetoothAdapter | 用于连接蓝牙设备 | + +--- + +## 三、如何配置 + +### 方式一:微信后台手动配置 + +1. 登录 [小程序后台](https://mp.weixin.qq.com/) +2. 设置 → 基本设置 → 用户隐私保护指引 +3. 选择需要使用的接口 +4. 填写使用说明 +5. 提交审核 + +### 方式二:通过API配置 + +```python +from mp_api import MiniProgramAPI + +api = MiniProgramAPI(access_token="你的token") + +# 配置隐私协议 +api.set_privacy_setting( + setting_list=[ + {"privacy_key": "UserInfo", "privacy_text": "用于展示您的头像和昵称"}, + {"privacy_key": "Location", "privacy_text": "用于获取您的位置信息以推荐附近服务"}, + {"privacy_key": "PhoneNumber", "privacy_text": "用于登录验证和订单通知"}, + {"privacy_key": "Album", "privacy_text": "用于上传图片"}, + {"privacy_key": "Clipboard", "privacy_text": "用于复制分享链接"}, + ], + contact_email="zhiqun@qq.com", + contact_phone="15880802661" +) +``` + +### 方式三:使用CLI工具快速配置 + +```bash +cd /Users/karuo/Documents/个人/卡若AI/02_卡人(水)/小程序管理/scripts +python mp_manager.py privacy --quick --email zhiqun@qq.com --phone 15880802661 +``` + +--- + +## 四、小程序端代码实现 + +### 1. 在app.json中声明 + +```json +{ + "usingComponents": {}, + "__usePrivacyCheck__": true +} +``` + +### 2. 隐私协议弹窗组件 + +```javascript +// privacy-popup.js +Component({ + data: { + showPrivacy: false + }, + + methods: { + // 显示隐私弹窗 + showPrivacyPopup() { + if (wx.getPrivacySetting) { + wx.getPrivacySetting({ + success: (res) => { + if (res.needAuthorization) { + this.setData({ showPrivacy: true }) + } + } + }) + } + }, + + // 同意隐私协议 + handleAgree() { + if (wx.agreePrivacyAuthorization) { + wx.agreePrivacyAuthorization({ + success: () => { + this.setData({ showPrivacy: false }) + } + }) + } + }, + + // 查看隐私协议详情 + openPrivacyContract() { + wx.openPrivacyContract() + } + } +}) +``` + +```html + + + + 用户隐私保护提示 + + 在使用本小程序前,请仔细阅读 + 《用户隐私保护指引》 + + + + + + + +``` + +### 3. 在需要的页面调用 + +```javascript +// pages/index/index.js +Page({ + onLoad() { + // 检查是否需要授权 + this.checkPrivacy() + }, + + checkPrivacy() { + if (wx.getPrivacySetting) { + wx.getPrivacySetting({ + success: (res) => { + if (res.needAuthorization) { + // 需要授权,显示弹窗 + this.selectComponent('#privacy-popup').showPrivacyPopup() + } + } + }) + } + } +}) +``` + +--- + +## 五、常见问题 + +### Q1: 隐私协议需要审核吗? + +配置后需要等待审核(通常1-2小时),审核通过后才会生效。 + +### Q2: 用户拒绝后怎么办? + +用户拒绝后,相关接口将无法使用。可以引导用户重新打开小程序,会再次弹出隐私协议。 + +### Q3: 如何测试隐私协议? + +在微信开发者工具中: +1. 详情 → 本地设置 +2. 勾选"启用隐私相关接口" +3. 清除授权记录后测试 + +### Q4: 提交审核时提示隐私协议不完整? + +检查以下几点: +1. 是否填写了所有使用到的隐私接口 +2. 每个接口的说明是否清晰 +3. 是否填写了联系方式(邮箱或电话至少一个) + +--- + +## 六、Soul派对小程序配置参考 + +```python +# Soul派对小程序的隐私配置 +setting_list = [ + {"privacy_key": "UserInfo", "privacy_text": "用于展示您在Soul派对中的头像和昵称"}, + {"privacy_key": "PhoneNumber", "privacy_text": "用于登录验证和购买通知"}, + {"privacy_key": "Clipboard", "privacy_text": "用于复制分享链接和邀请码"}, +] + +# 联系方式 +contact_email = "zhiqun@qq.com" +contact_phone = "15880802661" +``` + +--- + +## 七、参考文档 + +- [小程序隐私协议开发指南](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/) +- [用户隐私保护指引填写说明](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/) +- [隐私接口列表](https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html) diff --git a/开发文档/小程序管理/scripts/__pycache__/mp_api.cpython-314.pyc b/开发文档/小程序管理/scripts/__pycache__/mp_api.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17805a5ac97b4438f372be8aae2e95f83d86cb1a GIT binary patch literal 30696 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 LOw;c&41oU+sqm2s literal 0 HcmV?d00001 diff --git a/开发文档/小程序管理/scripts/apps_config.json b/开发文档/小程序管理/scripts/apps_config.json new file mode 100644 index 0000000..eeb0a7d --- /dev/null +++ b/开发文档/小程序管理/scripts/apps_config.json @@ -0,0 +1,40 @@ +{ + "_comment": "小程序配置文件 - 支持管理多个小程序", + "apps": [ + { + "id": "soul-party", + "name": "Soul派对", + "appid": "wxb8bbb2b10dec74aa", + "project_path": "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram", + "private_key_path": "", + "api_domain": "https://soul.quwanzhi.com", + "description": "一场SOUL的创业实验场", + "certification": { + "status": "pending", + "enterprise_name": "泉州市卡若网络技术有限公司", + "license_number": "", + "legal_persona_name": "", + "legal_persona_wechat": "", + "component_phone": "15880802661" + } + } + ], + "certification_materials": { + "_comment": "企业认证通用材料(所有小程序共用)", + "enterprise_name": "泉州市卡若网络技术有限公司", + "license_number": "", + "license_media_id": "", + "legal_persona_name": "", + "legal_persona_wechat": "", + "legal_persona_idcard": "", + "component_phone": "15880802661", + "contact_email": "zhiqun@qq.com" + }, + "third_party_platform": { + "_comment": "第三方平台配置(用于代认证)", + "component_appid": "", + "component_appsecret": "", + "component_verify_ticket": "", + "authorized": false + } +} diff --git a/开发文档/小程序管理/scripts/env_template.txt b/开发文档/小程序管理/scripts/env_template.txt new file mode 100644 index 0000000..9f7686d --- /dev/null +++ b/开发文档/小程序管理/scripts/env_template.txt @@ -0,0 +1,30 @@ +# 微信小程序管理 - 环境变量配置 +# 复制此文件为 .env 并填入实际值 + +# ==================== 方式一:直接使用 access_token ==================== +# 如果你已经有 access_token,直接填入即可(适合快速测试) +ACCESS_TOKEN=你的access_token + +# ==================== 方式二:使用第三方平台凭证 ==================== +# 如果你有第三方平台资质,填入以下信息可自动刷新token + +# 第三方平台 AppID +COMPONENT_APPID=你的第三方平台AppID + +# 第三方平台密钥 +COMPONENT_APPSECRET=你的第三方平台密钥 + +# 授权小程序 AppID(要管理的小程序) +AUTHORIZER_APPID=wxb8bbb2b10dec74aa + +# 授权刷新令牌(从授权回调中获取) +AUTHORIZER_REFRESH_TOKEN=授权时获取的refresh_token + +# ==================== 小程序项目路径 ==================== +# 用于 CLI 工具操作 +MINIPROGRAM_PATH=/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram + +# ==================== 可选配置 ==================== +# 隐私协议联系方式(用于快速配置) +CONTACT_EMAIL=zhiqun@qq.com +CONTACT_PHONE=15880802661 diff --git a/开发文档/小程序管理/scripts/mp_api.py b/开发文档/小程序管理/scripts/mp_api.py new file mode 100644 index 0000000..61fae3a --- /dev/null +++ b/开发文档/小程序管理/scripts/mp_api.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +微信小程序管理API封装 +支持:注册、配置、代码管理、审核、发布、数据分析 +""" + +import os +import json +import time +import httpx +from typing import Optional, Dict, Any, List +from dataclasses import dataclass +from pathlib import Path + +# 尝试加载dotenv(可选依赖) +try: + from dotenv import load_dotenv + load_dotenv() +except ImportError: + pass # dotenv不是必需的 + + +@dataclass +class MiniProgramInfo: + """小程序基础信息""" + appid: str + nickname: str + head_image_url: str + signature: str + principal_name: str + realname_status: int # 1=已认证 + + +@dataclass +class AuditStatus: + """审核状态""" + auditid: int + status: int # 0=成功,1=被拒,2=审核中,3=已撤回,4=延后 + reason: Optional[str] = None + screenshot: Optional[str] = None + + @property + def status_text(self) -> str: + status_map = { + 0: "✅ 审核成功", + 1: "❌ 审核被拒", + 2: "⏳ 审核中", + 3: "↩️ 已撤回", + 4: "⏸️ 审核延后" + } + return status_map.get(self.status, "未知状态") + + +class MiniProgramAPI: + """微信小程序管理API""" + + BASE_URL = "https://api.weixin.qq.com" + + def __init__( + self, + component_appid: Optional[str] = None, + component_appsecret: Optional[str] = None, + authorizer_appid: Optional[str] = None, + access_token: Optional[str] = None + ): + """ + 初始化API + + Args: + component_appid: 第三方平台AppID + component_appsecret: 第三方平台密钥 + authorizer_appid: 授权小程序AppID + access_token: 直接使用的access_token(如已获取) + """ + self.component_appid = component_appid or os.getenv("COMPONENT_APPID") + self.component_appsecret = component_appsecret or os.getenv("COMPONENT_APPSECRET") + self.authorizer_appid = authorizer_appid or os.getenv("AUTHORIZER_APPID") + self._access_token = access_token or os.getenv("ACCESS_TOKEN") + self._token_expires_at = 0 + + self.client = httpx.Client(timeout=30.0) + + @property + def access_token(self) -> str: + """获取access_token,如果过期则刷新""" + if self._access_token and time.time() < self._token_expires_at: + return self._access_token + + # 如果没有配置刷新token的信息,直接返回现有token + if not self.component_appid: + return self._access_token or "" + + # TODO: 实现token刷新逻辑 + return self._access_token or "" + + def set_access_token(self, token: str, expires_in: int = 7200): + """手动设置access_token""" + self._access_token = token + self._token_expires_at = time.time() + expires_in - 300 # 提前5分钟过期 + + def _request( + self, + method: str, + path: str, + params: Optional[Dict] = None, + json_data: Optional[Dict] = None, + **kwargs + ) -> Dict[str, Any]: + """发起API请求""" + url = f"{self.BASE_URL}{path}" + + # 添加access_token + if params is None: + params = {} + if "access_token" not in params: + params["access_token"] = self.access_token + + if method.upper() == "GET": + resp = self.client.get(url, params=params, **kwargs) + else: + resp = self.client.post(url, params=params, json=json_data, **kwargs) + + # 解析响应 + try: + result = resp.json() + except json.JSONDecodeError: + # 可能是二进制数据(如图片) + return {"_binary": resp.content} + + # 检查错误 + if result.get("errcode", 0) != 0: + raise APIError(result.get("errcode"), result.get("errmsg", "Unknown error")) + + return result + + # ==================== 基础信息 ==================== + + def get_basic_info(self) -> MiniProgramInfo: + """获取小程序基础信息""" + result = self._request("POST", "/cgi-bin/account/getaccountbasicinfo") + return MiniProgramInfo( + appid=result.get("appid", ""), + nickname=result.get("nickname", ""), + head_image_url=result.get("head_image_url", ""), + signature=result.get("signature", ""), + principal_name=result.get("principal_name", ""), + realname_status=result.get("realname_status", 0) + ) + + def modify_signature(self, signature: str) -> bool: + """修改简介(4-120字)""" + self._request("POST", "/cgi-bin/account/modifysignature", json_data={ + "signature": signature + }) + return True + + # ==================== 域名配置 ==================== + + def get_domain(self) -> Dict[str, List[str]]: + """获取服务器域名配置""" + result = self._request("POST", "/wxa/modify_domain", json_data={ + "action": "get" + }) + return { + "requestdomain": result.get("requestdomain", []), + "wsrequestdomain": result.get("wsrequestdomain", []), + "uploaddomain": result.get("uploaddomain", []), + "downloaddomain": result.get("downloaddomain", []) + } + + def set_domain( + self, + requestdomain: Optional[List[str]] = None, + wsrequestdomain: Optional[List[str]] = None, + uploaddomain: Optional[List[str]] = None, + downloaddomain: Optional[List[str]] = None + ) -> bool: + """设置服务器域名""" + data = {"action": "set"} + if requestdomain: + data["requestdomain"] = requestdomain + if wsrequestdomain: + data["wsrequestdomain"] = wsrequestdomain + if uploaddomain: + data["uploaddomain"] = uploaddomain + if downloaddomain: + data["downloaddomain"] = downloaddomain + + self._request("POST", "/wxa/modify_domain", json_data=data) + return True + + def get_webview_domain(self) -> List[str]: + """获取业务域名""" + result = self._request("POST", "/wxa/setwebviewdomain", json_data={ + "action": "get" + }) + return result.get("webviewdomain", []) + + def set_webview_domain(self, webviewdomain: List[str]) -> bool: + """设置业务域名""" + self._request("POST", "/wxa/setwebviewdomain", json_data={ + "action": "set", + "webviewdomain": webviewdomain + }) + return True + + # ==================== 隐私协议 ==================== + + def get_privacy_setting(self, privacy_ver: int = 2) -> Dict[str, Any]: + """获取隐私协议设置""" + result = self._request("POST", "/cgi-bin/component/getprivacysetting", json_data={ + "privacy_ver": privacy_ver + }) + return result + + def set_privacy_setting( + self, + setting_list: List[Dict[str, str]], + contact_email: Optional[str] = None, + contact_phone: Optional[str] = None, + notice_method: str = "弹窗提示" + ) -> bool: + """ + 设置隐私协议 + + Args: + setting_list: 隐私配置列表,如 [{"privacy_key": "UserInfo", "privacy_text": "用于展示头像"}] + contact_email: 联系邮箱 + contact_phone: 联系电话 + notice_method: 告知方式 + """ + data = { + "privacy_ver": 2, + "setting_list": setting_list + } + + owner_setting = {"notice_method": notice_method} + if contact_email: + owner_setting["contact_email"] = contact_email + if contact_phone: + owner_setting["contact_phone"] = contact_phone + data["owner_setting"] = owner_setting + + self._request("POST", "/cgi-bin/component/setprivacysetting", json_data=data) + return True + + # ==================== 类目管理 ==================== + + def get_all_categories(self) -> List[Dict]: + """获取可选类目列表""" + result = self._request("GET", "/cgi-bin/wxopen/getallcategories") + return result.get("categories_list", {}).get("categories", []) + + def get_category(self) -> List[Dict]: + """获取已设置的类目""" + result = self._request("GET", "/cgi-bin/wxopen/getcategory") + return result.get("categories", []) + + def add_category(self, categories: List[Dict]) -> bool: + """ + 添加类目 + + Args: + categories: 类目列表,如 [{"first": 1, "second": 2}] + """ + self._request("POST", "/cgi-bin/wxopen/addcategory", json_data={ + "categories": categories + }) + return True + + def delete_category(self, first: int, second: int) -> bool: + """删除类目""" + self._request("POST", "/cgi-bin/wxopen/deletecategory", json_data={ + "first": first, + "second": second + }) + return True + + # ==================== 代码管理 ==================== + + def commit_code( + self, + template_id: int, + user_version: str, + user_desc: str, + ext_json: Optional[str] = None + ) -> bool: + """ + 上传代码 + + Args: + template_id: 代码模板ID + user_version: 版本号 + user_desc: 版本描述 + ext_json: 扩展配置JSON字符串 + """ + data = { + "template_id": template_id, + "user_version": user_version, + "user_desc": user_desc + } + if ext_json: + data["ext_json"] = ext_json + + self._request("POST", "/wxa/commit", json_data=data) + return True + + def get_page(self) -> List[str]: + """获取已上传代码的页面列表""" + result = self._request("GET", "/wxa/get_page") + return result.get("page_list", []) + + def get_qrcode(self, path: Optional[str] = None) -> bytes: + """ + 获取体验版二维码 + + Args: + path: 页面路径,如 "pages/index/index" + + Returns: + 二维码图片二进制数据 + """ + params = {"access_token": self.access_token} + if path: + params["path"] = path + + resp = self.client.get(f"{self.BASE_URL}/wxa/get_qrcode", params=params) + return resp.content + + # ==================== 审核管理 ==================== + + def submit_audit( + self, + item_list: Optional[List[Dict]] = None, + version_desc: Optional[str] = None, + feedback_info: Optional[str] = None + ) -> int: + """ + 提交审核 + + Args: + item_list: 页面审核信息列表 + version_desc: 版本说明 + feedback_info: 反馈内容 + + Returns: + 审核单ID + """ + data = {} + if item_list: + data["item_list"] = item_list + if version_desc: + data["version_desc"] = version_desc + if feedback_info: + data["feedback_info"] = feedback_info + + result = self._request("POST", "/wxa/submit_audit", json_data=data) + return result.get("auditid", 0) + + def get_audit_status(self, auditid: int) -> AuditStatus: + """查询审核状态""" + result = self._request("POST", "/wxa/get_auditstatus", json_data={ + "auditid": auditid + }) + return AuditStatus( + auditid=auditid, + status=result.get("status", -1), + reason=result.get("reason"), + screenshot=result.get("screenshot") + ) + + def get_latest_audit_status(self) -> AuditStatus: + """查询最新审核状态""" + result = self._request("GET", "/wxa/get_latest_auditstatus") + return AuditStatus( + auditid=result.get("auditid", 0), + status=result.get("status", -1), + reason=result.get("reason"), + screenshot=result.get("screenshot") + ) + + def undo_code_audit(self) -> bool: + """撤回审核(每天限1次)""" + self._request("GET", "/wxa/undocodeaudit") + return True + + # ==================== 发布管理 ==================== + + def release(self) -> bool: + """发布已审核通过的版本""" + self._request("POST", "/wxa/release", json_data={}) + return True + + def revert_code_release(self) -> bool: + """版本回退(只能回退到上一版本)""" + self._request("GET", "/wxa/revertcoderelease") + return True + + def get_revert_history(self) -> List[Dict]: + """获取可回退版本历史""" + result = self._request("GET", "/wxa/revertcoderelease", params={ + "action": "get_history_version" + }) + return result.get("version_list", []) + + def gray_release(self, gray_percentage: int) -> bool: + """ + 分阶段发布 + + Args: + gray_percentage: 灰度比例 1-100 + """ + self._request("POST", "/wxa/grayrelease", json_data={ + "gray_percentage": gray_percentage + }) + return True + + # ==================== 小程序码 ==================== + + def get_wxacode( + self, + path: str, + width: int = 430, + auto_color: bool = False, + line_color: Optional[Dict[str, int]] = None, + is_hyaline: bool = False + ) -> bytes: + """ + 获取小程序码(有限制,每个path最多10万个) + + Args: + path: 页面路径,如 "pages/index/index?id=123" + width: 宽度 280-1280 + auto_color: 自动配置线条颜色 + line_color: 线条颜色 {"r": 0, "g": 0, "b": 0} + is_hyaline: 是否透明背景 + + Returns: + 二维码图片二进制数据 + """ + data = { + "path": path, + "width": width, + "auto_color": auto_color, + "is_hyaline": is_hyaline + } + if line_color: + data["line_color"] = line_color + + resp = self.client.post( + f"{self.BASE_URL}/wxa/getwxacode", + params={"access_token": self.access_token}, + json=data + ) + return resp.content + + def get_wxacode_unlimit( + self, + scene: str, + page: Optional[str] = None, + width: int = 430, + auto_color: bool = False, + line_color: Optional[Dict[str, int]] = None, + is_hyaline: bool = False + ) -> bytes: + """ + 获取无限小程序码(推荐) + + Args: + scene: 场景值,最长32字符,如 "user_id=123&from=share" + page: 页面路径,必须是已发布的页面 + width: 宽度 280-1280 + auto_color: 自动配置线条颜色 + line_color: 线条颜色 {"r": 0, "g": 0, "b": 0} + is_hyaline: 是否透明背景 + + Returns: + 二维码图片二进制数据 + """ + data = { + "scene": scene, + "width": width, + "auto_color": auto_color, + "is_hyaline": is_hyaline + } + if page: + data["page"] = page + if line_color: + data["line_color"] = line_color + + resp = self.client.post( + f"{self.BASE_URL}/wxa/getwxacodeunlimit", + params={"access_token": self.access_token}, + json=data + ) + return resp.content + + def gen_short_link( + self, + page_url: str, + page_title: str, + is_permanent: bool = False + ) -> str: + """ + 生成小程序短链接 + + Args: + page_url: 页面路径,如 "pages/index/index?id=123" + page_title: 页面标题 + is_permanent: 是否永久有效 + + Returns: + 短链接 + """ + result = self._request("POST", "/wxa/genwxashortlink", json_data={ + "page_url": page_url, + "page_title": page_title, + "is_permanent": is_permanent + }) + return result.get("link", "") + + # ==================== 数据分析 ==================== + + def get_daily_visit_trend(self, begin_date: str, end_date: str) -> List[Dict]: + """ + 获取每日访问趋势 + + Args: + begin_date: 开始日期 YYYYMMDD + end_date: 结束日期 YYYYMMDD + """ + result = self._request( + "POST", + "/datacube/getweanalysisappiddailyvisittrend", + json_data={"begin_date": begin_date, "end_date": end_date} + ) + return result.get("list", []) + + def get_user_portrait(self, begin_date: str, end_date: str) -> Dict: + """ + 获取用户画像 + + Args: + begin_date: 开始日期 YYYYMMDD + end_date: 结束日期 YYYYMMDD + """ + result = self._request( + "POST", + "/datacube/getweanalysisappiduserportrait", + json_data={"begin_date": begin_date, "end_date": end_date} + ) + return result + + # ==================== API配额 ==================== + + def get_api_quota(self, cgi_path: str) -> Dict: + """ + 查询接口调用额度 + + Args: + cgi_path: 接口路径,如 "/wxa/getwxacode" + """ + result = self._request("POST", "/cgi-bin/openapi/quota/get", json_data={ + "cgi_path": cgi_path + }) + return result.get("quota", {}) + + def clear_quota(self, appid: Optional[str] = None) -> bool: + """重置接口调用次数(每月限10次)""" + self._request("POST", "/cgi-bin/clear_quota", json_data={ + "appid": appid or self.authorizer_appid + }) + return True + + def close(self): + """关闭连接""" + self.client.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +class APIError(Exception): + """API错误""" + + ERROR_CODES = { + -1: "系统繁忙", + 40001: "access_token无效", + 40002: "grant_type不正确", + 40013: "appid不正确", + 40029: "code无效", + 40125: "appsecret不正确", + 41002: "缺少appid参数", + 41004: "缺少appsecret参数", + 42001: "access_token过期", + 42007: "refresh_token过期", + 45009: "调用超过限制", + 61039: "代码检测任务未完成,请稍后再试", + 85006: "标签格式错误", + 85007: "页面路径错误", + 85009: "已有审核版本,请先撤回", + 85010: "版本输入错误", + 85011: "当前版本不能回退", + 85012: "无效的版本", + 85015: "该账号已有发布中的版本", + 85019: "没有审核版本", + 85020: "审核状态异常", + 85064: "找不到模板", + 85085: "该小程序不能被操作", + 85086: "小程序没有绑定任何类目", + 87013: "每天只能撤回1次审核", + 89020: "该小程序尚未认证", + 89248: "隐私协议内容不完整", + } + + def __init__(self, code: int, message: str): + self.code = code + self.message = message + super().__init__(f"[{code}] {self.ERROR_CODES.get(code, message)}") + + +# 便捷函数 +def create_api_from_env() -> MiniProgramAPI: + """从环境变量创建API实例""" + return MiniProgramAPI() + + +if __name__ == "__main__": + # 测试 + api = create_api_from_env() + print("API初始化成功") diff --git a/开发文档/小程序管理/scripts/mp_deploy.py b/开发文档/小程序管理/scripts/mp_deploy.py new file mode 100644 index 0000000..22f859f --- /dev/null +++ b/开发文档/小程序管理/scripts/mp_deploy.py @@ -0,0 +1,725 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +小程序一键部署工具 v2.0 + +功能: +- 多小程序管理 +- 一键部署上线 +- 自动认证提交 +- 认证状态检查 +- 材料有效性验证 + +使用方法: + python mp_deploy.py list # 列出所有小程序 + python mp_deploy.py add # 添加新小程序 + python mp_deploy.py deploy # 一键部署 + python mp_deploy.py cert # 提交认证 + python mp_deploy.py cert-status # 查询认证状态 + python mp_deploy.py upload # 上传代码 + python mp_deploy.py release # 发布上线 +""" + +import os +import sys +import json +import subprocess +import argparse +from datetime import datetime +from pathlib import Path +from typing import Optional, Dict, List, Any +from dataclasses import dataclass, asdict + +# 配置文件路径 +CONFIG_FILE = Path(__file__).parent / "apps_config.json" + + +@dataclass +class AppConfig: + """小程序配置""" + id: str + name: str + appid: str + project_path: str + private_key_path: str = "" + api_domain: str = "" + description: str = "" + certification: Dict = None + + def __post_init__(self): + if self.certification is None: + self.certification = { + "status": "unknown", + "enterprise_name": "", + "license_number": "", + "legal_persona_name": "", + "legal_persona_wechat": "", + "component_phone": "" + } + + +class ConfigManager: + """配置管理器""" + + def __init__(self, config_file: Path = CONFIG_FILE): + self.config_file = config_file + self.config = self._load_config() + + def _load_config(self) -> Dict: + """加载配置""" + if self.config_file.exists(): + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + return {"apps": [], "certification_materials": {}, "third_party_platform": {}} + + def _save_config(self): + """保存配置""" + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(self.config, f, ensure_ascii=False, indent=2) + + def get_apps(self) -> List[AppConfig]: + """获取所有小程序""" + return [AppConfig(**app) for app in self.config.get("apps", [])] + + def get_app(self, app_id: str) -> Optional[AppConfig]: + """获取指定小程序""" + for app in self.config.get("apps", []): + if app["id"] == app_id or app["appid"] == app_id: + return AppConfig(**app) + return None + + def add_app(self, app: AppConfig): + """添加小程序""" + apps = self.config.get("apps", []) + # 检查是否已存在 + for i, existing in enumerate(apps): + if existing["id"] == app.id: + apps[i] = asdict(app) + self.config["apps"] = apps + self._save_config() + return + apps.append(asdict(app)) + self.config["apps"] = apps + self._save_config() + + def update_app(self, app_id: str, updates: Dict): + """更新小程序配置""" + apps = self.config.get("apps", []) + for i, app in enumerate(apps): + if app["id"] == app_id: + apps[i].update(updates) + self.config["apps"] = apps + self._save_config() + return True + return False + + def get_cert_materials(self) -> Dict: + """获取通用认证材料""" + return self.config.get("certification_materials", {}) + + def update_cert_materials(self, materials: Dict): + """更新认证材料""" + self.config["certification_materials"] = materials + self._save_config() + + +class MiniProgramDeployer: + """小程序部署器""" + + # 微信开发者工具CLI路径 + WX_CLI = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli" + + def __init__(self): + self.config = ConfigManager() + + def _check_wx_cli(self) -> bool: + """检查微信开发者工具是否安装""" + return os.path.exists(self.WX_CLI) + + def _run_cli(self, *args, project_path: str = None) -> tuple: + """运行CLI命令""" + cmd = [self.WX_CLI] + list(args) + if project_path: + cmd.extend(["--project", project_path]) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + return result.returncode == 0, result.stdout + result.stderr + except subprocess.TimeoutExpired: + return False, "命令执行超时" + except Exception as e: + return False, str(e) + + def list_apps(self): + """列出所有小程序""" + apps = self.config.get_apps() + + if not apps: + print("\n📭 暂无配置的小程序") + print(" 运行 'python mp_deploy.py add' 添加小程序") + return + + print("\n" + "=" * 60) + print(" 📱 小程序列表") + print("=" * 60) + + for i, app in enumerate(apps, 1): + cert_status = app.certification.get("status", "unknown") + status_icon = { + "verified": "✅", + "pending": "⏳", + "rejected": "❌", + "expired": "⚠️", + "unknown": "❓" + }.get(cert_status, "❓") + + print(f"\n [{i}] {app.name}") + print(f" ID: {app.id}") + print(f" AppID: {app.appid}") + print(f" 认证: {status_icon} {cert_status}") + print(f" 路径: {app.project_path}") + + print("\n" + "-" * 60) + print(" 使用方法:") + print(" python mp_deploy.py deploy 一键部署") + print(" python mp_deploy.py cert 提交认证") + print("=" * 60 + "\n") + + def add_app(self): + """交互式添加小程序""" + print("\n" + "=" * 50) + print(" ➕ 添加新小程序") + print("=" * 50 + "\n") + + # 收集信息 + app_id = input("小程序ID(用于标识,如 my-app): ").strip() + if not app_id: + print("❌ ID不能为空") + return + + name = input("小程序名称: ").strip() + appid = input("AppID(如 wx1234567890): ").strip() + project_path = input("项目路径: ").strip() + + if not os.path.exists(project_path): + print(f"⚠️ 警告:路径不存在 {project_path}") + + api_domain = input("API域名(可选): ").strip() + description = input("描述(可选): ").strip() + + # 认证信息 + print("\n📋 认证信息(可稍后配置):") + enterprise_name = input("企业名称: ").strip() + + app = AppConfig( + id=app_id, + name=name, + appid=appid, + project_path=project_path, + api_domain=api_domain, + description=description, + certification={ + "status": "unknown", + "enterprise_name": enterprise_name, + "license_number": "", + "legal_persona_name": "", + "legal_persona_wechat": "", + "component_phone": "15880802661" + } + ) + + self.config.add_app(app) + print(f"\n✅ 小程序 [{name}] 添加成功!") + + def deploy(self, app_id: str, skip_cert_check: bool = False): + """一键部署流程""" + app = self.config.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + print(" 运行 'python mp_deploy.py list' 查看所有小程序") + return False + + print("\n" + "=" * 60) + print(f" 🚀 一键部署: {app.name}") + print("=" * 60) + + steps = [ + ("检查环境", self._step_check_env), + ("检查认证状态", lambda a: self._step_check_cert(a, skip_cert_check)), + ("编译项目", self._step_build), + ("上传代码", self._step_upload), + ("提交审核", self._step_submit_audit), + ] + + for step_name, step_func in steps: + print(f"\n📍 步骤: {step_name}") + print("-" * 40) + + success = step_func(app) + if not success: + print(f"\n❌ 部署中断于: {step_name}") + return False + + print("\n" + "=" * 60) + print(" 🎉 部署完成!") + print("=" * 60) + print(f"\n 下一步操作:") + print(f" 1. 等待审核(通常1-3个工作日)") + print(f" 2. 审核通过后运行: python mp_deploy.py release {app_id}") + print(f" 3. 查看状态: python mp_deploy.py status {app_id}") + + return True + + def _step_check_env(self, app: AppConfig) -> bool: + """检查环境""" + # 检查微信开发者工具 + if not self._check_wx_cli(): + print("❌ 未找到微信开发者工具") + print(" 请安装: https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html") + return False + print("✅ 微信开发者工具已安装") + + # 检查项目路径 + if not os.path.exists(app.project_path): + print(f"❌ 项目路径不存在: {app.project_path}") + return False + print(f"✅ 项目路径存在") + + # 检查project.config.json + config_file = os.path.join(app.project_path, "project.config.json") + if os.path.exists(config_file): + with open(config_file, 'r') as f: + config = json.load(f) + if config.get("appid") != app.appid: + print(f"⚠️ 警告: project.config.json中的AppID与配置不一致") + print(f" 配置: {app.appid}") + print(f" 文件: {config.get('appid')}") + + print("✅ 环境检查通过") + return True + + def _step_check_cert(self, app: AppConfig, skip: bool = False) -> bool: + """检查认证状态""" + if skip: + print("⏭️ 跳过认证检查") + return True + + cert_status = app.certification.get("status", "unknown") + + if cert_status == "verified": + print("✅ 已完成微信认证") + return True + + if cert_status == "pending": + print("⏳ 认证审核中") + print(" 可选择:") + print(" 1. 继续部署(未认证可上传,但无法发布)") + print(" 2. 等待认证完成") + + choice = input("\n是否继续? (y/n): ").strip().lower() + return choice == 'y' + + if cert_status == "expired": + print("⚠️ 认证已过期,需要重新认证") + print(" 运行: python mp_deploy.py cert " + app.id) + + choice = input("\n是否继续部署? (y/n): ").strip().lower() + return choice == 'y' + + # 未认证或未知状态 + print("⚠️ 未完成微信认证") + print(" 未认证的小程序可以上传代码,但无法发布上线") + print(" 运行: python mp_deploy.py cert " + app.id + " 提交认证") + + choice = input("\n是否继续? (y/n): ").strip().lower() + return choice == 'y' + + def _step_build(self, app: AppConfig) -> bool: + """编译项目""" + print("📦 编译项目...") + + # 使用CLI编译 + success, output = self._run_cli("build-npm", project_path=app.project_path) + + if not success: + # build-npm可能失败(如果没有npm依赖),不算错误 + print("ℹ️ 编译完成(无npm依赖或编译失败,继续...)") + else: + print("✅ 编译成功") + + return True + + def _step_upload(self, app: AppConfig) -> bool: + """上传代码""" + # 获取版本号 + version = datetime.now().strftime("%Y.%m.%d.%H%M") + desc = f"自动部署 - {datetime.now().strftime('%Y-%m-%d %H:%M')}" + + print(f"📤 上传代码...") + print(f" 版本: {version}") + print(f" 描述: {desc}") + + success, output = self._run_cli( + "upload", + "--version", version, + "--desc", desc, + project_path=app.project_path + ) + + if not success: + print(f"❌ 上传失败") + print(f" {output}") + + # 常见错误处理 + if "login" in output.lower(): + print("\n💡 提示: 请在微信开发者工具中登录后重试") + return False + + print("✅ 上传成功") + return True + + def _step_submit_audit(self, app: AppConfig) -> bool: + """提交审核""" + print("📝 提交审核...") + + # 使用CLI提交审核 + success, output = self._run_cli( + "submit-audit", + project_path=app.project_path + ) + + if not success: + if "未认证" in output or "认证" in output: + print("⚠️ 提交审核失败:未完成微信认证") + print(" 代码已上传,但需要完成认证后才能提交审核") + print(f" 运行: python mp_deploy.py cert {app.id}") + return True # 不算失败,只是需要认证 + + print(f"❌ 提交审核失败") + print(f" {output}") + return False + + print("✅ 审核已提交") + return True + + def submit_certification(self, app_id: str): + """提交企业认证""" + app = self.config.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return + + print("\n" + "=" * 60) + print(f" 📋 提交认证: {app.name}") + print("=" * 60) + + # 获取通用认证材料 + materials = self.config.get_cert_materials() + cert = app.certification + + # 合并材料(小程序配置优先) + enterprise_name = cert.get("enterprise_name") or materials.get("enterprise_name", "") + + print(f"\n📌 认证信息:") + print(f" 小程序: {app.name} ({app.appid})") + print(f" 企业名称: {enterprise_name}") + + # 检查必要材料 + missing = [] + if not enterprise_name: + missing.append("企业名称") + if not materials.get("license_number"): + missing.append("营业执照号") + if not materials.get("legal_persona_name"): + missing.append("法人姓名") + + if missing: + print(f"\n⚠️ 缺少认证材料:") + for m in missing: + print(f" - {m}") + + print(f"\n请先完善认证材料:") + print(f" 编辑: {self.config.config_file}") + print(f" 或运行: python mp_deploy.py cert-config") + return + + print("\n" + "-" * 40) + print("📋 认证方式说明:") + print("-" * 40) + print(""" + 【方式一】微信后台手动认证(推荐) + + 1. 登录小程序后台: https://mp.weixin.qq.com/ + 2. 设置 → 基本设置 → 微信认证 + 3. 选择"企业"类型 + 4. 填写企业信息、上传营业执照 + 5. 法人微信扫码验证 + 6. 支付认证费用(300元/年) + 7. 等待审核(1-5个工作日) + + 【方式二】通过第三方平台代认证(需开发) + + 如果你有第三方平台资质,可以通过API代认证: + 1. 配置第三方平台凭证 + 2. 获取授权 + 3. 调用认证API + + API接口: POST /wxa/sec/wxaauth + """) + + print("\n" + "-" * 40) + print("📝 认证材料清单:") + print("-" * 40) + print(""" + 必需材料: + ☐ 企业营业执照(扫描件或照片) + ☐ 法人身份证(正反面) + ☐ 法人微信号(用于扫码验证) + ☐ 联系人手机号 + ☐ 认证费用 300元 + + 认证有效期: 1年 + 到期后需重新认证(年审) + """) + + # 更新状态为待认证 + self.config.update_app(app_id, { + "certification": { + **cert, + "status": "pending", + "submit_time": datetime.now().isoformat() + } + }) + + print("\n✅ 已标记为待认证状态") + print(" 完成认证后运行: python mp_deploy.py cert-done " + app_id) + + def check_cert_status(self, app_id: str): + """检查认证状态""" + app = self.config.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return + + print("\n" + "=" * 60) + print(f" 🔍 认证状态: {app.name}") + print("=" * 60) + + cert = app.certification + status = cert.get("status", "unknown") + + status_info = { + "verified": ("✅ 已认证", "认证有效"), + "pending": ("⏳ 审核中", "请等待审核结果"), + "rejected": ("❌ 被拒绝", "请查看拒绝原因并重新提交"), + "expired": ("⚠️ 已过期", "需要重新认证(年审)"), + "unknown": ("❓ 未知", "请在微信后台确认状态") + } + + icon, desc = status_info.get(status, ("❓", "未知状态")) + + print(f"\n📌 当前状态: {icon}") + print(f" 说明: {desc}") + print(f" 企业: {cert.get('enterprise_name', '未填写')}") + + if cert.get("submit_time"): + print(f" 提交时间: {cert.get('submit_time')}") + + if cert.get("verify_time"): + print(f" 认证时间: {cert.get('verify_time')}") + + if cert.get("expire_time"): + print(f" 到期时间: {cert.get('expire_time')}") + + # 提示下一步操作 + print("\n" + "-" * 40) + if status == "unknown" or status == "rejected": + print("👉 下一步: python mp_deploy.py cert " + app_id) + elif status == "pending": + print("👉 等待审核,通常1-5个工作日") + print(" 审核通过后运行: python mp_deploy.py cert-done " + app_id) + elif status == "verified": + print("👉 可以发布小程序: python mp_deploy.py deploy " + app_id) + elif status == "expired": + print("👉 需要重新认证: python mp_deploy.py cert " + app_id) + + def mark_cert_done(self, app_id: str): + """标记认证完成""" + app = self.config.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return + + cert = app.certification + self.config.update_app(app_id, { + "certification": { + **cert, + "status": "verified", + "verify_time": datetime.now().isoformat(), + "expire_time": datetime.now().replace(year=datetime.now().year + 1).isoformat() + } + }) + + print(f"✅ 已标记 [{app.name}] 认证完成") + print(f" 有效期至: {datetime.now().year + 1}年") + + def release(self, app_id: str): + """发布上线""" + app = self.config.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return + + print("\n" + "=" * 60) + print(f" 🎉 发布上线: {app.name}") + print("=" * 60) + + # 检查认证状态 + if app.certification.get("status") != "verified": + print("\n⚠️ 警告: 小程序未完成认证") + print(" 未认证的小程序无法发布上线") + + choice = input("\n是否继续尝试? (y/n): ").strip().lower() + if choice != 'y': + return + + print("\n📦 正在发布...") + + # 尝试使用CLI发布 + success, output = self._run_cli("release", project_path=app.project_path) + + if success: + print("\n🎉 发布成功!小程序已上线") + else: + print(f"\n发布结果: {output}") + + if "认证" in output: + print("\n💡 提示: 请先完成微信认证") + print(f" 运行: python mp_deploy.py cert {app_id}") + else: + print("\n💡 提示: 请在微信后台手动发布") + print(" 1. 登录 https://mp.weixin.qq.com/") + print(" 2. 版本管理 → 审核版本 → 发布") + + def quick_upload(self, app_id: str, version: str = None, desc: str = None): + """快速上传代码""" + app = self.config.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return + + if not version: + version = datetime.now().strftime("%Y.%m.%d.%H%M") + if not desc: + desc = f"快速上传 - {datetime.now().strftime('%Y-%m-%d %H:%M')}" + + print(f"\n📤 上传代码: {app.name}") + print(f" 版本: {version}") + print(f" 描述: {desc}") + + success, output = self._run_cli( + "upload", + "--version", version, + "--desc", desc, + project_path=app.project_path + ) + + if success: + print("✅ 上传成功") + else: + print(f"❌ 上传失败: {output}") + + +def print_header(title: str): + print("\n" + "=" * 50) + print(f" {title}") + print("=" * 50) + + +def main(): + parser = argparse.ArgumentParser( + description="小程序一键部署工具 v2.0", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + python mp_deploy.py list 列出所有小程序 + python mp_deploy.py add 添加新小程序 + python mp_deploy.py deploy soul-party 一键部署 + python mp_deploy.py cert soul-party 提交认证 + python mp_deploy.py cert-status soul-party 查询认证状态 + python mp_deploy.py cert-done soul-party 标记认证完成 + python mp_deploy.py upload soul-party 仅上传代码 + python mp_deploy.py release soul-party 发布上线 + +部署流程: + 1. add 添加小程序配置 + 2. cert 提交企业认证(首次) + 3. cert-done 认证通过后标记 + 4. deploy 一键部署(编译+上传+提审) + 5. release 审核通过后发布 +""" + ) + + subparsers = parser.add_subparsers(dest="command", help="子命令") + + # list + subparsers.add_parser("list", help="列出所有小程序") + + # add + subparsers.add_parser("add", help="添加新小程序") + + # deploy + deploy_parser = subparsers.add_parser("deploy", help="一键部署") + deploy_parser.add_argument("app_id", help="小程序ID") + deploy_parser.add_argument("--skip-cert", action="store_true", help="跳过认证检查") + + # cert + cert_parser = subparsers.add_parser("cert", help="提交认证") + cert_parser.add_argument("app_id", help="小程序ID") + + # cert-status + cert_status_parser = subparsers.add_parser("cert-status", help="查询认证状态") + cert_status_parser.add_argument("app_id", help="小程序ID") + + # cert-done + cert_done_parser = subparsers.add_parser("cert-done", help="标记认证完成") + cert_done_parser.add_argument("app_id", help="小程序ID") + + # upload + upload_parser = subparsers.add_parser("upload", help="上传代码") + upload_parser.add_argument("app_id", help="小程序ID") + upload_parser.add_argument("-v", "--version", help="版本号") + upload_parser.add_argument("-d", "--desc", help="版本描述") + + # release + release_parser = subparsers.add_parser("release", help="发布上线") + release_parser.add_argument("app_id", help="小程序ID") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + deployer = MiniProgramDeployer() + + commands = { + "list": lambda: deployer.list_apps(), + "add": lambda: deployer.add_app(), + "deploy": lambda: deployer.deploy(args.app_id, args.skip_cert if hasattr(args, 'skip_cert') else False), + "cert": lambda: deployer.submit_certification(args.app_id), + "cert-status": lambda: deployer.check_cert_status(args.app_id), + "cert-done": lambda: deployer.mark_cert_done(args.app_id), + "upload": lambda: deployer.quick_upload(args.app_id, getattr(args, 'version', None), getattr(args, 'desc', None)), + "release": lambda: deployer.release(args.app_id), + } + + cmd_func = commands.get(args.command) + if cmd_func: + cmd_func() + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/开发文档/小程序管理/scripts/mp_full.py b/开发文档/小程序管理/scripts/mp_full.py new file mode 100644 index 0000000..2a754a2 --- /dev/null +++ b/开发文档/小程序管理/scripts/mp_full.py @@ -0,0 +1,555 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +小程序全能管理工具 v3.0 + +整合能力: +- 微信开发者工具CLI +- miniprogram-ci (npm官方工具) +- 微信开放平台API +- 多小程序管理 +- 自动化部署+提审 +- 汇总报告生成 + +使用方法: + python mp_full.py report # 生成汇总报告 + python mp_full.py check # 检查项目问题 + python mp_full.py auto # 全自动部署(上传+提审) + python mp_full.py batch-report # 批量生成所有小程序报告 +""" + +import os +import sys +import json +import subprocess +import argparse +from datetime import datetime +from pathlib import Path +from typing import Optional, Dict, List, Any +from dataclasses import dataclass, asdict, field + +# 配置文件路径 +SCRIPT_DIR = Path(__file__).parent +CONFIG_FILE = SCRIPT_DIR / "apps_config.json" +REPORT_DIR = SCRIPT_DIR / "reports" + + +@dataclass +class CheckResult: + """检查结果""" + name: str + status: str # ok, warning, error + message: str + fix_hint: str = "" + + +@dataclass +class AppReport: + """小程序报告""" + app_id: str + app_name: str + appid: str + check_time: str + checks: List[CheckResult] = field(default_factory=list) + summary: Dict = field(default_factory=dict) + + @property + def has_errors(self) -> bool: + return any(c.status == "error" for c in self.checks) + + @property + def has_warnings(self) -> bool: + return any(c.status == "warning" for c in self.checks) + + +class MiniProgramManager: + """小程序全能管理器""" + + # 工具路径 + WX_CLI = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli" + + def __init__(self): + self.config = self._load_config() + REPORT_DIR.mkdir(exist_ok=True) + + def _load_config(self) -> Dict: + if CONFIG_FILE.exists(): + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + return {"apps": []} + + def _save_config(self): + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(self.config, f, ensure_ascii=False, indent=2) + + def get_app(self, app_id: str) -> Optional[Dict]: + for app in self.config.get("apps", []): + if app["id"] == app_id or app["appid"] == app_id: + return app + return None + + def _run_cmd(self, cmd: List[str], timeout: int = 120) -> tuple: + """运行命令""" + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + return result.returncode == 0, result.stdout + result.stderr + except subprocess.TimeoutExpired: + return False, "命令执行超时" + except Exception as e: + return False, str(e) + + def _check_tool(self, tool: str) -> bool: + """检查工具是否可用""" + success, _ = self._run_cmd(["which", tool], timeout=5) + return success + + # ==================== 检查功能 ==================== + + def check_project(self, app_id: str) -> AppReport: + """检查项目问题""" + app = self.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return None + + report = AppReport( + app_id=app["id"], + app_name=app["name"], + appid=app["appid"], + check_time=datetime.now().isoformat() + ) + + project_path = app["project_path"] + + # 1. 检查项目路径 + if os.path.exists(project_path): + report.checks.append(CheckResult("项目路径", "ok", f"路径存在: {project_path}")) + else: + report.checks.append(CheckResult("项目路径", "error", f"路径不存在: {project_path}", "请检查项目路径配置")) + return report # 路径不存在,无法继续检查 + + # 2. 检查project.config.json + config_file = os.path.join(project_path, "project.config.json") + if os.path.exists(config_file): + with open(config_file, 'r') as f: + config = json.load(f) + + if config.get("appid") == app["appid"]: + report.checks.append(CheckResult("AppID配置", "ok", f"AppID正确: {app['appid']}")) + else: + report.checks.append(CheckResult("AppID配置", "error", + f"AppID不匹配: 配置={app['appid']}, 文件={config.get('appid')}", + "请修改project.config.json中的appid")) + else: + report.checks.append(CheckResult("项目配置", "error", "project.config.json不存在", "请确认这是有效的小程序项目")) + + # 3. 检查app.js + app_js = os.path.join(project_path, "app.js") + if os.path.exists(app_js): + with open(app_js, 'r') as f: + content = f.read() + + # 检查API域名 + if "baseUrl" in content or "apiBase" in content: + if "https://" in content: + report.checks.append(CheckResult("API域名", "ok", "已配置HTTPS域名")) + elif "http://localhost" in content: + report.checks.append(CheckResult("API域名", "warning", "使用本地开发地址", "发布前请更换为HTTPS域名")) + else: + report.checks.append(CheckResult("API域名", "warning", "未检测到HTTPS域名")) + + report.checks.append(CheckResult("入口文件", "ok", "app.js存在")) + else: + report.checks.append(CheckResult("入口文件", "error", "app.js不存在")) + + # 4. 检查app.json + app_json = os.path.join(project_path, "app.json") + if os.path.exists(app_json): + with open(app_json, 'r') as f: + app_config = json.load(f) + + pages = app_config.get("pages", []) + if pages: + report.checks.append(CheckResult("页面配置", "ok", f"共{len(pages)}个页面")) + else: + report.checks.append(CheckResult("页面配置", "error", "没有配置页面")) + + # 检查隐私配置 + if app_config.get("__usePrivacyCheck__"): + report.checks.append(CheckResult("隐私配置", "ok", "已启用隐私检查")) + else: + report.checks.append(CheckResult("隐私配置", "warning", "未启用隐私检查", "建议添加 __usePrivacyCheck__: true")) + else: + report.checks.append(CheckResult("应用配置", "error", "app.json不存在")) + + # 5. 检查认证状态 + cert_status = app.get("certification", {}).get("status", "unknown") + if cert_status == "verified": + report.checks.append(CheckResult("企业认证", "ok", "已完成认证")) + elif cert_status == "pending": + report.checks.append(CheckResult("企业认证", "warning", "认证审核中", "等待审核结果")) + elif cert_status == "expired": + report.checks.append(CheckResult("企业认证", "error", "认证已过期", "请尽快完成年审")) + else: + report.checks.append(CheckResult("企业认证", "warning", "未认证", "无法发布上线,请先完成认证")) + + # 6. 检查开发工具 + if os.path.exists(self.WX_CLI): + report.checks.append(CheckResult("开发者工具", "ok", "微信开发者工具已安装")) + else: + report.checks.append(CheckResult("开发者工具", "error", "微信开发者工具未安装")) + + # 7. 检查miniprogram-ci + if self._check_tool("miniprogram-ci"): + report.checks.append(CheckResult("miniprogram-ci", "ok", "npm工具已安装")) + else: + report.checks.append(CheckResult("miniprogram-ci", "warning", "miniprogram-ci未安装", "运行: npm install -g miniprogram-ci")) + + # 8. 检查私钥 + if app.get("private_key_path") and os.path.exists(app["private_key_path"]): + report.checks.append(CheckResult("上传密钥", "ok", "私钥文件存在")) + else: + report.checks.append(CheckResult("上传密钥", "warning", "未配置私钥", "在小程序后台下载代码上传密钥")) + + # 生成汇总 + ok_count = sum(1 for c in report.checks if c.status == "ok") + warn_count = sum(1 for c in report.checks if c.status == "warning") + error_count = sum(1 for c in report.checks if c.status == "error") + + report.summary = { + "total": len(report.checks), + "ok": ok_count, + "warning": warn_count, + "error": error_count, + "can_deploy": error_count == 0, + "can_release": cert_status == "verified" and error_count == 0 + } + + return report + + def print_report(self, report: AppReport): + """打印报告""" + print("\n" + "=" * 70) + print(f" 📊 项目检查报告: {report.app_name}") + print("=" * 70) + print(f" AppID: {report.appid}") + print(f" 检查时间: {report.check_time}") + print("-" * 70) + + status_icons = {"ok": "✅", "warning": "⚠️", "error": "❌"} + + for check in report.checks: + icon = status_icons.get(check.status, "❓") + print(f" {icon} {check.name}: {check.message}") + if check.fix_hint: + print(f" 💡 {check.fix_hint}") + + print("-" * 70) + s = report.summary + print(f" 📈 汇总: 通过 {s['ok']} / 警告 {s['warning']} / 错误 {s['error']}") + + if s['can_release']: + print(" 🎉 状态: 可以发布上线") + elif s['can_deploy']: + print(" 📦 状态: 可以上传代码,但无法发布(需完成认证)") + else: + print(" 🚫 状态: 存在错误,请先修复") + + print("=" * 70 + "\n") + + def save_report(self, report: AppReport): + """保存报告到文件""" + filename = f"report_{report.app_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + filepath = REPORT_DIR / filename + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(asdict(report), f, ensure_ascii=False, indent=2) + + return filepath + + # ==================== 自动化部署 ==================== + + def auto_deploy(self, app_id: str, version: str = None, desc: str = None, submit_audit: bool = True) -> bool: + """全自动部署:编译 → 上传 → 提审""" + app = self.get_app(app_id) + if not app: + print(f"❌ 未找到小程序: {app_id}") + return False + + print("\n" + "=" * 70) + print(f" 🚀 全自动部署: {app['name']}") + print("=" * 70) + + # 1. 先检查项目 + print("\n📋 步骤1: 检查项目...") + report = self.check_project(app_id) + if report.has_errors: + print("❌ 项目存在错误,无法部署") + self.print_report(report) + return False + print("✅ 项目检查通过") + + # 2. 准备版本信息 + if not version: + version = datetime.now().strftime("%Y.%m.%d.%H%M") + if not desc: + desc = f"自动部署 - {datetime.now().strftime('%Y-%m-%d %H:%M')}" + + print(f"\n📦 步骤2: 上传代码...") + print(f" 版本: {version}") + print(f" 描述: {desc}") + + # 3. 上传代码 + success = self._upload_code(app, version, desc) + if not success: + print("❌ 代码上传失败") + return False + print("✅ 代码上传成功") + + # 4. 提交审核 + if submit_audit: + print(f"\n📝 步骤3: 提交审核...") + cert_status = app.get("certification", {}).get("status", "unknown") + + if cert_status != "verified": + print(f"⚠️ 认证状态: {cert_status}") + print(" 未认证的小程序无法提交审核") + print(" 代码已上传到开发版,请在微信后台手动提交") + print("\n" + "-" * 40) + print("👉 下一步操作:") + print(" 1. 完成企业认证") + print(" 2. 在微信后台提交审核") + print(" 3. 审核通过后发布上线") + else: + # 尝试通过API提交审核 + audit_success = self._submit_audit_via_api(app) + if audit_success: + print("✅ 审核已提交") + else: + print("⚠️ 自动提审失败,请在微信后台手动提交") + print(" 登录: https://mp.weixin.qq.com/") + print(" 版本管理 → 开发版本 → 提交审核") + + # 5. 生成报告 + print(f"\n📊 步骤4: 生成报告...") + report_file = self.save_report(report) + print(f"✅ 报告已保存: {report_file}") + + print("\n" + "=" * 70) + print(" 🎉 部署完成!") + print("=" * 70) + + return True + + def _upload_code(self, app: Dict, version: str, desc: str) -> bool: + """上传代码(优先使用CLI)""" + project_path = app["project_path"] + + # 方法1:使用微信开发者工具CLI + if os.path.exists(self.WX_CLI): + cmd = [ + self.WX_CLI, "upload", + "--project", project_path, + "--version", version, + "--desc", desc + ] + success, output = self._run_cmd(cmd, timeout=120) + if success: + return True + print(f" CLI上传失败: {output[:200]}") + + # 方法2:使用miniprogram-ci + if self._check_tool("miniprogram-ci") and app.get("private_key_path"): + cmd = [ + "miniprogram-ci", "upload", + "--pp", project_path, + "--pkp", app["private_key_path"], + "--appid", app["appid"], + "--uv", version, + "-r", "1", + "--desc", desc + ] + success, output = self._run_cmd(cmd, timeout=120) + if success: + return True + print(f" miniprogram-ci上传失败: {output[:200]}") + + return False + + def _submit_audit_via_api(self, app: Dict) -> bool: + """通过API提交审核(需要access_token)""" + # 这里需要access_token才能调用API + # 目前返回False,提示用户手动提交 + return False + + # ==================== 汇总报告 ==================== + + def generate_summary_report(self): + """生成所有小程序的汇总报告""" + apps = self.config.get("apps", []) + + if not apps: + print("📭 暂无配置的小程序") + return + + print("\n" + "=" * 80) + print(" 📊 小程序管理汇总报告") + print(f" 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 80) + + all_reports = [] + + for app in apps: + report = self.check_project(app["id"]) + if report: + all_reports.append(report) + + # 打印汇总表格 + print("\n┌" + "─" * 78 + "┐") + print(f"│ {'小程序名称':<20} │ {'AppID':<25} │ {'状态':<10} │ {'可发布':<8} │") + print("├" + "─" * 78 + "┤") + + for report in all_reports: + status = "✅ 正常" if not report.has_errors else "❌ 错误" + can_release = "✅" if report.summary.get("can_release") else "❌" + print(f"│ {report.app_name:<20} │ {report.appid:<25} │ {status:<10} │ {can_release:<8} │") + + print("└" + "─" * 78 + "┘") + + # 统计 + total = len(all_reports) + ok_count = sum(1 for r in all_reports if not r.has_errors and not r.has_warnings) + warn_count = sum(1 for r in all_reports if r.has_warnings and not r.has_errors) + error_count = sum(1 for r in all_reports if r.has_errors) + can_release = sum(1 for r in all_reports if r.summary.get("can_release")) + + print(f"\n📈 统计:") + print(f" 总计: {total} 个小程序") + print(f" 正常: {ok_count} | 警告: {warn_count} | 错误: {error_count}") + print(f" 可发布: {can_release} 个") + + # 问题清单 + issues = [] + for report in all_reports: + for check in report.checks: + if check.status == "error": + issues.append((report.app_name, check.name, check.message, check.fix_hint)) + + if issues: + print(f"\n⚠️ 问题清单 ({len(issues)} 个):") + print("-" * 60) + for app_name, check_name, message, hint in issues: + print(f" [{app_name}] {check_name}: {message}") + if hint: + print(f" 💡 {hint}") + else: + print(f"\n✅ 所有小程序状态正常") + + # 待办事项 + print(f"\n📋 待办事项:") + for report in all_reports: + cert_status = "unknown" + for check in report.checks: + if check.name == "企业认证": + if "审核中" in check.message: + cert_status = "pending" + elif "已完成" in check.message: + cert_status = "verified" + elif "未认证" in check.message: + cert_status = "unknown" + break + + if cert_status == "pending": + print(f" ⏳ {report.app_name}: 等待认证审核结果") + elif cert_status == "unknown": + print(f" 📝 {report.app_name}: 需要完成企业认证") + + print("\n" + "=" * 80) + + # 保存汇总报告 + summary_file = REPORT_DIR / f"summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + summary_data = { + "generated_at": datetime.now().isoformat(), + "total_apps": total, + "summary": { + "ok": ok_count, + "warning": warn_count, + "error": error_count, + "can_release": can_release + }, + "apps": [asdict(r) for r in all_reports] + } + with open(summary_file, 'w', encoding='utf-8') as f: + json.dump(summary_data, f, ensure_ascii=False, indent=2) + + print(f"📁 报告已保存: {summary_file}\n") + + +def main(): + parser = argparse.ArgumentParser( + description="小程序全能管理工具 v3.0", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + python mp_full.py report 生成汇总报告 + python mp_full.py check soul-party 检查项目问题 + python mp_full.py auto soul-party 全自动部署(上传+提审) + python mp_full.py auto soul-party -v 1.0.13 -d "修复问题" + +流程说明: + ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ + │ 检查 │ → │ 编译 │ → │ 上传 │ → │ 提审 │ → │ 发布 │ + │ check │ │ build │ │ upload │ │ audit │ │ release │ + └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ + +工具整合: + • 微信开发者工具CLI - 本地编译上传 + • miniprogram-ci - npm官方CI工具 + • 开放平台API - 审核/发布/认证 +""" + ) + + subparsers = parser.add_subparsers(dest="command", help="子命令") + + # report + subparsers.add_parser("report", help="生成汇总报告") + + # check + check_parser = subparsers.add_parser("check", help="检查项目问题") + check_parser.add_argument("app_id", help="小程序ID") + + # auto + auto_parser = subparsers.add_parser("auto", help="全自动部署") + auto_parser.add_argument("app_id", help="小程序ID") + auto_parser.add_argument("-v", "--version", help="版本号") + auto_parser.add_argument("-d", "--desc", help="版本描述") + auto_parser.add_argument("--no-audit", action="store_true", help="不提交审核") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + manager = MiniProgramManager() + + if args.command == "report": + manager.generate_summary_report() + + elif args.command == "check": + report = manager.check_project(args.app_id) + if report: + manager.print_report(report) + manager.save_report(report) + + elif args.command == "auto": + manager.auto_deploy( + args.app_id, + version=args.version, + desc=args.desc, + submit_audit=not args.no_audit + ) + + +if __name__ == "__main__": + main() diff --git a/开发文档/小程序管理/scripts/mp_manager.py b/开发文档/小程序管理/scripts/mp_manager.py new file mode 100644 index 0000000..197f780 --- /dev/null +++ b/开发文档/小程序管理/scripts/mp_manager.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +微信小程序管理命令行工具 + +使用方法: + python mp_manager.py status # 查看小程序状态 + python mp_manager.py audit # 查看审核状态 + python mp_manager.py release # 发布上线 + python mp_manager.py qrcode # 生成小程序码 + python mp_manager.py domain # 查看/配置域名 + python mp_manager.py privacy # 配置隐私协议 + python mp_manager.py data # 查看数据分析 +""" + +import os +import sys +import argparse +from datetime import datetime, timedelta +from pathlib import Path + +# 添加当前目录到路径 +sys.path.insert(0, str(Path(__file__).parent)) + +from mp_api import MiniProgramAPI, APIError, create_api_from_env + + +def print_header(title: str): + """打印标题""" + print("\n" + "=" * 50) + print(f" {title}") + print("=" * 50) + + +def print_success(message: str): + """打印成功信息""" + print(f"✅ {message}") + + +def print_error(message: str): + """打印错误信息""" + print(f"❌ {message}") + + +def print_info(message: str): + """打印信息""" + print(f"ℹ️ {message}") + + +def cmd_status(api: MiniProgramAPI, args): + """查看小程序状态""" + print_header("小程序基础信息") + + try: + info = api.get_basic_info() + print(f"\n📱 AppID: {info.appid}") + print(f"📝 名称: {info.nickname}") + print(f"📄 简介: {info.signature}") + print(f"🏢 主体: {info.principal_name}") + print(f"✓ 认证状态: {'已认证' if info.realname_status == 1 else '未认证'}") + + if info.head_image_url: + print(f"🖼️ 头像: {info.head_image_url}") + + # 获取类目 + print("\n📂 已设置类目:") + categories = api.get_category() + if categories: + for cat in categories: + print(f" - {cat.get('first_class', '')} > {cat.get('second_class', '')}") + else: + print(" (未设置类目)") + + except APIError as e: + print_error(f"获取信息失败: {e}") + + +def cmd_audit(api: MiniProgramAPI, args): + """查看审核状态""" + print_header("审核状态") + + try: + status = api.get_latest_audit_status() + print(f"\n🔢 审核单ID: {status.auditid}") + print(f"📊 状态: {status.status_text}") + + if status.reason: + print(f"\n❗ 拒绝原因:") + print(f" {status.reason}") + + if status.screenshot: + print(f"\n📸 问题截图: {status.screenshot}") + + if status.status == 0: + print("\n👉 下一步: 运行 'python mp_manager.py release' 发布上线") + elif status.status == 1: + print("\n👉 请根据拒绝原因修改后重新提交审核") + elif status.status == 2: + print("\n👉 审核中,请耐心等待(通常1-3个工作日)") + print(" 可运行 'python mp_manager.py audit' 再次查询") + + except APIError as e: + print_error(f"获取审核状态失败: {e}") + + +def cmd_submit(api: MiniProgramAPI, args): + """提交审核""" + print_header("提交审核") + + version_desc = args.desc or input("请输入版本说明: ").strip() + if not version_desc: + print_error("版本说明不能为空") + return + + try: + # 获取页面列表 + pages = api.get_page() + if not pages: + print_error("未找到页面,请先上传代码") + return + + print(f"\n📄 检测到 {len(pages)} 个页面:") + for p in pages[:5]: + print(f" - {p}") + if len(pages) > 5: + print(f" ... 还有 {len(pages) - 5} 个") + + # 确认提交 + confirm = input("\n确认提交审核? (y/n): ").strip().lower() + if confirm != 'y': + print_info("已取消") + return + + auditid = api.submit_audit(version_desc=version_desc) + print_success(f"审核已提交,审核单ID: {auditid}") + print("\n👉 运行 'python mp_manager.py audit' 查询审核状态") + + except APIError as e: + print_error(f"提交审核失败: {e}") + + +def cmd_release(api: MiniProgramAPI, args): + """发布上线""" + print_header("发布上线") + + try: + # 先检查审核状态 + status = api.get_latest_audit_status() + if status.status != 0: + print_error(f"当前审核状态: {status.status_text}") + print_info("只有审核通过的版本才能发布") + return + + print(f"📊 审核状态: {status.status_text}") + + # 确认发布 + confirm = input("\n确认发布上线? (y/n): ").strip().lower() + if confirm != 'y': + print_info("已取消") + return + + api.release() + print_success("🎉 发布成功!小程序已上线") + + except APIError as e: + print_error(f"发布失败: {e}") + + +def cmd_revert(api: MiniProgramAPI, args): + """版本回退""" + print_header("版本回退") + + try: + # 获取可回退版本 + history = api.get_revert_history() + if not history: + print_info("没有可回退的版本") + return + + print("\n📜 可回退版本:") + for v in history: + print(f" - {v.get('user_version', '?')}: {v.get('user_desc', '')}") + + # 确认回退 + confirm = input("\n确认回退到上一版本? (y/n): ").strip().lower() + if confirm != 'y': + print_info("已取消") + return + + api.revert_code_release() + print_success("版本回退成功") + + except APIError as e: + print_error(f"版本回退失败: {e}") + + +def cmd_qrcode(api: MiniProgramAPI, args): + """生成小程序码""" + print_header("生成小程序码") + + # 场景选择 + print("\n选择类型:") + print(" 1. 体验版二维码") + print(" 2. 小程序码(有限制,每个path最多10万个)") + print(" 3. 无限小程序码(推荐)") + + choice = args.type or input("\n请选择 (1/2/3): ").strip() + + output_file = args.output or f"qrcode_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + + try: + if choice == "1": + # 体验版二维码 + path = args.path or input("页面路径 (默认首页): ").strip() or None + data = api.get_qrcode(path) + + elif choice == "2": + # 小程序码 + path = args.path or input("页面路径: ").strip() + if not path: + print_error("页面路径不能为空") + return + data = api.get_wxacode(path) + + elif choice == "3": + # 无限小程序码 + scene = args.scene or input("场景值 (最长32字符): ").strip() + if not scene: + print_error("场景值不能为空") + return + page = args.path or input("页面路径 (需已发布): ").strip() or None + data = api.get_wxacode_unlimit(scene, page) + + else: + print_error("无效选择") + return + + # 保存文件 + with open(output_file, "wb") as f: + f.write(data) + + print_success(f"小程序码已保存: {output_file}") + + # 尝试打开 + if sys.platform == "darwin": + os.system(f'open "{output_file}"') + + except APIError as e: + print_error(f"生成小程序码失败: {e}") + + +def cmd_domain(api: MiniProgramAPI, args): + """查看/配置域名""" + print_header("域名配置") + + try: + # 获取当前配置 + domains = api.get_domain() + webview_domains = api.get_webview_domain() + + print("\n🌐 服务器域名:") + print(f" request: {', '.join(domains.get('requestdomain', [])) or '(无)'}") + print(f" wsrequest: {', '.join(domains.get('wsrequestdomain', [])) or '(无)'}") + print(f" upload: {', '.join(domains.get('uploaddomain', [])) or '(无)'}") + print(f" download: {', '.join(domains.get('downloaddomain', [])) or '(无)'}") + + print(f"\n🔗 业务域名:") + print(f" webview: {', '.join(webview_domains) or '(无)'}") + + # 是否要配置 + if args.set_request: + print(f"\n配置 request 域名: {args.set_request}") + api.set_domain(requestdomain=[args.set_request]) + print_success("域名配置成功") + + except APIError as e: + print_error(f"域名配置失败: {e}") + + +def cmd_privacy(api: MiniProgramAPI, args): + """配置隐私协议""" + print_header("隐私协议配置") + + try: + # 获取当前配置 + settings = api.get_privacy_setting() + + print("\n📋 当前隐私设置:") + setting_list = settings.get("setting_list", []) + if setting_list: + for s in setting_list: + print(f" - {s.get('privacy_key', '?')}: {s.get('privacy_text', '')}") + else: + print(" (未配置)") + + owner = settings.get("owner_setting", {}) + if owner: + print(f"\n📧 联系方式:") + if owner.get("contact_email"): + print(f" 邮箱: {owner['contact_email']}") + if owner.get("contact_phone"): + print(f" 电话: {owner['contact_phone']}") + + # 快速配置 + if args.quick: + print("\n⚡ 快速配置常用隐私项...") + + default_settings = [ + {"privacy_key": "UserInfo", "privacy_text": "用于展示您的头像和昵称"}, + {"privacy_key": "Location", "privacy_text": "用于获取您的位置信息以推荐附近服务"}, + {"privacy_key": "PhoneNumber", "privacy_text": "用于登录验证和订单通知"}, + ] + + api.set_privacy_setting( + setting_list=default_settings, + contact_email=args.email or "contact@example.com", + contact_phone=args.phone or "15880802661" + ) + print_success("隐私协议配置成功") + + except APIError as e: + print_error(f"隐私协议配置失败: {e}") + + +def cmd_data(api: MiniProgramAPI, args): + """查看数据分析""" + print_header("数据分析") + + # 默认查询最近7天 + end_date = datetime.now().strftime("%Y%m%d") + begin_date = (datetime.now() - timedelta(days=7)).strftime("%Y%m%d") + + if args.begin: + begin_date = args.begin + if args.end: + end_date = args.end + + try: + print(f"\n📊 访问趋势 ({begin_date} ~ {end_date}):") + + data = api.get_daily_visit_trend(begin_date, end_date) + if not data: + print(" (暂无数据)") + return + + # 统计汇总 + total_pv = sum(d.get("visit_pv", 0) for d in data) + total_uv = sum(d.get("visit_uv", 0) for d in data) + total_new = sum(d.get("visit_uv_new", 0) for d in data) + + print(f"\n📈 汇总数据:") + print(f" 总访问次数: {total_pv:,}") + print(f" 总访问人数: {total_uv:,}") + print(f" 新用户数: {total_new:,}") + + print(f"\n📅 每日明细:") + for d in data[-7:]: # 只显示最近7天 + date = d.get("ref_date", "?") + pv = d.get("visit_pv", 0) + uv = d.get("visit_uv", 0) + stay = d.get("stay_time_uv", 0) + print(f" {date}: PV={pv}, UV={uv}, 人均停留={stay:.1f}秒") + + except APIError as e: + print_error(f"获取数据失败: {e}") + + +def cmd_quota(api: MiniProgramAPI, args): + """查看API配额""" + print_header("API配额") + + common_apis = [ + "/wxa/getwxacode", + "/wxa/getwxacodeunlimit", + "/wxa/genwxashortlink", + "/wxa/submit_audit", + "/cgi-bin/message/subscribe/send" + ] + + try: + for cgi_path in common_apis: + try: + quota = api.get_api_quota(cgi_path) + daily_limit = quota.get("daily_limit", 0) + used = quota.get("used", 0) + remain = quota.get("remain", 0) + + print(f"\n📌 {cgi_path}") + print(f" 每日限额: {daily_limit:,}") + print(f" 已使用: {used:,}") + print(f" 剩余: {remain:,}") + except APIError: + pass + + except APIError as e: + print_error(f"获取配额失败: {e}") + + +def cmd_cli(api: MiniProgramAPI, args): + """使用微信开发者工具CLI""" + print_header("微信开发者工具CLI") + + cli_path = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli" + project_path = args.project or os.getenv("MINIPROGRAM_PATH", "") + + if not project_path: + project_path = input("请输入小程序项目路径: ").strip() + + if not os.path.exists(project_path): + print_error(f"项目路径不存在: {project_path}") + return + + if not os.path.exists(cli_path): + print_error("未找到微信开发者工具,请先安装") + return + + print(f"\n📂 项目路径: {project_path}") + print("\n选择操作:") + print(" 1. 打开项目") + print(" 2. 预览(生成二维码)") + print(" 3. 上传代码") + print(" 4. 编译") + + choice = input("\n请选择: ").strip() + + if choice == "1": + os.system(f'"{cli_path}" -o "{project_path}"') + print_success("项目已打开") + + elif choice == "2": + output = f"{project_path}/preview.png" + os.system(f'"{cli_path}" preview --project "{project_path}" --qr-format image --qr-output "{output}"') + if os.path.exists(output): + print_success(f"预览二维码已生成: {output}") + os.system(f'open "{output}"') + else: + print_error("生成失败,请检查开发者工具是否已登录") + + elif choice == "3": + version = input("版本号 (如 1.0.0): ").strip() + desc = input("版本说明: ").strip() + os.system(f'"{cli_path}" upload --project "{project_path}" --version "{version}" --desc "{desc}"') + print_success("代码上传完成") + + elif choice == "4": + os.system(f'"{cli_path}" build-npm --project "{project_path}"') + print_success("编译完成") + + else: + print_error("无效选择") + + +def main(): + parser = argparse.ArgumentParser( + description="微信小程序管理工具", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + python mp_manager.py status 查看小程序状态 + python mp_manager.py audit 查看审核状态 + python mp_manager.py submit -d "修复xxx问题" 提交审核 + python mp_manager.py release 发布上线 + python mp_manager.py qrcode -t 3 -s "id=123" 生成无限小程序码 + python mp_manager.py domain 查看域名配置 + python mp_manager.py privacy --quick 快速配置隐私协议 + python mp_manager.py data 查看数据分析 + python mp_manager.py cli 使用开发者工具CLI +""" + ) + + subparsers = parser.add_subparsers(dest="command", help="子命令") + + # status + subparsers.add_parser("status", help="查看小程序状态") + + # audit + subparsers.add_parser("audit", help="查看审核状态") + + # submit + submit_parser = subparsers.add_parser("submit", help="提交审核") + submit_parser.add_argument("-d", "--desc", help="版本说明") + + # release + subparsers.add_parser("release", help="发布上线") + + # revert + subparsers.add_parser("revert", help="版本回退") + + # qrcode + qr_parser = subparsers.add_parser("qrcode", help="生成小程序码") + qr_parser.add_argument("-t", "--type", choices=["1", "2", "3"], help="类型:1=体验版,2=小程序码,3=无限小程序码") + qr_parser.add_argument("-p", "--path", help="页面路径") + qr_parser.add_argument("-s", "--scene", help="场景值(类型3时使用)") + qr_parser.add_argument("-o", "--output", help="输出文件名") + + # domain + domain_parser = subparsers.add_parser("domain", help="查看/配置域名") + domain_parser.add_argument("--set-request", help="设置request域名") + + # privacy + privacy_parser = subparsers.add_parser("privacy", help="配置隐私协议") + privacy_parser.add_argument("--quick", action="store_true", help="快速配置常用隐私项") + privacy_parser.add_argument("--email", help="联系邮箱") + privacy_parser.add_argument("--phone", help="联系电话") + + # data + data_parser = subparsers.add_parser("data", help="查看数据分析") + data_parser.add_argument("--begin", help="开始日期 YYYYMMDD") + data_parser.add_argument("--end", help="结束日期 YYYYMMDD") + + # quota + subparsers.add_parser("quota", help="查看API配额") + + # cli + cli_parser = subparsers.add_parser("cli", help="使用微信开发者工具CLI") + cli_parser.add_argument("-p", "--project", help="小程序项目路径") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + # 创建API实例 + try: + api = create_api_from_env() + except Exception as e: + print_error(f"初始化API失败: {e}") + print_info("请检查 .env 文件中的配置") + return + + # 执行命令 + commands = { + "status": cmd_status, + "audit": cmd_audit, + "submit": cmd_submit, + "release": cmd_release, + "revert": cmd_revert, + "qrcode": cmd_qrcode, + "domain": cmd_domain, + "privacy": cmd_privacy, + "data": cmd_data, + "quota": cmd_quota, + "cli": cmd_cli, + } + + cmd_func = commands.get(args.command) + if cmd_func: + try: + cmd_func(api, args) + finally: + api.close() + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113301.json b/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113301.json new file mode 100644 index 0000000..628d5a3 --- /dev/null +++ b/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113301.json @@ -0,0 +1,76 @@ +{ + "app_id": "soul-party", + "app_name": "Soul派对", + "appid": "wxb8bbb2b10dec74aa", + "check_time": "2026-01-25T11:33:01.054516", + "checks": [ + { + "name": "项目路径", + "status": "ok", + "message": "路径存在: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram", + "fix_hint": "" + }, + { + "name": "AppID配置", + "status": "ok", + "message": "AppID正确: wxb8bbb2b10dec74aa", + "fix_hint": "" + }, + { + "name": "API域名", + "status": "ok", + "message": "已配置HTTPS域名", + "fix_hint": "" + }, + { + "name": "入口文件", + "status": "ok", + "message": "app.js存在", + "fix_hint": "" + }, + { + "name": "页面配置", + "status": "ok", + "message": "共9个页面", + "fix_hint": "" + }, + { + "name": "隐私配置", + "status": "warning", + "message": "未启用隐私检查", + "fix_hint": "建议添加 __usePrivacyCheck__: true" + }, + { + "name": "企业认证", + "status": "warning", + "message": "认证审核中", + "fix_hint": "等待审核结果" + }, + { + "name": "开发者工具", + "status": "ok", + "message": "微信开发者工具已安装", + "fix_hint": "" + }, + { + "name": "miniprogram-ci", + "status": "ok", + "message": "npm工具已安装", + "fix_hint": "" + }, + { + "name": "上传密钥", + "status": "warning", + "message": "未配置私钥", + "fix_hint": "在小程序后台下载代码上传密钥" + } + ], + "summary": { + "total": 10, + "ok": 7, + "warning": 3, + "error": 0, + "can_deploy": true, + "can_release": false + } +} \ No newline at end of file diff --git a/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113423.json b/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113423.json new file mode 100644 index 0000000..60ca8c2 --- /dev/null +++ b/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113423.json @@ -0,0 +1,76 @@ +{ + "app_id": "soul-party", + "app_name": "Soul派对", + "appid": "wxb8bbb2b10dec74aa", + "check_time": "2026-01-25T11:34:23.760802", + "checks": [ + { + "name": "项目路径", + "status": "ok", + "message": "路径存在: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram", + "fix_hint": "" + }, + { + "name": "AppID配置", + "status": "ok", + "message": "AppID正确: wxb8bbb2b10dec74aa", + "fix_hint": "" + }, + { + "name": "API域名", + "status": "ok", + "message": "已配置HTTPS域名", + "fix_hint": "" + }, + { + "name": "入口文件", + "status": "ok", + "message": "app.js存在", + "fix_hint": "" + }, + { + "name": "页面配置", + "status": "ok", + "message": "共9个页面", + "fix_hint": "" + }, + { + "name": "隐私配置", + "status": "ok", + "message": "已启用隐私检查", + "fix_hint": "" + }, + { + "name": "企业认证", + "status": "warning", + "message": "认证审核中", + "fix_hint": "等待审核结果" + }, + { + "name": "开发者工具", + "status": "ok", + "message": "微信开发者工具已安装", + "fix_hint": "" + }, + { + "name": "miniprogram-ci", + "status": "ok", + "message": "npm工具已安装", + "fix_hint": "" + }, + { + "name": "上传密钥", + "status": "warning", + "message": "未配置私钥", + "fix_hint": "在小程序后台下载代码上传密钥" + } + ], + "summary": { + "total": 10, + "ok": 8, + "warning": 2, + "error": 0, + "can_deploy": true, + "can_release": false + } +} \ No newline at end of file diff --git a/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113434.json b/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113434.json new file mode 100644 index 0000000..83e19c8 --- /dev/null +++ b/开发文档/小程序管理/scripts/reports/report_soul-party_20260125_113434.json @@ -0,0 +1,76 @@ +{ + "app_id": "soul-party", + "app_name": "Soul派对", + "appid": "wxb8bbb2b10dec74aa", + "check_time": "2026-01-25T11:34:28.854418", + "checks": [ + { + "name": "项目路径", + "status": "ok", + "message": "路径存在: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram", + "fix_hint": "" + }, + { + "name": "AppID配置", + "status": "ok", + "message": "AppID正确: wxb8bbb2b10dec74aa", + "fix_hint": "" + }, + { + "name": "API域名", + "status": "ok", + "message": "已配置HTTPS域名", + "fix_hint": "" + }, + { + "name": "入口文件", + "status": "ok", + "message": "app.js存在", + "fix_hint": "" + }, + { + "name": "页面配置", + "status": "ok", + "message": "共9个页面", + "fix_hint": "" + }, + { + "name": "隐私配置", + "status": "ok", + "message": "已启用隐私检查", + "fix_hint": "" + }, + { + "name": "企业认证", + "status": "warning", + "message": "认证审核中", + "fix_hint": "等待审核结果" + }, + { + "name": "开发者工具", + "status": "ok", + "message": "微信开发者工具已安装", + "fix_hint": "" + }, + { + "name": "miniprogram-ci", + "status": "ok", + "message": "npm工具已安装", + "fix_hint": "" + }, + { + "name": "上传密钥", + "status": "warning", + "message": "未配置私钥", + "fix_hint": "在小程序后台下载代码上传密钥" + } + ], + "summary": { + "total": 10, + "ok": 8, + "warning": 2, + "error": 0, + "can_deploy": true, + "can_release": false + } +} \ No newline at end of file diff --git a/开发文档/小程序管理/scripts/reports/summary_20260125_113255.json b/开发文档/小程序管理/scripts/reports/summary_20260125_113255.json new file mode 100644 index 0000000..88ed994 --- /dev/null +++ b/开发文档/小程序管理/scripts/reports/summary_20260125_113255.json @@ -0,0 +1,88 @@ +{ + "generated_at": "2026-01-25T11:32:55.447833", + "total_apps": 1, + "summary": { + "ok": 0, + "warning": 1, + "error": 0, + "can_release": 0 + }, + "apps": [ + { + "app_id": "soul-party", + "app_name": "Soul派对", + "appid": "wxb8bbb2b10dec74aa", + "check_time": "2026-01-25T11:32:55.428736", + "checks": [ + { + "name": "项目路径", + "status": "ok", + "message": "路径存在: /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram", + "fix_hint": "" + }, + { + "name": "AppID配置", + "status": "ok", + "message": "AppID正确: wxb8bbb2b10dec74aa", + "fix_hint": "" + }, + { + "name": "API域名", + "status": "ok", + "message": "已配置HTTPS域名", + "fix_hint": "" + }, + { + "name": "入口文件", + "status": "ok", + "message": "app.js存在", + "fix_hint": "" + }, + { + "name": "页面配置", + "status": "ok", + "message": "共9个页面", + "fix_hint": "" + }, + { + "name": "隐私配置", + "status": "warning", + "message": "未启用隐私检查", + "fix_hint": "建议添加 __usePrivacyCheck__: true" + }, + { + "name": "企业认证", + "status": "warning", + "message": "认证审核中", + "fix_hint": "等待审核结果" + }, + { + "name": "开发者工具", + "status": "ok", + "message": "微信开发者工具已安装", + "fix_hint": "" + }, + { + "name": "miniprogram-ci", + "status": "ok", + "message": "npm工具已安装", + "fix_hint": "" + }, + { + "name": "上传密钥", + "status": "warning", + "message": "未配置私钥", + "fix_hint": "在小程序后台下载代码上传密钥" + } + ], + "summary": { + "total": 10, + "ok": 7, + "warning": 3, + "error": 0, + "can_deploy": true, + "can_release": false + } + } + ] +} \ No newline at end of file diff --git a/开发文档/小程序管理/scripts/requirements.txt b/开发文档/小程序管理/scripts/requirements.txt new file mode 100644 index 0000000..29b7c9d --- /dev/null +++ b/开发文档/小程序管理/scripts/requirements.txt @@ -0,0 +1,7 @@ +# 微信小程序管理工具依赖 + +# HTTP客户端 +httpx>=0.25.0 + +# 环境变量管理 +python-dotenv>=1.0.0 diff --git a/开发文档/提现功能完整技术文档.md b/开发文档/提现功能完整技术文档.md new file mode 100644 index 0000000..14e9a5e --- /dev/null +++ b/开发文档/提现功能完整技术文档.md @@ -0,0 +1,1033 @@ +# 提现功能技术文档(微信支付API集成) + +## 文档说明 + +本文档专注于**微信支付商家转账到零钱API**的集成方法,包括: +- 微信支付官方API文档 +- 签名生成算法 +- 加密解密算法 +- 完整代码实现 +- 测试验证方法 + +**适用场景**:实现用户提现功能,将资金从商户号转账到用户微信零钱。 + +--- + +## 目录 + +1. [业务场景](#业务场景) +2. [微信支付官方API文档](#微信支付官方api文档) +3. [前置准备](#前置准备) +4. [API集成](#api集成) +5. [签名算法](#签名算法) +6. [加密解密](#加密解密) +7. [代码实现](#代码实现) +8. [测试验证](#测试验证) + +--- + +## 业务场景 + +### 典型流程 + +``` +用户申请提现 + ↓ +系统审核通过 + ↓ +调用微信支付【商家转账到零钱API】 + ↓ +微信返回处理中(PROCESSING) + ↓ +微信异步处理(7-15秒) + ↓ +微信【主动回调】通知转账结果 + ↓ +系统接收回调,验签、解密 + ↓ +更新提现状态 + ↓ +用户确认收款 +``` + +### 关键步骤 + +1. **发起转账**:调用微信API发起转账 +2. **接收回调**:接收微信异步通知 +3. **验证签名**:验证回调的真实性 +4. **解密数据**:解密回调中的加密数据 +5. **查询状态**:主动查询转账状态 + +--- + +## 微信支付官方API文档 + +### 核心API + +| API名称 | 官方文档地址 | +|--------|------------| +| 🔥 **商家转账到零钱** | https://pay.weixin.qq.com/doc/v3/merchant/4012716434 | +| 📋 **查询转账单(商户单号)** | https://pay.weixin.qq.com/doc/v3/merchant/4012716456 | +| 📋 **查询转账单(微信单号)** | https://pay.weixin.qq.com/doc/v3/merchant/4012716457 | +| 🔐 **签名生成与验证** | https://pay.weixin.qq.com/doc/v3/merchant/4013053249 | +| 🔒 **敏感信息加密** | https://pay.weixin.qq.com/doc/v3/merchant/4012070130 | +| 🔓 **回调通知解密** | https://pay.weixin.qq.com/doc/v3/merchant/4012071382 | +| 📝 **转账场景报备** | https://pay.weixin.qq.com/doc/v3/merchant/4012716437 | +| ❌ **错误码查询** | https://pay.weixin.qq.com/doc/v3/merchant/4012070193 | +| 📜 **平台证书管理** | https://pay.weixin.qq.com/doc/v3/merchant/4012154180 | + +### 开发指引 + +- **API V3 开发总览**:https://pay.weixin.qq.com/doc/v3/merchant/4012065168 +- **PHP SDK 使用**:https://pay.weixin.qq.com/doc/v3/merchant/4012076511 + +--- + +## 前置准备 + +### 1. 获取配置信息 + +登录微信商户平台:https://pay.weixin.qq.com + +| 配置项 | 说明 | 获取路径 | +|-------|------|---------| +| **商户号(mch_id)** | 微信支付商户号 | 账户中心 → 商户信息 | +| **APIv3密钥(api_v3_key)** | 32字节密钥,用于加密解密 | 账户中心 → API安全 → 设置APIv3密钥 | +| **商户私钥(apiclient_key.pem)** | 用于请求签名 | 账户中心 → API安全 → 申请证书 | +| **证书序列号(cert_serial_no)** | 商户证书标识 | 从证书文件提取 | +| **平台证书(wechat_pay_pub_key)** | 用于验证回调签名 | 下载或通过API获取 | +| **小程序AppId** | 小程序标识 | 小程序管理后台 | + +### 2. 提取证书序列号 + +**使用OpenSSL命令**: + +```bash +openssl x509 -in apiclient_cert.pem -noout -serial +``` + +输出: +``` +serial=4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5 +``` + +**使用PHP**: + +```php + +``` + +### 3. 配置IP白名单 + +路径:微信商户平台 → 账户中心 → API安全 → IP配置 + +添加服务器公网IP地址。 + +**获取服务器IP**: + +```bash +curl ifconfig.me +``` + +### 4. 配置转账场景 + +路径:微信商户平台 → 产品中心 → 商家转账到零钱 → 前往功能 + +可选场景: +- **1000**:现金营销 +- **1005**:营销活动 + +### 5. 环境要求 + +- PHP >= 7.0 +- OpenSSL 扩展(必须) +- cURL 扩展(必须) +- JSON 扩展(必须) +- TLS 1.2+ + +**检查环境**: + +```bash +php -v +php -m | grep openssl +php -m | grep curl +``` + +--- + +## API集成 + +### 1. 商家转账到零钱API + +#### 基本信息 + +- **接口地址**:`https://api.mch.weixin.qq.com/v3/transfer/batches` +- **请求方法**:POST +- **Content-Type**:application/json + +#### 请求头 + +``` +Authorization: WECHATPAY2-SHA256-RSA2048 mchid="商户号",nonce_str="随机字符串",signature="签名",timestamp="时间戳",serial_no="证书序列号" +Content-Type: application/json +Accept: application/json +User-Agent: YourApp/1.0 +``` + +#### 请求参数 + +```json +{ + "appid": "wx6489c26045912fe1", + "out_batch_no": "BATCH202601291234567890", + "batch_name": "提现", + "batch_remark": "用户提现", + "total_amount": 5000, + "total_num": 1, + "transfer_detail_list": [ + { + "out_detail_no": "TX202601291234567890", + "transfer_amount": 5000, + "transfer_remark": "提现", + "openid": "odq3g5IOG-Z1WLpbeG_amUme8EZk" + } + ], + "transfer_scene_id": "1005", + "transfer_scene_report_infos": [ + { + "info_type": "岗位类型", + "info_content": "兼职人员" + }, + { + "info_type": "报酬说明", + "info_content": "当日兼职费" + } + ] +} +``` + +**参数说明**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| appid | string | 是 | 小程序AppId | +| out_batch_no | string | 是 | 商户批次单号,商户下唯一 | +| batch_name | string | 是 | 批次名称 | +| batch_remark | string | 是 | 批次备注 | +| total_amount | integer | 是 | 转账总金额,单位:**分** | +| total_num | integer | 是 | 转账总笔数 | +| transfer_detail_list | array | 是 | 转账明细列表 | +| transfer_scene_id | string | 是 | 转账场景ID:1000或1005 | +| transfer_scene_report_infos | array | 否 | 场景报备信息 | + +**transfer_detail_list说明**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| out_detail_no | string | 是 | 商户明细单号 | +| transfer_amount | integer | 是 | 转账金额,单位:**分** | +| transfer_remark | string | 是 | 转账备注 | +| openid | string | 是 | 收款用户OpenId | + +**场景报备信息(场景ID=1005)**: + +```json +[ + { + "info_type": "岗位类型", + "info_content": "兼职人员" + }, + { + "info_type": "报酬说明", + "info_content": "当日兼职费" + } +] +``` + +**重要**: +- `info_type` 必须是固定值 +- 金额单位是**分**:`元 * 100` + +#### 响应数据 + +**成功响应**: + +```json +{ + "out_batch_no": "BATCH202601291234567890", + "batch_id": "1030000071100999991182020050700019480001", + "create_time": "2026-01-29T12:30:00+08:00", + "batch_status": "PROCESSING" +} +``` + +**字段说明**: + +| 字段 | 说明 | +|------|------| +| out_batch_no | 商户批次单号 | +| batch_id | 微信批次单号 | +| create_time | 批次创建时间 | +| batch_status | 批次状态:PROCESSING/SUCCESS/FAIL | + +**失败响应**: + +```json +{ + "code": "PARAM_ERROR", + "message": "参数错误" +} +``` + +### 2. 查询转账单API + +#### 按商户单号查询 + +**接口地址**: + +``` +GET https://api.mch.weixin.qq.com/v3/transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id} +``` + +**路径参数**: +- `batch_id`:商户批次单号(需URL编码) +- `detail_id`:商户明细单号(需URL编码) + +**示例**: + +``` +GET /v3/transfer/batches/batch-id/BATCH202601291234567890/details/detail-id/TX202601291234567890 +``` + +**响应示例**: + +```json +{ + "mchid": "1318592501", + "out_batch_no": "BATCH202601291234567890", + "batch_id": "1030000071100999991182020050700019480001", + "out_detail_no": "TX202601291234567890", + "detail_id": "1040000071100999991182020050700019500100", + "detail_status": "SUCCESS", + "transfer_amount": 5000, + "transfer_remark": "提现", + "openid": "odq3g5IOG-Z1WLpbeG_amUme8EZk", + "initiate_time": "2026-01-29T12:30:00+08:00", + "update_time": "2026-01-29T12:30:15+08:00" +} +``` + +**状态说明**: + +| detail_status | 说明 | +|--------------|------| +| PROCESSING | 转账中 | +| SUCCESS | 转账成功 | +| FAIL | 转账失败 | + +### 3. 转账结果通知(回调) + +#### 回调触发 + +当转账状态变更时,微信支付会主动向配置的 `notify_url` 发送POST请求。 + +#### 回调请求头 + +``` +Wechatpay-Signature: 签名值 +Wechatpay-Timestamp: 1769653396 +Wechatpay-Nonce: R0PDA5lOV3IMrBjrvbCH5U4L3Lb0gg8L +Wechatpay-Serial: 642B2B33557205BA79A1CFF08EA2A2478D67BD63 +Wechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048 +Content-Type: application/json +``` + +#### 回调请求体(加密) + +```json +{ + "id": "cb29e425-ca17-59fb-8045-8e5b58917154", + "create_time": "2026-01-29T10:23:11+08:00", + "resource_type": "encrypt-resource", + "event_type": "MCHTRANSFER.BILL.FINISHED", + "summary": "商家转账单据终态通知", + "resource": { + "original_type": "mch_payment", + "algorithm": "AEAD_AES_256_GCM", + "ciphertext": "加密的数据...", + "associated_data": "mch_payment", + "nonce": "随机字符串" + } +} +``` + +#### 解密后的数据 + +```json +{ + "mch_id": "1318592501", + "out_bill_no": "TX202601291234567890", + "transfer_bill_no": "1330000114850082601290057112302122", + "transfer_amount": 5000, + "state": "SUCCESS", + "openid": "odq3g5IOG-Z1WLpbeG_amUme8EZk", + "create_time": "2026-01-29T12:30:00+08:00", + "update_time": "2026-01-29T12:30:15+08:00" +} +``` + +**state状态说明**: + +| state | 说明 | +|-------|------| +| PROCESSING | 转账中 | +| SUCCESS | 转账成功 | +| FAIL | 转账失败 | +| WAIT_USER_CONFIRM | 待用户确认 | +| TRANSFERING | 正在转账 | + +#### 回调响应 + +处理完成后,返回给微信: + +```json +{ + "code": "SUCCESS" +} +``` + +--- + +## 签名算法 + +### 1. 签名生成(请求签名) + +#### 签名串格式 + +``` +请求方法\n +请求URL路径\n +请求时间戳\n +随机字符串\n +请求报文主体\n +``` + +**示例**: + +``` +POST +/v3/transfer/batches +1234567890 +RandomString123456 +{"appid":"wx6489c26045912fe1"} +``` + +**重要**:每部分末尾都有 `\n` 换行符。 + +#### 签名步骤 + +1. 构建签名串 +2. 使用商户私钥进行SHA256withRSA签名 +3. 对签名结果进行Base64编码 + +#### PHP实现 + +```php +function buildSignature($method, $url, $timestamp, $nonce, $body, $privateKeyPath) { + // 1. 构建签名串 + $signStr = $method . "\n" + . $url . "\n" + . $timestamp . "\n" + . $nonce . "\n" + . $body . "\n"; + + // 2. 加载私钥 + $privateKeyContent = file_get_contents($privateKeyPath); + $privateKeyResource = openssl_pkey_get_private($privateKeyContent); + + // 3. 使用私钥签名 + openssl_sign($signStr, $signature, $privateKeyResource, 'sha256WithRSAEncryption'); + + // 4. Base64编码 + return base64_encode($signature); +} +``` + +#### 构建Authorization头 + +```php +function buildAuthorization($mchId, $timestamp, $nonce, $signature, $serialNo) { + return sprintf( + 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"', + $mchId, + $nonce, + $signature, + $timestamp, + $serialNo + ); +} +``` + +### 2. 签名验证(回调验签) + +#### 验签串格式 + +``` +时间戳\n +随机字符串\n +请求报文主体\n +``` + +**示例**: + +``` +1769653396 +R0PDA5lOV3IMrBjrvbCH5U4L3Lb0gg8L +{"id":"cb29e425-ca17-59fb-8045-8e5b58917154",...} +``` + +#### PHP实现 + +```php +function verifySignature($timestamp, $nonce, $body, $signature, $publicKeyPath) { + // 1. 构建验签串 + $verifyStr = $timestamp . "\n" + . $nonce . "\n" + . $body . "\n"; + + // 2. Base64解码签名 + $signatureDecode = base64_decode($signature); + + // 3. 加载平台公钥 + $publicKeyContent = file_get_contents($publicKeyPath); + $publicKeyResource = openssl_pkey_get_public($publicKeyContent); + + // 4. 验证签名 + $result = openssl_verify( + $verifyStr, + $signatureDecode, + $publicKeyResource, + 'sha256WithRSAEncryption' + ); + + return $result === 1; // 1表示验证成功 +} +``` + +**重要**:验签使用的是**微信支付平台公钥**,不是商户私钥! + +--- + +## 加密解密 + +### 回调数据解密 + +#### 算法信息 + +- **算法**:AEAD_AES_256_GCM +- **密钥**:APIv3密钥(32字节) +- **密文格式**:实际密文 + 认证标签(16字节) + +#### 解密步骤 + +1. 提取加密数据(ciphertext、nonce、associated_data) +2. Base64解码密文 +3. 分离密文和认证标签(最后16字节) +4. 使用AES-256-GCM解密 +5. 解析JSON数据 + +#### PHP实现 + +```php +function decryptCallbackData($ciphertext, $nonce, $associatedData, $apiV3Key) { + // 1. 检查APIv3密钥长度(必须32字节) + if (strlen($apiV3Key) !== 32) { + throw new Exception('APIv3密钥长度必须为32字节'); + } + + // 2. Base64解码密文 + $ciphertextDecoded = base64_decode($ciphertext); + + // 3. 分离密文和认证标签 + $authTag = substr($ciphertextDecoded, -16); + $ctext = substr($ciphertextDecoded, 0, -16); + + // 4. 使用AES-256-GCM解密 + $decrypted = openssl_decrypt( + $ctext, // 密文 + 'aes-256-gcm', // 算法 + $apiV3Key, // 密钥 + OPENSSL_RAW_DATA, // 选项 + $nonce, // 随机串 + $authTag, // 认证标签 + $associatedData // 附加数据 + ); + + if ($decrypted === false) { + throw new Exception('解密失败'); + } + + // 5. 解析JSON + return json_decode($decrypted, true); +} +``` + +**使用示例**: + +```php +$resource = $callbackData['resource']; +$decrypted = decryptCallbackData( + $resource['ciphertext'], + $resource['nonce'], + $resource['associated_data'], + 'wx3e31b068be59ddc131b068be59ddc2' // APIv3密钥 +); +``` + +--- + +## 代码实现 + +### 完整的微信支付转账类 + +```php +mchId = $config['mch_id']; + $this->appId = $config['app_id']; + $this->apiV3Key = $config['api_v3_key']; + $this->certSerialNo = $config['cert_serial_no']; + + // 加载私钥 + $privateKeyContent = file_get_contents($config['private_key']); + $this->privateKey = openssl_pkey_get_private($privateKeyContent); + } + + /** + * 发起转账 + */ + public function createTransfer($params) + { + $url = '/v3/transfer/batches'; + $method = 'POST'; + + // 构建请求数据 + $data = [ + 'appid' => $this->appId, + 'out_batch_no' => 'BATCH' . date('YmdHis') . mt_rand(1000, 9999), + 'batch_name' => $params['batch_name'] ?? '提现', + 'batch_remark' => $params['batch_remark'] ?? '用户提现', + 'total_amount' => $params['transfer_amount'], + 'total_num' => 1, + 'transfer_detail_list' => [ + [ + 'out_detail_no' => $params['out_detail_no'], + 'transfer_amount' => $params['transfer_amount'], + 'transfer_remark' => $params['transfer_remark'], + 'openid' => $params['openid'], + ] + ], + 'transfer_scene_id' => $params['transfer_scene_id'] ?? '1005', + ]; + + // 添加场景报备信息 + if (!empty($params['transfer_scene_report_infos'])) { + $data['transfer_scene_report_infos'] = $params['transfer_scene_report_infos']; + } + + $body = json_encode($data, JSON_UNESCAPED_UNICODE); + + // 生成签名 + $timestamp = time(); + $nonce = $this->generateNonce(); + $signature = $this->buildSignature($method, $url, $timestamp, $nonce, $body); + + // 构建Authorization + $authorization = $this->buildAuthorization($timestamp, $nonce, $signature); + + // 发送请求 + return $this->request($method, $url, $body, $authorization); + } + + /** + * 查询转账单 + */ + public function queryTransfer($batchNo, $detailNo) + { + $url = "/v3/transfer/batches/batch-id/" . urlencode($batchNo) + . "/details/detail-id/" . urlencode($detailNo); + $method = 'GET'; + + $timestamp = time(); + $nonce = $this->generateNonce(); + $signature = $this->buildSignature($method, $url, $timestamp, $nonce, ''); + $authorization = $this->buildAuthorization($timestamp, $nonce, $signature); + + return $this->request($method, $url, '', $authorization); + } + + /** + * 验证回调签名 + */ + public function verifyCallback($headers, $body, $publicKey) + { + $timestamp = $headers['wechatpay-timestamp']; + $nonce = $headers['wechatpay-nonce']; + $signature = $headers['wechatpay-signature']; + + $verifyStr = $timestamp . "\n" . $nonce . "\n" . $body . "\n"; + $signatureDecode = base64_decode($signature); + + $publicKeyContent = file_get_contents($publicKey); + $publicKeyResource = openssl_pkey_get_public($publicKeyContent); + + $result = openssl_verify($verifyStr, $signatureDecode, $publicKeyResource, 'sha256WithRSAEncryption'); + + return $result === 1; + } + + /** + * 解密回调数据 + */ + public function decryptCallbackResource($resource) + { + $ciphertext = $resource['ciphertext']; + $nonce = $resource['nonce']; + $associatedData = $resource['associated_data']; + + if (strlen($this->apiV3Key) !== 32) { + throw new \Exception('APIv3密钥长度必须为32字节'); + } + + $ciphertextDecoded = base64_decode($ciphertext); + $authTag = substr($ciphertextDecoded, -16); + $ctext = substr($ciphertextDecoded, 0, -16); + + $decrypted = openssl_decrypt( + $ctext, + 'aes-256-gcm', + $this->apiV3Key, + OPENSSL_RAW_DATA, + $nonce, + $authTag, + $associatedData + ); + + if ($decrypted === false) { + throw new \Exception('解密失败'); + } + + return json_decode($decrypted, true); + } + + /** + * 生成签名 + */ + private function buildSignature($method, $url, $timestamp, $nonce, $body) + { + $signStr = $method . "\n" + . $url . "\n" + . $timestamp . "\n" + . $nonce . "\n" + . $body . "\n"; + + openssl_sign($signStr, $signature, $this->privateKey, 'sha256WithRSAEncryption'); + + return base64_encode($signature); + } + + /** + * 构建Authorization头 + */ + private function buildAuthorization($timestamp, $nonce, $signature) + { + return sprintf( + 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"', + $this->mchId, + $nonce, + $signature, + $timestamp, + $this->certSerialNo + ); + } + + /** + * 生成随机字符串 + */ + private function generateNonce($length = 32) + { + $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + $nonce = ''; + for ($i = 0; $i < $length; $i++) { + $nonce .= $chars[mt_rand(0, strlen($chars) - 1)]; + } + return $nonce; + } + + /** + * 发送HTTP请求 + */ + private function request($method, $url, $body, $authorization) + { + $fullUrl = 'https://api.mch.weixin.qq.com' . $url; + + $headers = [ + 'Authorization: ' . $authorization, + 'Content-Type: application/json', + 'Accept: application/json', + 'User-Agent: YourApp/1.0' + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $fullUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $result = json_decode($response, true); + + if ($httpCode >= 200 && $httpCode < 300) { + return ['success' => true, 'data' => $result]; + } else { + return [ + 'success' => false, + 'error_code' => $result['code'] ?? 'UNKNOWN', + 'error_msg' => $result['message'] ?? '未知错误' + ]; + } + } +} +``` + +### 使用示例 + +#### 1. 发起转账 + +```php +// 初始化配置 +$config = [ + 'mch_id' => '1318592501', + 'app_id' => 'wx6489c26045912fe1', + 'api_v3_key' => 'wx3e31b068be59ddc131b068be59ddc2', + 'private_key' => '/path/to/apiclient_key.pem', + 'cert_serial_no' => '4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5', +]; + +$wechatPay = new WechatPayTransfer($config); + +// 发起转账 +$result = $wechatPay->createTransfer([ + 'out_detail_no' => 'TX' . date('YmdHis') . mt_rand(1000, 9999), + 'transfer_amount' => 5000, // 50元 = 5000分 + 'transfer_remark' => '提现', + 'openid' => 'odq3g5IOG-Z1WLpbeG_amUme8EZk', + 'transfer_scene_id' => '1005', + 'transfer_scene_report_infos' => [ + ['info_type' => '岗位类型', 'info_content' => '兼职人员'], + ['info_type' => '报酬说明', 'info_content' => '当日兼职费'], + ], +]); + +if ($result['success']) { + echo "转账成功: " . json_encode($result['data']); +} else { + echo "转账失败: " . $result['error_msg']; +} +``` + +#### 2. 查询转账单 + +```php +$result = $wechatPay->queryTransfer('BATCH202601291234567890', 'TX202601291234567890'); + +if ($result['success']) { + echo "状态: " . $result['data']['detail_status']; +} else { + echo "查询失败: " . $result['error_msg']; +} +``` + +#### 3. 处理回调 + +```php +// 接收回调 +$headers = [ + 'wechatpay-signature' => $_SERVER['HTTP_WECHATPAY_SIGNATURE'], + 'wechatpay-timestamp' => $_SERVER['HTTP_WECHATPAY_TIMESTAMP'], + 'wechatpay-nonce' => $_SERVER['HTTP_WECHATPAY_NONCE'], + 'wechatpay-serial' => $_SERVER['HTTP_WECHATPAY_SERIAL'], +]; + +$body = file_get_contents('php://input'); +$callbackData = json_decode($body, true); + +// 验证签名 +$verified = $wechatPay->verifyCallback($headers, $body, '/path/to/wechat_pay_pub_key.pem'); + +if ($verified) { + // 解密数据 + $decrypted = $wechatPay->decryptCallbackResource($callbackData['resource']); + + // 处理转账结果 + if ($decrypted['state'] === 'SUCCESS') { + echo "转账成功: " . $decrypted['out_bill_no']; + } + + // 返回成功 + echo json_encode(['code' => 'SUCCESS']); +} else { + echo json_encode(['code' => 'FAIL', 'message' => '签名验证失败']); +} +``` + +--- + +## 测试验证 + +### 1. 签名生成测试 + +```php +$method = 'POST'; +$url = '/v3/transfer/batches'; +$timestamp = time(); +$nonce = 'RandomString123456'; +$body = '{"appid":"wx6489c26045912fe1"}'; + +$signature = buildSignature($method, $url, $timestamp, $nonce, $body, 'apiclient_key.pem'); + +echo "签名: " . $signature . "\n"; +``` + +### 2. 小额转账测试 + +```php +// 测试金额:0.01元 = 1分 +$result = $wechatPay->createTransfer([ + 'out_detail_no' => 'TEST' . time(), + 'transfer_amount' => 1, // 1分 + 'transfer_remark' => '测试', + 'openid' => 'test_openid', + 'transfer_scene_id' => '1005', + 'transfer_scene_report_infos' => [ + ['info_type' => '岗位类型', 'info_content' => '测试'], + ['info_type' => '报酬说明', 'info_content' => '测试'], + ], +]); +``` + +### 3. 解密测试 + +```php +$resource = [ + 'ciphertext' => 'xxx', + 'nonce' => 'xxx', + 'associated_data' => 'mch_payment', +]; + +try { + $decrypted = decryptCallbackData( + $resource['ciphertext'], + $resource['nonce'], + $resource['associated_data'], + 'wx3e31b068be59ddc131b068be59ddc2' + ); + print_r($decrypted); +} catch (Exception $e) { + echo "解密失败: " . $e->getMessage(); +} +``` + +### 4. 常见问题 + +| 问题 | 原因 | 解决方法 | +|------|------|---------| +| 签名验证失败 | 证书序列号错误 | 重新提取证书序列号 | +| IP白名单错误 | 服务器IP未配置 | 添加到微信商户平台 | +| 解密失败 | APIv3密钥错误 | 检查密钥长度(32字节) | +| 场景报备错误 | info_type不正确 | 使用固定值 | +| 余额不足 | 商户号余额不足 | 充值商户号 | + +--- + +## 附录 + +### A. 错误码对照表 + +https://pay.weixin.qq.com/doc/v3/merchant/4012070193 + +| 错误码 | 说明 | 处理建议 | +|-------|------|---------| +| PARAM_ERROR | 参数错误 | 检查请求参数格式 | +| NOTENOUGH | 商户余额不足 | 充值商户号 | +| INVALID_REQUEST | 不符合业务规则 | 检查业务逻辑 | +| SYSTEM_ERROR | 系统错误 | 稍后重试 | +| FREQUENCY_LIMITED | 频率限制 | 降低请求频率 | +| APPID_MCHID_NOT_MATCH | appid和mch_id不匹配 | 检查配置 | + +### B. 转账状态说明 + +| 状态 | 说明 | 处理方式 | +|------|------|---------| +| PROCESSING | 转账中 | 等待回调或主动查询 | +| SUCCESS | 转账成功 | 完成流程 | +| FAIL | 转账失败 | 检查失败原因 | +| WAIT_USER_CONFIRM | 待用户确认 | 等待用户操作 | +| TRANSFERING | 正在转账 | 等待处理完成 | + +### C. 开发工具 + +- **Postman**:API测试工具 +- **OpenSSL**:证书和密钥管理 +- **微信支付调试工具**:https://pay.weixin.qq.com/ + +--- + +**文档版本**:v3.0(纯微信支付API版) +**更新时间**:2026-01-29 +**适用场景**:微信支付商家转账到零钱功能集成 + +--- + +## 总结 + +本文档提供了微信支付转账功能的完整集成方案: + +✅ **3个核心API** +- 发起转账:`POST /v3/transfer/batches` +- 查询转账:`GET /v3/transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}` +- 接收回调:异步通知 + +✅ **3个核心算法** +- 签名生成:SHA256withRSA + Base64 +- 签名验证:使用平台公钥 +- 数据解密:AEAD_AES_256_GCM + +✅ **完整代码实现** +- WechatPayTransfer类(可直接使用) +- 包含发起转账、查询、验签、解密全部功能 + +根据本文档可以快速集成微信支付转账功能。 diff --git a/开发文档/服务器管理/SKILL.md b/开发文档/服务器管理/SKILL.md new file mode 100644 index 0000000..8bce80c --- /dev/null +++ b/开发文档/服务器管理/SKILL.md @@ -0,0 +1,314 @@ +--- +name: 服务器管理 +description: 宝塔服务器统一管理与自动化部署。触发词:服务器、宝塔、部署、上线、发布、Node项目、SSL证书、HTTPS、DNS解析、域名配置、端口、PM2、Nginx、MySQL数据库、服务器状态。涵盖多服务器资产管理、Node.js项目一键部署、SSL证书管理、DNS配置、系统诊断等运维能力。 +--- + +# 服务器管理 + +让 AI 写完代码后,无需人工介入,自动把项目「变成一个在线网站」。 + +--- + +## 快速入口(复制即用) + +### 服务器资产 + +| 服务器 | IP | 配置 | 用途 | 宝塔面板 | +|--------|-----|------|------|----------| +| **小型宝塔** | 42.194.232.22 | 2核4G 5M | 主力部署(Node项目) | https://42.194.232.22:9988/ckbpanel | +| **存客宝** | 42.194.245.239 | 2核16G 50M | 私域银行业务 | https://42.194.245.239:9988 | +| **kr宝塔** | 43.139.27.93 | 2核4G 5M | 辅助服务器 | https://43.139.27.93:9988 | + +### 凭证速查 + +```bash +# SSH连接(小型宝塔为例) +ssh root@42.194.232.22 +密码: Zhiqun1984 + +# 宝塔面板登录(小型宝塔) +地址: https://42.194.232.22:9988/ckbpanel +账号: ckb +密码: zhiqun1984 + +# 宝塔API密钥 +小型宝塔: hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd +存客宝: TNKjqDv5N1QLOU20gcmGVgr82Z4mXzRi +kr宝塔: qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT +``` + +--- + +## 一键操作 + +### 1. 检查服务器状态 + +```bash +# 运行快速检查脚本 +python3 /Users/karuo/Documents/个人/卡若AI/01_系统管理/服务器管理/scripts/快速检查服务器.py +``` + +### 2. 部署 Node 项目(标准流程) + +```bash +# 1. 压缩项目(排除无用目录) +cd /项目路径 +tar --exclude='node_modules' --exclude='.next' --exclude='.git' \ + -czf /tmp/项目名_update.tar.gz . + +# 2. 上传到服务器 +sshpass -p 'Zhiqun1984' scp /tmp/项目名_update.tar.gz root@42.194.232.22:/tmp/ + +# 3. SSH部署 +ssh root@42.194.232.22 +cd /www/wwwroot/项目名 +rm -rf app components lib public styles *.json *.js *.ts *.mjs *.md .next +tar -xzf /tmp/项目名_update.tar.gz +pnpm install +pnpm run build +rm /tmp/项目名_update.tar.gz + +# 4. 宝塔面板重启项目 +# 【网站】→【Node项目】→ 找到项目 → 点击【重启】 +``` + +### 3. SSL证书检查/修复 + +```bash +# 检查所有服务器SSL证书状态 +python3 /Users/karuo/Documents/个人/卡若AI/01_系统管理/服务器管理/scripts/ssl证书检查.py + +# 自动修复过期证书 +python3 /Users/karuo/Documents/个人/卡若AI/01_系统管理/服务器管理/scripts/ssl证书检查.py --fix +``` + +### 4. 常用诊断命令 + +```bash +# 检查端口占用 +ssh root@42.194.232.22 "ss -tlnp | grep :3006" + +# 检查PM2进程 +ssh root@42.194.232.22 "/www/server/nodejs/v22.14.0/bin/pm2 list" + +# 测试HTTP响应 +ssh root@42.194.232.22 "curl -I http://localhost:3006" + +# 检查Nginx配置 +ssh root@42.194.232.22 "nginx -t" + +# 重载Nginx +ssh root@42.194.232.22 "nginx -s reload" + +# DNS解析检查 +dig soul.quwanzhi.com +short @8.8.8.8 +``` + +--- + +## 端口配置表(小型宝塔 42.194.232.22) + +| 端口 | 项目名 | 类型 | 域名 | 状态 | +|------|--------|------|------|------| +| 3000 | cunkebao | Next.js | mckb.quwanzhi.com | ✅ | +| 3001 | ai_hair | NestJS | ai-hair.quwanzhi.com | ✅ | +| 3002 | kr_wb | Next.js | kr_wb.quwanzhi.com | ✅ | +| 3003 | hx | Vue | krjzk.quwanzhi.com | ⚠️ | +| 3004 | dlmdashboard | Next.js | dlm.quwanzhi.com | ✅ | +| 3005 | document | Next.js | docc.quwanzhi.com | ✅ | +| 3006 | soul | Next.js | soul.quwanzhi.com | ✅ | +| 3015 | 神射手 | Next.js | kr-users.quwanzhi.com | ⚠️ | +| 3018 | zhaoping | Next.js | zp.quwanzhi.com | ✅ | +| 3021 | is_phone | Next.js | is-phone.quwanzhi.com | ✅ | +| 3031 | word | Next.js | word.quwanzhi.com | ✅ | +| 3036 | ymao | Next.js | ymao.quwanzhi.com | ✅ | +| 3043 | tongzhi | Next.js | touzhi.lkdie.com | ✅ | +| 3045 | 玩值大屏 | Next.js | wz-screen.quwanzhi.com | ✅ | +| 3050 | zhiji | Next.js | zhiji.quwanzhi.com | ✅ | +| 3051 | zhiji1 | Next.js | zhiji1.quwanzhi.com | ✅ | +| 3055 | wzdj | Next.js | wzdj.quwanzhi.com | ✅ | +| 3305 | AITOUFA | Next.js | ai-tf.quwanzhi.com | ✅ | +| 9528 | mbti | Vue | mbtiadmin.quwanzhi.com | ✅ | + +### 端口分配原则 + +- **3000-3099**: Next.js / React 项目 +- **3100-3199**: Vue 项目 +- **3200-3299**: NestJS / Express 后端 +- **3300-3399**: AI相关项目 +- **9000-9999**: 管理面板 / 特殊用途 + +--- + +## 核心工作流程 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Node项目一键部署流程 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ START │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 1. 压缩本地代码 │ 排除 node_modules, .next, .git │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 2. 上传到服务器 │ scp 到 /tmp/ │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 3. 清理旧文件 │ 保留 .env 等配置文件 │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 4. 解压新代码 │ tar -xzf │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 5. 安装依赖 │ pnpm install │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 6. 构建项目 │ pnpm run build │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 7. 宝塔面板重启 │ Node项目 → 重启 │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 8. 验证访问 │ curl https://域名 │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ SUCCESS │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 操作优先级矩阵 + +| 操作类型 | 优先方式 | 备选方式 | 说明 | +|---------|---------|---------|------| +| 查询信息 | ✅ 宝塔API | SSH | API稳定 | +| 文件操作 | ✅ 宝塔API | SSH | API支持 | +| 配置Nginx | ✅ 宝塔API | SSH | API可读写 | +| 重载服务 | ⚠️ SSH | - | API无接口 | +| 上传代码 | ⚠️ SSH/scp | - | 大文件 | +| 添加项目 | ❌ 宝塔界面 | - | API不稳定 | + +--- + +## 常见问题速查 + +### Q1: 外网无法访问(ERR_EMPTY_RESPONSE) + +**原因**: 腾讯云安全组只开放443端口 + +**解决**: +1. 必须配置SSL证书 +2. Nginx配置添加443监听 + +### Q2: Node项目启动失败(Could not find production build) + +**原因**: 使用 `npm run start` 但未执行 `npm run build` + +**解决**: 先 `pnpm run build` 再重启 + +### Q3: 端口冲突(EADDRINUSE) + +**解决**: +```bash +# 检查端口占用 +ss -tlnp | grep :端口号 + +# 修改package.json中的端口 +"start": "next start -p 新端口" +``` + +### Q4: DNS被代理劫持 + +**现象**: 本地DNS解析到198.18.x.x + +**解决**: +- 关闭代理软件 +- 或用手机4G网络测试 + +### Q5: 宝塔与PM2冲突 + +**原因**: 同时使用root用户PM2和宝塔PM2 + +**解决**: +- 停止所有独立PM2: `pm2 kill` +- 只使用宝塔界面管理 + +--- + +## 安全约束 + +### 绝对禁止 + +- ❌ 输出完整密码/密钥到聊天 +- ❌ 执行危险命令(rm -rf /, reboot等) +- ❌ 跳过验证步骤 +- ❌ 使用独立PM2(避免与宝塔冲突) + +### 必须遵守 + +- ✅ 操作前检查服务器状态 +- ✅ 操作后验证结果 +- ✅ 生成操作报告 + +--- + +## 相关脚本 + +| 脚本 | 功能 | 位置 | +|------|------|------| +| `快速检查服务器.py` | 一键检查所有服务器状态 | `./scripts/` | +| `一键部署.py` | 根据配置文件部署项目 | `./scripts/` | +| `ssl证书检查.py` | 检查/修复SSL证书 | `./scripts/` | + +--- + +## 相关文档 + +| 文档 | 内容 | 位置 | +|------|------|------| +| `宝塔API接口文档.md` | 宝塔API完整接口说明 | `./references/` | +| `端口配置表.md` | 完整端口分配表 | `./references/` | +| `常见问题手册.md` | 问题解决方案大全 | `./references/` | +| `部署配置模板.md` | JSON配置文件模板 | `./references/` | +| `系统架构说明.md` | 完整架构图和流程图 | `./references/` | + +--- + +## 历史对话整理 + +### kr_wb白板项目部署(2026-01-23) + +- 项目类型: Next.js +- 部署位置: /www/wwwroot/kr_wb +- 域名: kr_wb.quwanzhi.com +- 端口: 3002 +- 遇到问题: AI功能401错误(API密钥未配置) +- 解决方案: 修改 lib/ai-client.ts,改用 SiliconFlow 作为默认服务 + +### soul项目部署(2026-01-23) + +- 项目类型: Next.js +- 部署位置: /www/wwwroot/soul +- 域名: soul.quwanzhi.com +- 端口: 3006 +- 部署流程: 压缩→上传→解压→安装依赖→构建→PM2启动→配置Nginx→配置SSL diff --git a/开发文档/服务器管理/references/宝塔api接口文档.md b/开发文档/服务器管理/references/宝塔api接口文档.md new file mode 100644 index 0000000..ea972d1 --- /dev/null +++ b/开发文档/服务器管理/references/宝塔api接口文档.md @@ -0,0 +1,142 @@ +# 宝塔面板 API 接口文档 + +## 1. 鉴权机制 + +所有 API 请求均需包含鉴权参数,使用 POST 方式提交。 + +### 签名算法 + +```python +import time +import hashlib + +def get_sign(api_key): + now_time = int(time.time()) + # md5(timestamp + md5(api_key)) + sign_str = str(now_time) + hashlib.md5(api_key.encode('utf-8')).hexdigest() + request_token = hashlib.md5(sign_str.encode('utf-8')).hexdigest() + return now_time, request_token +``` + +### 基础参数 + +每次 POST 请求必须包含: +- `request_time`: 当前时间戳 (10位) +- `request_token`: 计算生成的签名 + +--- + +## 2. 系统管理接口 + +### 获取系统基础统计 +- **URL**: `/system?action=GetSystemTotal` +- **功能**: 获取 CPU、内存、系统版本等信息 + +### 获取磁盘信息 +- **URL**: `/system?action=GetDiskInfo` +- **功能**: 获取各分区使用情况 + +### 获取网络状态 +- **URL**: `/system?action=GetNetWork` +- **功能**: 获取实时网络流量 + +--- + +## 3. 网站管理接口 + +### 获取网站列表 +- **URL**: `/data?action=getData&table=sites` +- **参数**: + - `limit`: 每页条数 (默认15) + - `p`: 页码 (默认1) + - `search`: 搜索关键词 (可选) + +### 添加静态/PHP网站 +- **URL**: `/site?action=AddSite` +- **参数**: + - `webname`: 域名 (json字符串) + - `path`: 根目录路径 + - `version`: PHP版本 (`00`=纯静态, `74`=PHP 7.4) + - `port`: 端口 (默认 `80`) + +### 删除网站 +- **URL**: `/site?action=DeleteSite` +- **参数**: + - `id`: 网站ID + - `webname`: 网站域名 + +--- + +## 4. 文件管理接口 + +### 读取文件内容 +- **URL**: `/files?action=GetFileBody` +- **参数**: `path`: 文件绝对路径 + +### 保存文件内容 +- **URL**: `/files?action=SaveFileBody` +- **参数**: + - `path`: 文件绝对路径 + - `data`: 文件内容 + - `encoding`: 编码 (默认 `utf-8`) + +### 创建目录 +- **URL**: `/files?action=CreateDir` +- **参数**: `path`: 目录绝对路径 + +### 删除文件/目录 +- **URL**: `/files?action=DeleteFile` +- **参数**: `path`: 绝对路径 + +--- + +## 5. Node.js 项目管理 (PM2) + +> 注:部分接口可能随宝塔版本更新而变化 + +### 获取 Node 项目列表 +- **URL**: `/project/nodejs/get_project_list` + +### 添加 Node 项目 +- **URL**: `/project/nodejs/add_project` +- **参数**: + - `name`: 项目名称 + - `path`: 项目根目录 + - `run_cmd`: 启动命令 + - `port`: 项目端口 + +### 启动/停止/重启项目 +- **URL**: `/project/nodejs/start_project` (或 `stop_project`, `restart_project`) +- **参数**: `project_name`: 项目名称 + +--- + +## 6. SSL证书管理 + +### 获取证书列表 +- **URL**: `/ssl?action=GetCertList` + +### 获取网站SSL配置 +- **URL**: `/site?action=GetSSL` +- **参数**: `siteName`: 网站名称 + +--- + +## 7. 计划任务 + +### 获取计划任务列表 +- **URL**: `/crontab?action=GetCrontab` + +### 执行计划任务 +- **URL**: `/crontab?action=StartTask` +- **参数**: `id`: 任务ID + +--- + +## 8. 服务管理 + +### 重载/重启服务 +- **URL**: `/system?action=ServiceAdmin` +- **参数**: + - `name`: 服务名称 (nginx, mysql等) + - `type`: 操作类型 (reload, restart, stop, start) diff --git a/开发文档/服务器管理/references/常见问题手册.md b/开发文档/服务器管理/references/常见问题手册.md new file mode 100644 index 0000000..97f0ed4 --- /dev/null +++ b/开发文档/服务器管理/references/常见问题手册.md @@ -0,0 +1,184 @@ +# 常见问题手册 + +## 1. 宝塔面板问题 + +### 问题1: JSON解析错误 + +``` +json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes +``` + +**原因**: 数据库中`project_config`字段不是有效JSON格式 + +**错误格式**: `{project_name: xxx, port: 3000}` +**正确格式**: `{"project_name": "xxx", "port": 3000}` + +**修复方法**: +```python +import sqlite3, json +conn = sqlite3.connect('/www/server/panel/data/db/site.db') +c = conn.cursor() +c.execute("SELECT id, name, project_config FROM sites WHERE project_type='Node'") +for row in c.fetchall(): + try: + json.loads(row[2]) + except: + # 手动构建正确JSON并更新 + pass +``` + +--- + +## 2. 网络访问问题 + +### 问题2: 外网无法访问(ERR_EMPTY_RESPONSE) + +**原因**: 腾讯云安全组只开放443端口,未开放80端口 + +**解决方案**: +1. 配置SSL证书(使用通配符证书) +2. Nginx配置添加443监听: +```nginx +listen 443 ssl; +listen [::]:443 ssl; +ssl_certificate /www/server/panel/vhost/cert/www.quwanzhi.com/fullchain.pem; +ssl_certificate_key /www/server/panel/vhost/cert/www.quwanzhi.com/privkey.pem; +``` + +### 问题3: DNS被代理劫持 + +**原因**: 本地使用VPN/代理(Clash, V2Ray等) + +**现象**: +- 本地DNS解析到198.18.x.x +- 服务器内部测试正常 +- 外部访问失败 + +**解决方案**: +- 关闭代理软件 +- 或修改hosts文件 +- 用手机4G网络测试 + +--- + +## 3. Node项目问题 + +### 问题4: 启动失败(Could not find production build) + +**原因**: 使用`npm run start`但未执行`npm run build` + +**解决方案**: +- 方案A: 先`npm run build`再`npm run start` +- 方案B: 改用`npm run dev`模式 + +### 问题5: 端口冲突(EADDRINUSE) + +**原因**: 多个项目配置相同端口,或package.json中端口写死 + +**解决方案**: +1. 检查端口占用: `ss -tlnp | grep :端口号` +2. 修改package.json: `"start": "next start -p 新端口"` +3. 更新数据库中的端口配置 + +### 问题6: Vue项目 Invalid Host header + +**原因**: Nginx代理时Host头不匹配 + +**解决方案**: 修改`vue.config.js`: +```javascript +devServer: { + disableHostCheck: true +} +``` + +--- + +## 4. 腾讯云特性 + +### 问题7: 服务器无法访问自己公网IP + +**原因**: 轻量服务器网络配置限制 + +**现象**: +- 服务器无法通过公网IP访问自己 +- 外部访问返回Empty reply + +**解决方案**: +- 这是正常现象 +- 服务器内部测试用127.0.0.1 +- 外部访问问题需其他网络测试 + +### 问题8: 端口只监听IPv6 + +**问题**: 端口显示`tcp6 :::3006`而不是`tcp 0.0.0.0:3006` + +**说明**: +- Node.js监听`::`会同时响应IPv4 +- 这是正常现象,无需特别处理 + +--- + +## 5. Nginx问题 + +### 问题9: 重复server_name警告 + +**原因**: 同一域名在多个配置文件中定义 + +**解决方案**: +- 删除或备份重复配置文件 +- Node项目使用`node_项目名.conf` +- 不要同时创建`域名.conf` + +### 问题10: HTTPS强制重定向导致无法访问 + +**问题**: Nginx配置了`return 301 https://`但SSL证书未配置 + +**解决方案**: +- 删除HTTPS重定向 +- 或正确配置SSL证书 +- 同时支持HTTP和HTTPS + +--- + +## 6. 宝塔与PM2冲突 + +### 问题11: 权限错误 EACCES + +**原因**: 同时使用root用户PM2和宝塔PM2(www用户) + +**现象**: +- 权限错误:`EACCES: permission denied` +- 宝塔面板显示未启动但实际在运行 +- 状态不同步 + +**解决方案**: +- 停止所有独立PM2:`pm2 kill` +- 只使用宝塔界面管理 +- 所有操作通过宝塔面板 + +--- + +## 7. 诊断命令速查 + +```bash +# 检查端口占用 +ss -tlnp | grep :3006 +netstat -tlnp | grep :3006 + +# 测试HTTP响应 +curl -I http://localhost:3006 +curl -I -H 'Host: soul.quwanzhi.com' http://127.0.0.1 + +# 检查Nginx配置 +nginx -t + +# 重载Nginx +nginx -s reload + +# 检查PM2进程 +/www/server/nodejs/v22.14.0/bin/pm2 list + +# 检查DNS解析 +dig soul.quwanzhi.com +dig +short soul.quwanzhi.com @8.8.8.8 +``` diff --git a/开发文档/服务器管理/references/端口配置表.md b/开发文档/服务器管理/references/端口配置表.md new file mode 100644 index 0000000..fb71c3b --- /dev/null +++ b/开发文档/服务器管理/references/端口配置表.md @@ -0,0 +1,64 @@ +# 端口配置表 + +> 最后更新: 2026-01-18 + +## 小型宝塔 (42.194.232.22) + +### 服务器配置 +- **配置**: 2核4G,内存3.6G +- **带宽**: 5M +- **安全组**: 443端口开放,80端口受限 +- **注意**: 所有Node项目必须配置HTTPS + +### 端口分配表 + +| 端口 | 项目名 | 类型 | 域名 | 启动命令 | 状态 | +|------|--------|------|------|----------|------| +| 3000 | cunkebao | Next.js | mckb.quwanzhi.com | dev | ✅ | +| 3001 | ai_hair | NestJS | ai-hair.quwanzhi.com | start | ✅ | +| 3002 | kr_wb | Next.js | kr_wb.quwanzhi.com | start | ✅ | +| 3003 | hx | Vue | krjzk.quwanzhi.com | build | ⚠️ | +| 3004 | dlmdashboard | Next.js | dlm.quwanzhi.com | dev | ✅ | +| 3005 | document | Next.js | docc.quwanzhi.com | dev | ✅ | +| 3006 | soul | Next.js | soul.quwanzhi.com | start | ✅ | +| 3015 | 神射手 | Next.js | kr-users.quwanzhi.com | build | ⚠️ | +| 3018 | zhaoping | Next.js | zp.quwanzhi.com | start | ✅ | +| 3021 | is_phone | Next.js | is-phone.quwanzhi.com | dev | ✅ | +| 3031 | word | Next.js | word.quwanzhi.com | start | ✅ | +| 3036 | ymao | Next.js | ymao.quwanzhi.com | dev | ✅ | +| 3043 | tongzhi | Next.js | touzhi.lkdie.com | start | ✅ | +| 3045 | 玩值大屏 | Next.js | wz-screen.quwanzhi.com | start | ✅ | +| 3050 | zhiji | Next.js | zhiji.quwanzhi.com | start | ✅ | +| 3051 | zhiji1 | Next.js | zhiji1.quwanzhi.com | start | ✅ | +| 3055 | wzdj | Next.js | wzdj.quwanzhi.com | start | ✅ | +| 3305 | AITOUFA | Next.js | ai-tf.quwanzhi.com | start | ✅ | +| 9528 | mbti | Vue | mbtiadmin.quwanzhi.com | dev | ✅ | + +### 域名Nginx配置对照表 + +| 域名 | 反向代理端口 | SSL证书 | +|------|-------------|---------| +| soul.quwanzhi.com | 127.0.0.1:3006 | 通配符证书 | +| zhiji.quwanzhi.com | 127.0.0.1:3050 | 通配符证书 | +| touzhi.lkdie.com | 127.0.0.1:3043 | 通配符证书 | +| mbtiadmin.quwanzhi.com | 127.0.0.1:9528 | 通配符证书 | + +--- + +## 端口分配原则 + +1. **3000-3099**: Next.js / React 项目 +2. **3100-3199**: Vue 项目 +3. **3200-3299**: NestJS / Express 后端 +4. **3300-3399**: AI相关项目 +5. **9000-9999**: 管理面板 / 特殊用途 + +--- + +## 新增项目端口申请 + +新增项目前必须: +1. 在端口分配表中登记 +2. 确认端口未被占用 +3. 配置SSL证书 +4. 确保package.json中端口正确 diff --git a/开发文档/服务器管理/references/系统架构说明.md b/开发文档/服务器管理/references/系统架构说明.md new file mode 100644 index 0000000..8576f48 --- /dev/null +++ b/开发文档/服务器管理/references/系统架构说明.md @@ -0,0 +1,310 @@ +# 卡若服务器管理系统 - 完整架构说明 + +## 一、系统定位 + +**一句话定位**:让AI写完代码之后,不需要任何人介入,系统自动把项目"变成一个在线网站"。 + +**核心理念**: +- 代码开发 = 由 TRAE / Cursor / AI 工具完成 +- 本系统只负责:组合 + 自动化部署 + 上线 + 同步 + +--- + +## 二、整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 卡若服务器管理系统 v2.0 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 用户交互层 │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Cursor AI │ │ 终端命令 │ │ Python脚本 │ │ 配置文件 │ │ │ +│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ +│ └─────────┼────────────────┼────────────────┼────────────────┼─────────┘ │ +│ │ │ │ │ │ +│ └────────────────┴────────────────┴────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ Skill 触发层 │ │ +│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 触发关键词: 服务器 / 宝塔 / 部署 / Node / SSL / DNS / MySQL │ │ │ +│ │ │ 触发场景: 项目上线 / 状态检查 / 问题诊断 / 证书管理 │ │ │ +│ │ └─────────────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 核心组件层 │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ 宝塔API │ │ SSH客户端 │ │ 环境修复 │ │ 服务器档案 │ │ │ +│ │ │ (优先使用) │ │ (备选方案) │ │ (白名单等) │ │ (配置管理) │ │ │ +│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ +│ │ │ │ │ │ │ │ +│ │ └────────────────┴────────────────┴────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 功能插件层 │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ 宝塔部署器 │ │ SSL证书管理 │ │阿里云DNS管理│ │ MySQL管理 │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Nginx管理 │ │ 状态报告 │ │ 系统诊断 │ │ 项目修复 │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 服务器资源层 │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ 小型宝塔 │ │ 存客宝 │ │ kr宝塔 │ │ │ +│ │ │ 42.194.232.22 │ │ 42.194.245.239 │ │ 43.139.27.93 │ │ │ +│ │ │ 2核4G 5M │ │ 2核16G 50M │ │ 2核4G 5M │ │ │ +│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────────────────┐ │ │ +│ │ │ 外部服务 │ │ │ +│ │ │ 阿里云DNS API | 腾讯云MySQL | GitHub/Coding 代码仓库 │ │ │ +│ │ └──────────────────────────────────────────────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、核心流程图 + +### 3.1 标准部署流程 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Node项目一键部署流程 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ START │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 1. 读取部署配置 │ 配置文件: 部署配置/*.json │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 2. 修复API白名单 │ 环境修复.py → 确保本机IP在宝塔白名单 │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 3. 查询服务器状态│────▶│ 宝塔API: 系统统计 │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 4. 探测空闲端口 │────▶│ SSH: netstat检查 │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 5. 创建项目目录 │────▶│ 宝塔API: 创建目录 │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 6. 上传代码压缩包│────▶│ SSH: scp上传 │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 7. 解压代码 │────▶│ SSH: unzip │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 8. 安装依赖 │────▶│ SSH: pnpm install│ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 9. 构建项目 │────▶│ SSH: pnpm build │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │10. 修正启动端口 │────▶│ SSH: 修改package │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │11. 宝塔添加项目 │────▶│ 宝塔API: create_project │ │ +│ └────────┬─────────┘ │ (失败则需手动在面板添加) │ │ +│ │ └──────────────────────────┘ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │12. 绑定域名 │────▶│ SSH: 调用宝塔内部│ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │13. 配置HTTPS │────▶│ SSH: 修改Nginx │ │ +│ └────────┬─────────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │14. 健康检查 │ │ +│ │ - 端口监听? │ │ +│ │ - HTTP 200? │ │ +│ │ - HTTPS 200? │ │ +│ └────────┬─────────┘ │ +│ │ │ +│ ▼ │ +│ SUCCESS │ +│ │ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ 输出部署报告 │ │ +│ └──────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 问题诊断流程 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 问题诊断流程 │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 用户报告问题 │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ 否 ┌─────────────────────────────────────────┐ │ +│ │ 能连接服务器? │─────────▶│ 检查项: │ │ +│ └───────┬─────────┘ │ - SSH连接是否正常 │ │ +│ │是 │ - 宝塔面板是否能访问 │ │ +│ │ │ - API白名单是否包含本机IP │ │ +│ ▼ └─────────────────────────────────────────┘ │ +│ ┌─────────────────┐ 否 ┌─────────────────────────────────────────┐ │ +│ │ 项目端口监听? │─────────▶│ 检查项: │ │ +│ └───────┬─────────┘ │ - PM2进程是否运行 │ │ +│ │是 │ - 宝塔面板项目状态 │ │ +│ │ │ - npm start是否报错 │ │ +│ ▼ │ - 端口是否被其他进程占用 │ │ +│ ┌─────────────────┐ 否 └─────────────────────────────────────────┘ │ +│ │ Nginx配置正确? │─────────▶│ 检查项: │ │ +│ └───────┬─────────┘ │ - nginx -t 语法检查 │ │ +│ │是 │ - server_name是否正确 │ │ +│ │ │ - proxy_pass端口是否正确 │ │ +│ ▼ │ - 是否有重复配置文件 │ │ +│ ┌─────────────────┐ 否 └─────────────────────────────────────────┘ │ +│ │ SSL证书有效? │─────────▶│ 检查项: │ │ +│ └───────┬─────────┘ │ - 证书是否过期 │ │ +│ │是 │ - 证书路径是否正确 │ │ +│ │ │ - 是否为通配符证书 │ │ +│ ▼ └─────────────────────────────────────────┘ │ +│ ┌─────────────────┐ 否 ┌─────────────────────────────────────────┐ │ +│ │ DNS解析正确? │─────────▶│ 检查项: │ │ +│ └───────┬─────────┘ │ - dig查询A记录 │ │ +│ │是 │ - 阿里云DNS控制台 │ │ +│ │ │ - 本地DNS缓存 │ │ +│ ▼ │ - 是否被代理劫持 │ │ +│ ┌─────────────────┐ └─────────────────────────────────────────┘ │ +│ │ 服务器内部正常 │ │ +│ │ 检查客户端网络 │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 四、文件结构说明 + +``` +服务器管理/ +├── .codex/skills/karuo-server-manager/ # Skill 目录 +│ ├── SKILL.md # 主文件(触发条件+核心流程) +│ ├── references/ # 参考文档 +│ │ ├── 宝塔API接口文档.md +│ │ ├── 端口配置表.md +│ │ ├── 常见问题手册.md +│ │ └── 部署配置模板.md +│ ├── scripts/ # 快捷脚本 +│ │ ├── 快速检查服务器.py +│ │ ├── 一键部署.py +│ │ └── SSL证书检查.py +│ └── assets/ # 资源文件 +│ └── 系统架构说明.md +│ +├── .cursor/rules/serverconnect.mdc # Cursor规则文件(凭证+规范) +│ +├── 核心组件/ # 核心功能库 +│ ├── SSH客户端.py +│ ├── 宝塔API.py +│ ├── 服务器档案.py +│ ├── 环境修复.py +│ └── 腾讯云MySQL管理.py +│ +├── 功能插件/ # 独立功能模块 +│ ├── MySQL数据库分析.py +│ ├── MySQL自动清理.py +│ ├── Nginx管理器.py +│ ├── Node管理器.py +│ ├── SSL证书管理器.py +│ ├── 宝塔中控.py +│ ├── 宝塔部署器.py +│ ├── 状态报告生成.py +│ ├── 系统诊断.py +│ ├── 阿里云DNS管理器.py +│ └── 项目深度修复.py +│ +├── 部署配置/ # JSON配置文件 +│ ├── soul_部署配置.json +│ ├── zhiji_部署配置.json +│ └── ... +│ +├── 状态报告/ # 生成的报告 +│ └── *.html +│ +├── 开发文档/ # 开发文档 +│ ├── 服务器管理插件系统总览.md +│ ├── 宝塔API接口文档.md +│ └── ... +│ +└── 配置.py # 全局配置 +``` + +--- + +## 五、使用规范 + +### 5.1 操作优先级 + +1. **优先使用宝塔API** - 稳定、可追溯 +2. **备选使用SSH** - 用于API无法完成的操作 +3. **必要时手动操作** - 如添加Node项目到宝塔面板 + +### 5.2 安全规范 + +- ❌ 不在代码中硬编码密码 +- ❌ 不执行危险命令(reboot, rm -rf /等) +- ❌ 不使用独立PM2(避免与宝塔冲突) +- ✅ 操作前先检查服务器状态 +- ✅ 操作后验证结果 +- ✅ 生成操作报告 + +### 5.3 输出规范 + +每次操作完成后,必须输出: +1. 操作结果(成功/失败) +2. 关键信息(端口、域名、访问地址) +3. 验证结果(端口监听、HTTP响应) +4. 后续操作建议 diff --git a/开发文档/服务器管理/references/部署配置模板.md b/开发文档/服务器管理/references/部署配置模板.md new file mode 100644 index 0000000..5c4a891 --- /dev/null +++ b/开发文档/服务器管理/references/部署配置模板.md @@ -0,0 +1,154 @@ +# 部署配置模板 + +## JSON配置文件结构 + +```json +{ + "项目名称": "soul", + "项目说明": "Soul创业实验书籍阅读平台", + "域名": "soul.quwanzhi.com", + "域名列表": ["soul.quwanzhi.com"], + + "本地项目路径": "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验", + "服务器项目路径": "/www/wwwroot/soul", + + "运行用户": "www", + "Node版本": "v22.14.0", + "包管理器": "pnpm", + + "端口策略": { + "优先端口": 3006, + "候选范围": [3000, 3100] + }, + + "构建命令": { + "安装依赖": "pnpm install", + "构建": "pnpm run build", + "启动脚本模板": "next start -p {PORT}" + }, + + "部署策略": { + "部署前清空目录": true, + "排除目录": ["node_modules", ".next", ".git", "out"] + }, + + "宝塔Node项目": { + "最大内存MB": 4096, + "自动启动": true + }, + + "HTTPS": { + "启用": true, + "证书目录": "/www/server/panel/vhost/cert/www.quwanzhi.com" + }, + + "Nginx": { + "清理重复域名配置": true, + "配置模板": "标准反向代理" + } +} +``` + +--- + +## 最小化配置(必填字段) + +```json +{ + "项目名称": "myproject", + "域名": "myproject.quwanzhi.com", + "本地项目路径": "/path/to/local/project", + "服务器项目路径": "/www/wwwroot/myproject", + "端口策略": { + "优先端口": 3010 + } +} +``` + +--- + +## 不同项目类型配置示例 + +### Next.js 项目 + +```json +{ + "项目名称": "nextjs-app", + "域名": "app.quwanzhi.com", + "本地项目路径": "/path/to/nextjs", + "服务器项目路径": "/www/wwwroot/nextjs-app", + "包管理器": "pnpm", + "端口策略": {"优先端口": 3020}, + "构建命令": { + "启动脚本模板": "next start -p {PORT}" + } +} +``` + +### Vue 项目 + +```json +{ + "项目名称": "vue-admin", + "域名": "admin.quwanzhi.com", + "本地项目路径": "/path/to/vue", + "服务器项目路径": "/www/wwwroot/vue-admin", + "包管理器": "npm", + "端口策略": {"优先端口": 9528}, + "构建命令": { + "安装依赖": "npm install", + "构建": "npm run build:prod", + "启动脚本模板": "serve -s dist -p {PORT}" + } +} +``` + +### NestJS 后端 + +```json +{ + "项目名称": "api-server", + "域名": "api.quwanzhi.com", + "本地项目路径": "/path/to/nestjs", + "服务器项目路径": "/www/wwwroot/api-server", + "包管理器": "pnpm", + "端口策略": {"优先端口": 3200}, + "构建命令": { + "安装依赖": "pnpm install", + "构建": "pnpm run build", + "启动脚本模板": "node dist/main.js" + } +} +``` + +--- + +## 配置文件存放位置 + +``` +服务器管理/ +└── 部署配置/ + ├── soul_部署配置.json + ├── zhiji_部署配置.json + ├── kr_wb_部署配置.json + └── ... +``` + +--- + +## 使用方法 + +```python +import json +from 功能插件.宝塔部署器 import 宝塔部署器 + +# 读取配置 +with open("部署配置/soul_部署配置.json", "r") as f: + 配置 = json.load(f) + +# 执行部署 +部署器 = 宝塔部署器() +结果 = 部署器.部署Node网站(配置) + +print(结果) +``` diff --git a/开发文档/服务器管理/scripts/ssl证书检查.py b/开发文档/服务器管理/scripts/ssl证书检查.py new file mode 100644 index 0000000..4ac5695 --- /dev/null +++ b/开发文档/服务器管理/scripts/ssl证书检查.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +SSL证书检查脚本 +=============== +用途:检查所有服务器的SSL证书状态 + +使用方法: +python3 ssl证书检查.py +python3 ssl证书检查.py --fix # 自动修复过期证书 +""" + +import sys +import time +import hashlib +import requests +import urllib3 +from datetime import datetime + +# 禁用SSL警告 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 服务器配置 +服务器列表 = { + "小型宝塔": { + "面板地址": "https://42.194.232.22:9988", + "密钥": "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd" + }, + "存客宝": { + "面板地址": "https://42.194.245.239:9988", + "密钥": "TNKjqDv5N1QLOU20gcmGVgr82Z4mXzRi" + }, + "kr宝塔": { + "面板地址": "https://43.139.27.93:9988", + "密钥": "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" + } +} + +def 生成签名(api_key: str) -> tuple: + """生成宝塔API签名""" + now_time = int(time.time()) + sign_str = str(now_time) + hashlib.md5(api_key.encode('utf-8')).hexdigest() + request_token = hashlib.md5(sign_str.encode('utf-8')).hexdigest() + return now_time, request_token + +def 获取证书列表(面板地址: str, 密钥: str) -> dict: + """获取SSL证书列表""" + now_time, request_token = 生成签名(密钥) + + url = f"{面板地址}/ssl?action=GetCertList" + data = { + "request_time": now_time, + "request_token": request_token + } + + try: + response = requests.post(url, data=data, timeout=10, verify=False) + return response.json() + except Exception as e: + return {"error": str(e)} + +def 获取网站列表(面板地址: str, 密钥: str) -> dict: + """获取网站列表""" + now_time, request_token = 生成签名(密钥) + + url = f"{面板地址}/data?action=getData&table=sites" + data = { + "request_time": now_time, + "request_token": request_token, + "limit": 100, + "p": 1 + } + + try: + response = requests.post(url, data=data, timeout=10, verify=False) + return response.json() + except Exception as e: + return {"error": str(e)} + +def 检查服务器证书(名称: str, 配置: dict) -> dict: + """检查单台服务器的证书状态""" + print(f"\n检查服务器: {名称}") + print("-" * 40) + + try: + # 获取网站列表 + 网站数据 = 获取网站列表(配置["面板地址"], 配置["密钥"]) + + if "error" in 网站数据: + print(f" ❌ API错误: {网站数据['error']}") + return {"error": 网站数据['error']} + + 网站列表 = 网站数据.get("data", []) + + if not 网站列表: + print(" ⚠️ 没有找到网站") + return {"网站数": 0} + + print(f" 📊 共 {len(网站列表)} 个网站") + + # 统计 + 已配置SSL = 0 + 未配置SSL = 0 + + for 网站 in 网站列表: + 网站名 = 网站.get("name", "未知") + ssl状态 = 网站.get("ssl", 0) + + if ssl状态: + 已配置SSL += 1 + 状态标识 = "🔒" + else: + 未配置SSL += 1 + 状态标识 = "🔓" + + print(f" {状态标识} {网站名}") + + print(f"\n 统计: 已配置SSL {已配置SSL} 个, 未配置 {未配置SSL} 个") + + return { + "网站数": len(网站列表), + "已配置SSL": 已配置SSL, + "未配置SSL": 未配置SSL + } + + except Exception as e: + print(f" ❌ 检查失败: {e}") + return {"error": str(e)} + +def main(): + 自动修复 = "--fix" in sys.argv + + print("=" * 60) + print(" SSL证书状态检查报告") + print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 60) + + 总统计 = { + "服务器数": 0, + "网站总数": 0, + "已配置SSL": 0, + "未配置SSL": 0 + } + + for 服务器名称, 配置 in 服务器列表.items(): + 结果 = 检查服务器证书(服务器名称, 配置) + + if "error" not in 结果: + 总统计["服务器数"] += 1 + 总统计["网站总数"] += 结果.get("网站数", 0) + 总统计["已配置SSL"] += 结果.get("已配置SSL", 0) + 总统计["未配置SSL"] += 结果.get("未配置SSL", 0) + + print("\n" + "=" * 60) + print(" 汇总统计") + print("=" * 60) + print(f" 服务器数量: {总统计['服务器数']}") + print(f" 网站总数: {总统计['网站总数']}") + print(f" 已配置SSL: {总统计['已配置SSL']} 🔒") + print(f" 未配置SSL: {总统计['未配置SSL']} 🔓") + print("=" * 60) + + if 自动修复 and 总统计['未配置SSL'] > 0: + print("\n⚠️ --fix 模式需要手动在宝塔面板配置SSL证书") + print(" 建议使用通配符证书 *.quwanzhi.com") + +if __name__ == "__main__": + main() diff --git a/开发文档/服务器管理/scripts/一键部署.py b/开发文档/服务器管理/scripts/一键部署.py new file mode 100644 index 0000000..a5660e1 --- /dev/null +++ b/开发文档/服务器管理/scripts/一键部署.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +一键部署脚本 +============ +用途:根据配置文件一键部署Node项目到宝塔服务器 + +使用方法: +python3 一键部署.py 项目名称 本地项目路径 + +示例: +python3 一键部署.py soul /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验 +""" + +import sys +import os +import subprocess +import time + +# 默认服务器配置 +默认配置 = { + "服务器IP": "42.194.232.22", + "SSH用户": "root", + "SSH密码": "Zhiqun1984", + "服务器根目录": "/www/wwwroot" +} + +def 执行命令(命令: str, 显示输出: bool = True) -> tuple: + """执行shell命令""" + result = subprocess.run(命令, shell=True, capture_output=True, text=True) + if 显示输出 and result.stdout: + print(result.stdout) + if result.stderr and "Warning" not in result.stderr: + print(f"错误: {result.stderr}") + return result.returncode, result.stdout + +def 部署项目(项目名称: str, 本地路径: str): + """执行部署流程""" + 服务器路径 = f"{默认配置['服务器根目录']}/{项目名称}" + 压缩文件 = f"/tmp/{项目名称}_update.tar.gz" + + print(f"\n{'='*60}") + print(f"开始部署: {项目名称}") + print(f"本地路径: {本地路径}") + print(f"服务器路径: {服务器路径}") + print(f"{'='*60}\n") + + # 步骤1: 压缩项目 + print("📦 步骤1: 压缩项目文件...") + 排除项 = "--exclude='node_modules' --exclude='.next' --exclude='.git' --exclude='android' --exclude='out'" + 压缩命令 = f"cd '{本地路径}' && tar {排除项} -czf {压缩文件} ." + code, _ = 执行命令(压缩命令, False) + if code != 0: + print("❌ 压缩失败") + return False + + # 获取文件大小 + 大小 = os.path.getsize(压缩文件) / 1024 / 1024 + print(f" ✅ 压缩完成,大小: {大小:.2f} MB") + + # 步骤2: 上传到服务器 + print("\n📤 步骤2: 上传到服务器...") + 上传命令 = f"sshpass -p '{默认配置['SSH密码']}' scp -o StrictHostKeyChecking=no {压缩文件} {默认配置['SSH用户']}@{默认配置['服务器IP']}:/tmp/" + code, _ = 执行命令(上传命令, False) + if code != 0: + print("❌ 上传失败") + return False + print(" ✅ 上传完成") + + # 步骤3-6: SSH远程执行 + print("\n🔧 步骤3-6: 服务器端操作...") + + SSH前缀 = f"sshpass -p '{默认配置['SSH密码']}' ssh -o StrictHostKeyChecking=no {默认配置['SSH用户']}@{默认配置['服务器IP']}" + + # 清理旧文件 + 清理命令 = f"{SSH前缀} 'cd {服务器路径} && rm -rf app components lib public styles *.json *.js *.ts *.mjs *.md .next 2>/dev/null || true'" + 执行命令(清理命令, False) + print(" ✅ 清理旧文件") + + # 解压 + 解压命令 = f"{SSH前缀} 'cd {服务器路径} && tar -xzf /tmp/{项目名称}_update.tar.gz'" + 执行命令(解压命令, False) + print(" ✅ 解压新代码") + + # 安装依赖 + print("\n📚 安装依赖 (这可能需要几分钟)...") + 安装命令 = f"{SSH前缀} 'export PATH=/www/server/nodejs/v22.14.0/bin:$PATH && cd {服务器路径} && npm install --legacy-peer-deps 2>&1'" + 执行命令(安装命令, True) + + # 构建 + print("\n🏗️ 构建项目...") + 构建命令 = f"{SSH前缀} 'export PATH=/www/server/nodejs/v22.14.0/bin:$PATH && cd {服务器路径} && npm run build 2>&1'" + 执行命令(构建命令, True) + + # 重启PM2 + print("\n🔄 重启服务...") + 重启命令 = f"{SSH前缀} 'export PATH=/www/server/nodejs/v22.14.0/bin:$PATH && pm2 restart {项目名称} 2>&1'" + 执行命令(重启命令, True) + + # 清理临时文件 + 清理临时命令 = f"{SSH前缀} 'rm -f /tmp/{项目名称}_update.tar.gz'" + 执行命令(清理临时命令, False) + os.remove(压缩文件) + + print(f"\n{'='*60}") + print("✅ 部署完成!") + print(f"{'='*60}") + print("\n⚠️ 请在宝塔面板手动重启项目:") + print(f" 1. 登录 https://42.194.232.22:9988/ckbpanel") + print(f" 2. 进入【网站】→【Node项目】") + print(f" 3. 找到 {项目名称},点击【重启】") + + return True + +def main(): + if len(sys.argv) < 3: + print("用法: python3 一键部署.py <项目名称> <本地项目路径>") + print("\n示例:") + print(" python3 一键部署.py soul /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验") + print(" python3 一键部署.py kr_wb /Users/karuo/Documents/开发/4、小工具/whiteboard") + sys.exit(1) + + 项目名称 = sys.argv[1] + 本地路径 = sys.argv[2] + + if not os.path.exists(本地路径): + print(f"❌ 本地路径不存在: {本地路径}") + sys.exit(1) + + 确认 = input(f"\n确认部署 {项目名称} 到服务器? (y/n): ") + if 确认.lower() != 'y': + print("已取消部署") + sys.exit(0) + + 部署项目(项目名称, 本地路径) + +if __name__ == "__main__": + main() diff --git a/开发文档/服务器管理/scripts/快速检查服务器.py b/开发文档/服务器管理/scripts/快速检查服务器.py new file mode 100644 index 0000000..5667788 --- /dev/null +++ b/开发文档/服务器管理/scripts/快速检查服务器.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +快速检查服务器状态 +================== +用途:一键检查所有服务器的基本状态 + +使用方法: +python3 快速检查服务器.py +""" + +import time +import hashlib +import requests +import urllib3 + +# 禁用SSL警告 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# 服务器配置 +服务器列表 = { + "小型宝塔": { + "面板地址": "https://42.194.232.22:9988", + "密钥": "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd" + }, + "存客宝": { + "面板地址": "https://42.194.245.239:9988", + "密钥": "TNKjqDv5N1QLOU20gcmGVgr82Z4mXzRi" + }, + "kr宝塔": { + "面板地址": "https://43.139.27.93:9988", + "密钥": "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT" + } +} + +def 生成签名(api_key: str) -> tuple: + """生成宝塔API签名""" + now_time = int(time.time()) + sign_str = str(now_time) + hashlib.md5(api_key.encode('utf-8')).hexdigest() + request_token = hashlib.md5(sign_str.encode('utf-8')).hexdigest() + return now_time, request_token + +def 获取系统信息(面板地址: str, 密钥: str) -> dict: + """获取系统基础统计信息""" + now_time, request_token = 生成签名(密钥) + + url = f"{面板地址}/system?action=GetSystemTotal" + data = { + "request_time": now_time, + "request_token": request_token + } + + try: + response = requests.post(url, data=data, timeout=10, verify=False) + return response.json() + except Exception as e: + return {"error": str(e)} + +def 检查单台服务器(名称: str, 配置: dict) -> dict: + """检查单台服务器状态""" + try: + 系统信息 = 获取系统信息(配置["面板地址"], 配置["密钥"]) + + if isinstance(系统信息, dict) and "error" not in 系统信息 and 系统信息.get("status") != False: + return { + "名称": 名称, + "状态": "✅ 正常", + "CPU": f"{系统信息.get('cpuRealUsed', 'N/A')}%", + "内存": f"{系统信息.get('memRealUsed', 'N/A')}%", + "磁盘": f"{系统信息.get('diskPer', 'N/A')}%" + } + else: + return { + "名称": 名称, + "状态": "❌ API错误", + "错误": str(系统信息) + } + except Exception as e: + return { + "名称": 名称, + "状态": "❌ 连接失败", + "错误": str(e) + } + +def main(): + print("=" * 60) + print(" 服务器状态检查报告") + print("=" * 60) + print() + + for 名称, 配置 in 服务器列表.items(): + 结果 = 检查单台服务器(名称, 配置) + print(f"📦 {结果['名称']}") + print(f" 状态: {结果['状态']}") + if "CPU" in 结果: + print(f" CPU: {结果['CPU']} | 内存: {结果['内存']} | 磁盘: {结果['磁盘']}") + if "错误" in 结果: + print(f" 错误: {结果['错误'][:50]}...") + print() + + print("=" * 60) + +if __name__ == "__main__": + main()