更新.gitignore以排除部署配置文件,删除不再使用的一键部署脚本,优化小程序部署流程,增强文档说明。
This commit is contained in:
21
.dockerignore
Normal file
21
.dockerignore
Normal file
@@ -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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,3 +5,7 @@ node_modules/
|
||||
.trae/
|
||||
*.log
|
||||
node_modules
|
||||
|
||||
# 部署配置(含服务器信息,勿提交)
|
||||
deploy_config.json
|
||||
scripts/deploy_config.json
|
||||
|
||||
113
DEPLOYMENT.md
113
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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
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<string, unknown>[] = []
|
||||
try {
|
||||
if (userId) {
|
||||
rows = (await query(
|
||||
"SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC",
|
||||
[userId]
|
||||
)) as Record<string, unknown>[]
|
||||
} else {
|
||||
// 管理后台:无 userId 时返回全部订单
|
||||
rows = (await query(
|
||||
"SELECT * FROM orders ORDER BY created_at DESC"
|
||||
)) as Record<string, unknown>[]
|
||||
}
|
||||
} 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)
|
||||
|
||||
65
app/api/payment/wechat/transfer/notify/route.ts
Normal file
65
app/api/payment/wechat/transfer/notify/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
19
ecosystem.config.cjs
Normal file
19
ecosystem.config.cjs
Normal file
@@ -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
|
||||
},
|
||||
],
|
||||
};
|
||||
212
lib/wechat-transfer.ts
Normal file
212
lib/wechat-transfer.ts
Normal file
@@ -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<CreateTransferResult> {
|
||||
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<string, unknown>
|
||||
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<string, unknown> {
|
||||
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<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证回调签名(需平台公钥,可选)
|
||||
*/
|
||||
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
|
||||
}
|
||||
@@ -52,7 +52,8 @@
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos": [
|
||||
"getLocation"
|
||||
"getLocation",
|
||||
"chooseAddress"
|
||||
],
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"style": "v2",
|
||||
|
||||
@@ -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",
|
||||
|
||||
3
requirements-deploy.txt
Normal file
3
requirements-deploy.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# 仅用于「部署到宝塔」脚本,非项目运行依赖
|
||||
# 使用: pip install -r requirements-deploy.txt
|
||||
paramiko>=2.9.0
|
||||
370
scripts/deploy_baota.py
Normal file
370
scripts/deploy_baota.py
Normal file
@@ -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())
|
||||
12
scripts/deploy_config.example.json
Normal file
12
scripts/deploy_config.example.json
Normal file
@@ -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 填私钥路径则用密钥登录,否则用密码。"
|
||||
}
|
||||
22
一键部署小程序.bat
22
一键部署小程序.bat
@@ -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
|
||||
225
一键部署小程序.py
225
一键部署小程序.py
@@ -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)
|
||||
77
开发文档/8、部署/宝塔配置检查说明.md
Normal file
77
开发文档/8、部署/宝塔配置检查说明.md
Normal file
@@ -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`(宝塔部署章节)
|
||||
85
开发文档/8、部署/当前项目部署到线上.md
Normal file
85
开发文档/8、部署/当前项目部署到线上.md
Normal file
@@ -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
|
||||
1797
开发文档/小程序管理/SKILL.md
Normal file
1797
开发文档/小程序管理/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
176
开发文档/小程序管理/references/API接口速查表.md
Normal file
176
开发文档/小程序管理/references/API接口速查表.md
Normal file
@@ -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/)
|
||||
307
开发文档/小程序管理/references/企业认证完整指南.md
Normal file
307
开发文档/小程序管理/references/企业认证完整指南.md
Normal file
@@ -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
|
||||
**适用于**:所有需要企业认证的小程序
|
||||
276
开发文档/小程序管理/references/审核规范.md
Normal file
276
开发文档/小程序管理/references/审核规范.md
Normal file
@@ -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)
|
||||
242
开发文档/小程序管理/references/隐私协议填写指南.md
Normal file
242
开发文档/小程序管理/references/隐私协议填写指南.md
Normal file
@@ -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
|
||||
<!-- privacy-popup.wxml -->
|
||||
<view class="privacy-popup" wx:if="{{showPrivacy}}">
|
||||
<view class="popup-content">
|
||||
<view class="title">用户隐私保护提示</view>
|
||||
<view class="desc">
|
||||
在使用本小程序前,请仔细阅读
|
||||
<text class="link" bindtap="openPrivacyContract">《用户隐私保护指引》</text>
|
||||
</view>
|
||||
<view class="buttons">
|
||||
<button class="btn-disagree" bindtap="handleDisagree">不同意</button>
|
||||
<button class="btn-agree" id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgree">同意</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
### 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)
|
||||
BIN
开发文档/小程序管理/scripts/__pycache__/mp_api.cpython-314.pyc
Normal file
BIN
开发文档/小程序管理/scripts/__pycache__/mp_api.cpython-314.pyc
Normal file
Binary file not shown.
40
开发文档/小程序管理/scripts/apps_config.json
Normal file
40
开发文档/小程序管理/scripts/apps_config.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
30
开发文档/小程序管理/scripts/env_template.txt
Normal file
30
开发文档/小程序管理/scripts/env_template.txt
Normal file
@@ -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
|
||||
635
开发文档/小程序管理/scripts/mp_api.py
Normal file
635
开发文档/小程序管理/scripts/mp_api.py
Normal file
@@ -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初始化成功")
|
||||
725
开发文档/小程序管理/scripts/mp_deploy.py
Normal file
725
开发文档/小程序管理/scripts/mp_deploy.py
Normal file
@@ -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 <app_id> # 一键部署
|
||||
python mp_deploy.py cert <app_id> # 提交认证
|
||||
python mp_deploy.py cert-status <app_id> # 查询认证状态
|
||||
python mp_deploy.py upload <app_id> # 上传代码
|
||||
python mp_deploy.py release <app_id> # 发布上线
|
||||
"""
|
||||
|
||||
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 <id> 一键部署")
|
||||
print(" python mp_deploy.py cert <id> 提交认证")
|
||||
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()
|
||||
555
开发文档/小程序管理/scripts/mp_full.py
Normal file
555
开发文档/小程序管理/scripts/mp_full.py
Normal file
@@ -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 <app_id> # 检查项目问题
|
||||
python mp_full.py auto <app_id> # 全自动部署(上传+提审)
|
||||
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()
|
||||
558
开发文档/小程序管理/scripts/mp_manager.py
Normal file
558
开发文档/小程序管理/scripts/mp_manager.py
Normal file
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
88
开发文档/小程序管理/scripts/reports/summary_20260125_113255.json
Normal file
88
开发文档/小程序管理/scripts/reports/summary_20260125_113255.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
开发文档/小程序管理/scripts/requirements.txt
Normal file
7
开发文档/小程序管理/scripts/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# 微信小程序管理工具依赖
|
||||
|
||||
# HTTP客户端
|
||||
httpx>=0.25.0
|
||||
|
||||
# 环境变量管理
|
||||
python-dotenv>=1.0.0
|
||||
1033
开发文档/提现功能完整技术文档.md
Normal file
1033
开发文档/提现功能完整技术文档.md
Normal file
File diff suppressed because it is too large
Load Diff
314
开发文档/服务器管理/SKILL.md
Normal file
314
开发文档/服务器管理/SKILL.md
Normal file
@@ -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
|
||||
142
开发文档/服务器管理/references/宝塔api接口文档.md
Normal file
142
开发文档/服务器管理/references/宝塔api接口文档.md
Normal file
@@ -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)
|
||||
184
开发文档/服务器管理/references/常见问题手册.md
Normal file
184
开发文档/服务器管理/references/常见问题手册.md
Normal file
@@ -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
|
||||
```
|
||||
64
开发文档/服务器管理/references/端口配置表.md
Normal file
64
开发文档/服务器管理/references/端口配置表.md
Normal file
@@ -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中端口正确
|
||||
310
开发文档/服务器管理/references/系统架构说明.md
Normal file
310
开发文档/服务器管理/references/系统架构说明.md
Normal file
@@ -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. 后续操作建议
|
||||
154
开发文档/服务器管理/references/部署配置模板.md
Normal file
154
开发文档/服务器管理/references/部署配置模板.md
Normal file
@@ -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(结果)
|
||||
```
|
||||
168
开发文档/服务器管理/scripts/ssl证书检查.py
Normal file
168
开发文档/服务器管理/scripts/ssl证书检查.py
Normal file
@@ -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()
|
||||
138
开发文档/服务器管理/scripts/一键部署.py
Normal file
138
开发文档/服务器管理/scripts/一键部署.py
Normal file
@@ -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()
|
||||
104
开发文档/服务器管理/scripts/快速检查服务器.py
Normal file
104
开发文档/服务器管理/scripts/快速检查服务器.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user