更新.gitignore以排除部署配置文件,删除不再使用的一键部署脚本,优化小程序部署流程,增强文档说明。

This commit is contained in:
乘风
2026-01-31 17:39:21 +08:00
parent ceac5b73ff
commit 70497d3047
45 changed files with 9346 additions and 272 deletions

21
.dockerignore Normal file
View 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
View File

@@ -5,3 +5,7 @@ node_modules/
.trae/
*.log
node_modules
# 部署配置(含服务器信息,勿提交)
deploy_config.json
scripts/deploy_config.json

View File

@@ -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

View File

@@ -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') {

View File

@@ -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)

View 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 })
}
}

View File

@@ -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
View File

@@ -0,0 +1,19 @@
/**
* PM2 配置:用于 standalone 部署的服务器
* 启动方式node server.js不要用 npm start / next startstandalone 无 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
View 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 || '请求失败',
}
}
/**
* 解密回调 resourceAEAD_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
}

View File

@@ -52,7 +52,8 @@
}
},
"requiredPrivateInfos": [
"getLocation"
"getLocation",
"chooseAddress"
],
"lazyCodeLoading": "requiredComponents",
"style": "v2",

View File

@@ -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
View File

@@ -0,0 +1,3 @@
# 仅用于「部署到宝塔」脚本,非项目运行依赖
# 使用: pip install -r requirements-deploy.txt
paramiko>=2.9.0

370
scripts/deploy_baota.py Normal file
View 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 buildstandalone', step_label)
else:
log('本地构建 pnpm buildstandalone')
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 buildstandalone', 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.jsPORT=%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())

View 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 填私钥路径则用密钥登录,否则用密码。"
}

View File

@@ -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

View File

@@ -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)

View 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`(宝塔部署章节)

View 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

File diff suppressed because it is too large Load Diff

View 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/)

View 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
**适用于**:所有需要企业认证的小程序

View 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)

View 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)

View 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
}
}

View 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

View 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初始化成功")

View 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()

View 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()

View 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()

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View 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
}
}
]
}

View File

@@ -0,0 +1,7 @@
# 微信小程序管理工具依赖
# HTTP客户端
httpx>=0.25.0
# 环境变量管理
python-dotenv>=1.0.0

File diff suppressed because it is too large Load Diff

View 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

View 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)

View 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和宝塔PM2www用户
**现象**:
- 权限错误:`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
```

View 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中端口正确

View 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. 后续操作建议

View 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(结果)
```

View 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()

View 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()

View 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()