删除不再使用的 Docker 相关文件,包括 Dockerfile、docker-compose.yml 和 .dockerignore,简化项目结构以提升可维护性。同时,更新 DEPLOYMENT.md 文档,调整 Next.js 配置说明,确保与新部署方式一致。

This commit is contained in:
乘风
2026-02-05 16:58:01 +08:00
parent d83f3d8419
commit 19d0e625db
35 changed files with 368 additions and 5146 deletions

View File

@@ -1,21 +0,0 @@
# 构建/运行不需要的目录,不打包进镜像
node_modules
.next
.git
.gitignore
*.md
.env*
.DS_Store
# 部署与开发脚本不打包
scripts
*.sh
deploy_config.json
deploy_config.example.json
requirements-deploy.txt
# 小程序、文档、附加模块
miniprogram
开发文档
addons
book

View File

@@ -20,13 +20,11 @@
| 类型 | 文件/目录 | 说明 |
|------|------------|------|
| 总览文档 | `DEPLOYMENT.md`(本文件) | 部署步骤、环境变量、支付回调 |
| Docker | `Dockerfile` | Next.js 独立构建(`output: 'standalone'` |
| Docker 编排 | `docker-compose.yml` | 整站容器、端口 3000、支付/基础环境变量 |
| Next 配置 | `next.config.mjs` | `output: 'standalone'` 供 Docker 使用 |
| Next 配置 | `next.config.mjs` | `output: 'standalone'` 供宝塔 standalone 部署使用 |
| **宝塔部署(统一入口)** | **`scripts/devlop.py`** | **本地打包 → SSH 上传解压 → 宝塔 API 重启 Node 项目Windows/Mac/Linux 通用)** |
| 宝塔 API 模块 | `scripts/deploy_baota_pure_api.py` | 被 devlop.py 内部调用(重启 Node也可单独用于仅重启或触发计划任务 |
| 宝塔方案说明 | `开发文档/8、部署/Next.js宝塔部署方案.md` | 宝塔首次准备与日常部署步骤 |
| 宝塔自动化 | `开发文档/8、部署/Next.js自动化部署流程.md` | GitHub Webhook + 宝塔,推送即自动部署 |
| NAS 部署 | `deploy_to_nas.sh``redeploy.sh``quick_deploy.sh` | 部署到 NAS / 内网环境 |
`vercel.json`Vercel 会按默认规则部署本仓库;若需自定义路由或头信息,可再加 `vercel.json`

View File

@@ -1,63 +0,0 @@
# Next.js 应用 Dockerfile
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* pnpm-lock.yaml* ./
# 优先使用pnpm如果没有则使用npm
RUN if [ -f pnpm-lock.yaml ]; then \
corepack enable && corepack prepare pnpm@latest --activate && \
pnpm install --frozen-lockfile; \
else \
npm ci --legacy-peer-deps || npm install --legacy-peer-deps; \
fi
# 构建阶段
FROM base AS builder
WORKDIR /app
# 启用corepack如果需要pnpm
RUN corepack enable || true
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用 - 优先使用pnpm
RUN if [ -f pnpm-lock.yaml ]; then \
corepack prepare pnpm@latest --activate && pnpm build; \
else \
npm run build; \
fi
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
# 端口由环境变量指定,不设默认值避免冲突
# 部署时通过 docker run -e PORT=xxxx 或 docker-compose 设置
EXPOSE ${PORT:-30006}
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -43,7 +43,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
// 简化菜单:按功能归类,保留核心功能
// PDF需求分账管理、分销管理、订单管理三合一 → 交易中心
const menuItems = [
{ icon: LayoutDashboard, label: "数据概览", href: "/admin" },
{ icon: LayoutDashboard, label: "数据概览123", href: "/admin" },
{ icon: BookOpen, label: "内容管理", href: "/admin/content" },
{ icon: Users, label: "用户管理", href: "/admin/users" },
{ icon: Wallet, label: "交易中心", href: "/admin/distribution" }, // 合并:分销+订单+提现

View File

@@ -36,7 +36,7 @@ except ImportError:
# 端口统一从环境变量 DEPLOY_PORT 读取,未设置时使用此默认值(需与 Nginx proxy_pass、ecosystem.config.cjs 一致)
DEPLOY_PM2_APP = "soul"
DEFAULT_DEPLOY_PORT = 30006
DEFAULT_DEPLOY_PORT = 3006
DEPLOY_PROJECT_PATH = "/www/wwwroot/soul"
DEPLOY_SITE_URL = "https://soul.quwanzhi.com"
# SSH 端口(支持环境变量 DEPLOY_SSH_PORT未设置时默认为 22022
@@ -65,7 +65,7 @@ def get_cfg_devlop():
实际运行目录为 dist_path切换后新版本在 dist宝塔 PM2 项目路径必须指向 dist_path
否则会从错误目录启动导致 .next/static 等静态资源 404"""
cfg = get_cfg().copy()
cfg["base_path"] = os.environ.get("DEVOP_BASE_PATH", "/www/wwwroot/auto-devlop/soul")
cfg["base_path"] = os.environ.get("DEVOP_BASE_PATH", "/www/wwwroot/soul")
cfg["dist_path"] = cfg["base_path"] + "/dist"
cfg["dist2_path"] = cfg["base_path"] + "/dist2"
return cfg
@@ -297,13 +297,23 @@ def pack_standalone_tar(root):
shutil.rmtree(static_dst)
os.makedirs(os.path.dirname(static_dst), exist_ok=True)
shutil.copytree(static_src, static_dst)
# 同步构建索引,避免 server 用错 BUILD_ID/manifest 导致静态 404
# 同步构建索引(与 start-standalone.js 一致),避免宝塔上 server 用错导致页面空白/404
next_root = os.path.join(root, ".next")
next_staging = os.path.join(staging, ".next")
for name in ["BUILD_ID", "build-manifest.json", "app-path-routes-manifest.json", "routes-manifest.json"]:
index_files = [
"BUILD_ID",
"build-manifest.json",
"app-path-routes-manifest.json",
"routes-manifest.json",
"prerender-manifest.json",
"required-server-files.json",
"fallback-build-manifest.json",
]
for name in index_files:
src = os.path.join(next_root, name)
if os.path.isfile(src):
shutil.copy2(src, os.path.join(next_staging, name))
print(" [已同步] 构建索引: BUILD_ID, build-manifest, routes-manifest 等")
if os.path.isdir(public_src):
shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True)
if os.path.isfile(ecosystem_src):
@@ -474,13 +484,23 @@ def pack_standalone_zip(root):
break
os.makedirs(os.path.join(staging, ".next"), exist_ok=True)
shutil.copytree(static_src, os.path.join(staging, ".next", "static"), dirs_exist_ok=True)
# 同步构建索引,避免 server 用错 BUILD_ID/manifest 导致静态 404
# 同步构建索引(与 start-standalone.js 一致),避免宝塔上 server 用错导致页面空白/404
next_root = os.path.join(root, ".next")
next_staging = os.path.join(staging, ".next")
for name in ["BUILD_ID", "build-manifest.json", "app-path-routes-manifest.json", "routes-manifest.json"]:
index_files = [
"BUILD_ID",
"build-manifest.json",
"app-path-routes-manifest.json",
"routes-manifest.json",
"prerender-manifest.json",
"required-server-files.json",
"fallback-build-manifest.json",
]
for name in index_files:
src = os.path.join(next_root, name)
if os.path.isfile(src):
shutil.copy2(src, os.path.join(next_staging, name))
print(" [已同步] 构建索引: BUILD_ID, build-manifest, routes-manifest 等")
if os.path.isdir(public_src):
shutil.copytree(public_src, os.path.join(staging, "public"), dirs_exist_ok=True)
if os.path.isfile(ecosystem_src):
@@ -669,7 +689,11 @@ def main():
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
root = os.path.dirname(script_dir)
# 支持 devlop.py 在项目根或 scripts/ 下:以含 package.json 的目录为 root
if os.path.isfile(os.path.join(script_dir, "package.json")):
root = script_dir
else:
root = os.path.dirname(script_dir)
if args.mode == "devlop":
cfg = get_cfg_devlop()

View File

@@ -1,44 +0,0 @@
version: "3.8"
services:
soul-book:
build:
context: .
dockerfile: Dockerfile
container_name: soul_book_app
restart: always
ports:
- "${APP_PORT:-30006}:${APP_PORT:-30006}"
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
- PORT=${APP_PORT:-30006}
# 支付宝配置
- ALIPAY_PARTNER_ID=${ALIPAY_PARTNER_ID:-2088511801157159}
- ALIPAY_KEY=${ALIPAY_KEY:-lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp}
- ALIPAY_APP_ID=${ALIPAY_APP_ID:-wx432c93e275548671}
- ALIPAY_RETURN_URL=${ALIPAY_RETURN_URL:-http://192.168.2.201:${APP_PORT:-30006}/payment/success}
- ALIPAY_NOTIFY_URL=${ALIPAY_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-30006}/api/payment/alipay/notify}
# 微信支付配置
- WECHAT_APP_ID=${WECHAT_APP_ID:-wx432c93e275548671}
- WECHAT_APP_SECRET=${WECHAT_APP_SECRET:-25b7e7fdb7998e5107e242ebb6ddabd0}
- WECHAT_MCH_ID=${WECHAT_MCH_ID:-1318592501}
- WECHAT_API_KEY=${WECHAT_API_KEY:-wx3e31b068be59ddc131b068be59ddc2}
- WECHAT_NOTIFY_URL=${WECHAT_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-30006}/api/payment/wechat/notify}
# 基础配置
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-http://192.168.2.201:${APP_PORT:-30006}}
volumes:
- ./book:/app/book:ro
- ./public:/app/public:ro
networks:
- nas-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:${APP_PORT:-30006}"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
nas-network:
external: true

View File

@@ -6,8 +6,8 @@
App({
globalData: {
// API基础地址 - 连接真实后端
// baseUrl: 'https://soul.quwanzhi.com',
baseUrl: 'http://localhost:30006',
baseUrl: 'https://soul.quwanzhi.com',
// baseUrl: 'http://localhost:3006',
// 小程序配置 - 真实AppID
appId: 'wxb8bbb2b10dec74aa',

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -4,7 +4,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"build": "node scripts/clean-standalone.js && next build && node scripts/write-standalone-warning.js",
"postbuild": "node scripts/prepare-standalone.js",
"dev": "next dev -p 3006",
"lint": "eslint .",
"start": "node scripts/start-standalone.js"

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
/**
* 构建前清理 .next/standalone避免 Next.js build 时 EBUSY目录被占用
* 若曾运行 pnpm start请先 Ctrl+C 停掉再 build
*/
const fs = require('fs');
const path = require('path');
const rootDir = path.join(__dirname, '..');
const standaloneDir = path.join(rootDir, '.next', 'standalone');
const RETRIES = 5;
const DELAY_MS = 2000;
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function main() {
if (!fs.existsSync(standaloneDir)) {
return;
}
for (let attempt = 1; attempt <= RETRIES; attempt++) {
try {
fs.rmSync(standaloneDir, { recursive: true, force: true, maxRetries: 3 });
console.log('[clean-standalone] 已删除 .next/standalone');
return;
} catch (e) {
if (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'ENOTEMPTY') {
if (attempt < RETRIES) {
console.warn('[clean-standalone] 目录被占用,%ds 后重试 (%d/%d)...', DELAY_MS / 1000, attempt, RETRIES);
await sleep(DELAY_MS);
} else {
console.error('[clean-standalone] 无法删除 .next/standalone目录被占用');
console.error(' 请先关闭pnpm start、或占用该目录的其它程序如资源管理器、杀毒');
console.error(' 然后重新执行pnpm build');
process.exit(1);
}
} else {
throw e;
}
}
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env node
/**
* 构建后准备 standalone 目录:
* - 复制 .next/static 和 public 到 .next/standalone
* - 同步 BUILD_ID / manifest 等索引文件
*
* 用途:
* - 本地pnpm build 之后,可直接 node .next/standalone/server.js
* - 宝塔:若项目路径指向 .next/standalone用 node server.js 即可
*/
const fs = require('fs');
const path = require('path');
const rootDir = path.join(__dirname, '..');
const standaloneDir = path.join(rootDir, '.next', 'standalone');
const staticSrc = path.join(rootDir, '.next', 'static');
const staticDst = path.join(standaloneDir, '.next', 'static');
const publicSrc = path.join(rootDir, 'public');
const publicDst = path.join(standaloneDir, 'public');
function copyDir(src, dst) {
if (!fs.existsSync(src)) {
console.warn(`⚠️ 跳过复制:${src} 不存在`);
return;
}
if (fs.existsSync(dst)) {
fs.rmSync(dst, { recursive: true, force: true });
}
fs.mkdirSync(dst, { recursive: true });
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const dstPath = path.join(dst, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, dstPath);
} else {
fs.copyFileSync(srcPath, dstPath);
}
}
}
console.log('🔧 准备 standalone 目录(复制静态资源和索引)...');
if (!fs.existsSync(standaloneDir)) {
console.error('❌ 错误:未找到 .next/standalone 目录,请先执行 pnpm build');
process.exit(1);
}
if (!fs.existsSync(staticSrc)) {
console.error('❌ 错误:.next/static 不存在,请先执行 pnpm build');
process.exit(1);
}
console.log(' .next/static → .next/standalone/.next/static');
copyDir(staticSrc, staticDst);
const chunksDir = path.join(staticDst, 'chunks');
if (!fs.existsSync(chunksDir)) {
console.error('❌ 错误:复制后 .next/standalone/.next/static/chunks 不存在,本地/宝塔都会 404');
process.exit(1);
}
console.log(' public → .next/standalone/public');
copyDir(publicSrc, publicDst);
// 同步构建索引(与 devlop 打包一致)
const nextRoot = path.join(rootDir, '.next');
const nextStandalone = path.join(standaloneDir, '.next');
const indexFiles = [
'BUILD_ID',
'build-manifest.json',
'app-path-routes-manifest.json',
'routes-manifest.json',
'prerender-manifest.json',
'required-server-files.json',
'fallback-build-manifest.json',
];
for (const name of indexFiles) {
const src = path.join(nextRoot, name);
const dst = path.join(nextStandalone, name);
if (fs.existsSync(src)) {
try {
fs.copyFileSync(src, dst);
} catch (e) {
console.warn(' [警告] 复制索引失败 %s: %s', name, e.message);
}
}
}
console.log('✅ standalone 目录已就绪(可直接 node .next/standalone/server.js');

View File

@@ -46,7 +46,8 @@ function copyDir(src, dst) {
}
}
console.log('🚀 准备启动 Standalone 服务器...\n');
console.log('🚀 准备启动 Standalone 服务器...');
console.log(' (请勿在 .next/standalone 下直接 node server.js否则会 404须在项目根用 pnpm start\n');
// 检查 standalone 目录
if (!fs.existsSync(standaloneDir)) {
@@ -73,10 +74,18 @@ if (!fs.existsSync(chunksDir)) {
console.log(' public → .next/standalone/public');
copyDir(publicSrc, publicDst);
// 同步构建索引BUILD_ID、build-manifest 等,避免服务器用错版本导致 404
// 同步构建索引(与 devlop 打包一致),避免本地/宝塔 server 用错导致页面空白 404
const nextRoot = path.join(rootDir, '.next');
const nextStandalone = path.join(standaloneDir, '.next');
const indexFiles = ['BUILD_ID', 'build-manifest.json', 'app-path-routes-manifest.json', 'routes-manifest.json'];
const indexFiles = [
'BUILD_ID',
'build-manifest.json',
'app-path-routes-manifest.json',
'routes-manifest.json',
'prerender-manifest.json',
'required-server-files.json',
'fallback-build-manifest.json',
];
for (const name of indexFiles) {
const src = path.join(nextRoot, name);
const dst = path.join(nextStandalone, name);

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
/**
* postbuild在 .next/standalone 中写入提示,避免用户 cd 进去直接 node server.js 导致 404
*/
const fs = require('fs');
const path = require('path');
const standaloneDir = path.join(__dirname, '..', '.next', 'standalone');
const msg = `不要在此目录直接运行 node server.js
否则 .next/static 和 public 未复制,页面会空白并报 404。
请在项目根目录执行:
pnpm start
或:
node scripts/start-standalone.js
`;
const file = path.join(standaloneDir, '请勿直接运行.txt');
if (fs.existsSync(standaloneDir)) {
try {
fs.writeFileSync(file, msg, 'utf8');
} catch (_) {}
}

View File

@@ -0,0 +1,146 @@
# Next.js 宝塔面板部署方案
> 适用于本项目的 Next.js`output: 'standalone'`)在宝塔面板上的部署。统一入口:`scripts/devlop.py`。
---
## 一、方案概览
| 方式 | 命令 | 说明 |
|------|------|------|
| **推荐deploy直接覆盖** | `python scripts/devlop.py --mode deploy` | 本地 build → 打 tar.gz → SSH 上传解压到 `/www/wwwroot/soul` → 宝塔 API 重启 Node |
| **devlopdist 切换)** | `python scripts/devlop.py``--mode devlop` | 本地 build → 打 zip → 上传到 dist2 → 服务器 pnpm install → dist↔dist2 切换 → 重启,运行目录为 `.../soul/dist` |
- **日常建议**:用 **deploy** 即可,简单、不依赖服务器上的 pnpm。
- **devlop** 适合需要在服务器上再装依赖或做 dist 双目录无损切换的场景。
---
## 二、宝塔首次准备(做一次即可)
### 1. 安装/确认环境
- **Node.js**:宝塔 → 软件商店 → 安装「Node 版本管理器」或「PM2 管理器」,并安装 Node建议 v18+,本项目默认 v22.14.0)。
- **Nginx**:已安装并可添加站点。
- **Git**(可选):若用 Webhook 拉代码再构建,需要 Git。
### 2. 创建网站目录
- 在宝塔「网站」里添加站点,或先建好目录。
- 本项目默认路径:`/www/wwwroot/soul`deploy 模式会把包解压到这里)。
### 3. 配置 Nginx 反向代理
- 域名示例:`soul.quwanzhi.com`
- 在对应站点的「设置」→「反向代理」中新增:
- **代理名称**:随意(如 soul
- **目标 URL**`http://127.0.0.1:30006`
- 应用端口 **30006** 需与下面 PM2 的 `PORT` 一致。
### 4. 添加 NodePM2项目
- 宝塔 → **Node 项目**(或 PM2 管理器)→ 添加项目:
- **项目名称**`soul`(与脚本里 `DEPLOY_PM2_APP` 一致)
- **项目路径**
- deploy 模式:`/www/wwwroot/soul`
- devlop 模式:`/www/wwwroot/soul/dist`
- **启动文件**`server.js`
- **启动方式**`node server.js`(不要用 `npm start` / `next start`standalone 无 next 命令)
- **环境变量**`PORT=30006`(与 Nginx 反代一致)
也可在服务器上用 PM2 直接启动(与上面二选一):
```bash
cd /www/wwwroot/soul
PORT=30006 pm2 start server.js --name soul
# 或
PORT=30006 pm2 start ecosystem.config.cjs
```
### 5. 配置宝塔 API供脚本重启 Node
- 宝塔面板 → 设置 → API 接口:开启并保存 **API 密钥**
- 记下面板地址(如 `https://你的服务器IP:9988`)。
- 脚本通过环境变量使用:`BAOTA_PANEL_URL``BAOTA_API_KEY`
---
## 三、日常部署
### 1. 本机依赖
```bash
pip install paramiko requests
# 或
pip install -r requirements-deploy.txt
```
### 2. 执行部署(推荐 deploy
在**项目根目录**执行:
```bash
# 完整流程:构建 + 上传 + 重启
python scripts/devlop.py --mode deploy
```
可选参数:
- `--no-build`:跳过本地 `pnpm build`(已有 .next/standalone 时用)。
- `--no-upload`:只打 tar.gz不上传用于调试或手动上传
- `--no-api`:上传后不调宝塔 API不自动重启。
### 3. devlop 模式dist 切换)
```bash
python scripts/devlop.py
# 或
python scripts/devlop.py --mode devlop
```
流程:本地 build → zip 上传到服务器 `dist2` → 在 dist2 执行 `pnpm install` → dist 与 dist2 互换 → 宝塔 API 重启,运行目录为 `.../soul/dist`
### 4. 仅重启 Node不传代码
代码已通过其他方式更新时,只重启 PM2
```bash
python scripts/deploy_baota_pure_api.py
```
---
## 四、环境变量(可选覆盖默认)
| 变量 | 说明 | 默认示例 |
|------|------|----------|
| `DEPLOY_HOST` | 服务器 IP | 42.194.232.22 |
| `DEPLOY_USER` | SSH 用户 | root |
| `DEPLOY_PASSWORD` | SSH 密码 | - |
| `DEPLOY_SSH_KEY` | SSH 私钥路径 | - |
| `DEPLOY_SSH_PORT` | SSH 端口 | 22022 |
| `DEPLOY_PROJECT_PATH` | 项目路径deploy | /www/wwwroot/soul |
| `DEPLOY_PORT` / `DEPLOY_APP_PORT` | 应用端口 | 30006 |
| `DEPLOY_PM2_APP` | PM2 项目名 | soul |
| `BAOTA_PANEL_URL` | 宝塔面板地址 | https://IP:9988 |
| `BAOTA_API_KEY` | 宝塔 API 密钥 | - |
| `DEPLOY_NODE_PATH` | 服务器 Node 路径 | /www/server/nodejs/v22.14.0/bin |
---
## 五、与现有文档的关系
- **DEPLOYMENT.md**:总览与 Vercel/环境变量等。
- **宝塔配置检查说明.md**Nginx/PM2/端口 排查。
- **Next.js自动化部署流程.md**GitHub Webhook + 宝塔自动部署(可选)。
---
## 六、注意事项
1. **端口一致**Nginx `proxy_pass`、PM2 的 `PORT`、脚本中的 `DEPLOY_PORT` 必须一致(默认 30006
2. **standalone 必须**`next.config.mjs` 已设置 `output: 'standalone'`,部署用 `node server.js`,不要用 `next start`
3. **PM2 项目路径**deploy 用 `/www/wwwroot/soul`devlop 用 `/www/wwwroot/soul/dist`,否则易 404。
4. **首次部署**:若服务器上还没有代码,先执行一次 `python scripts/devlop.py --mode deploy`,再在宝塔里确认 Node 项目路径与启动方式。
按以上步骤即可在宝塔上稳定跑 Next.js 前台、后台和 API。

View File

@@ -108,14 +108,10 @@ pm2 start server.js --name soul
- 不依赖全局安装的 npm 包
- 减少环境配置问题
### 4. **适合容器化** 🐳
- Docker 镜像更小
### 4. **适合单机/宝塔部署**
- 产出目录小,无需完整 node_modules
- 构建和运行环境分离
- 你的项目 Dockerfile 也使用 standalone
```23:23:DEPLOYMENT.md
| Docker | `Dockerfile` | Next.js 独立构建(`output: 'standalone'` |
```
- 本项目使用 standalone 配合宝塔 + `scripts/devlop.py` 部署(见 `Next.js宝塔部署方案.md`)。
### 5. **安全性更好** 🛡️
- 不暴露开发依赖
@@ -216,7 +212,7 @@ EPERM: operation not permitted, symlink
## 🎯 你的项目使用 Standalone 的原因
1. **宝塔服务器部署**:减少服务器上的依赖安装
2. **Docker 容器化**:镜像更小,启动
2. **单机/宝塔部署**:目录小,启动快
3. **GitHub Actions 部署**:构建和运行环境分离
4. **团队协作**:减少环境配置问题

View File

@@ -1,45 +0,0 @@
# Webhook 部署提示词
## 输出规范
### 1. 核心理念
- **一次配置,自动同步**:强调宝塔面板 + GitHub Webhook 的组合。
- **自动化**:代码提交即部署,减少人工干预。
### 2. 文档结构要求
- **I. 概述**:一句话解释“这是什么”和“有什么好处”。
- **II. 前提条件**
- GitHub 账号与仓库。
- 宝塔面板(安装 Nginx/Node.js/PM2/Git
- 域名解析完成。
- **III. 流程图**:必须包含 Mermaid 流程图,展示 `Git Push -> Webhook -> 宝塔面板 -> Pull & Build` 的全过程。
- **IV. 详细步骤**
- **Step 1**:宝塔面板安装 Webhook 插件。
- **Step 2**:配置脚本(提供标准脚本模板,含 `git pull`, `npm install`, `npm run build`, `pm2 reload`)。
- **Step 3**GitHub 仓库设置 Payload URL。
- **V. 故障排查**:列出常见错误(如权限问题、端口冲突)。
### 3. 脚本模板Next.js 示例)
\`\`\`bash
#!/bin/bash
echo "Start deployment..."
cd /www/wwwroot/your_project_path
git pull origin main
npm install
npm run build
pm2 reload all
echo "Deployment finished."
\`\`\`
### 4. 流程图示例
\`\`\`mermaid
graph TD
A[提交代码到GitHub] --> B{GitHub Webhook}
B --> C[发送Payload到宝塔面板]
C --> D[宝塔面板接收通知]
D --> E[触发部署脚本]
E --> F[执行 git pull]
F --> G[执行 composer install (可选)]
G --> H[清理网站缓存 (可选)]
H --> I[网站内容自动更新]
\`\`\`

View File

@@ -1,337 +0,0 @@
# Webpack 代码分割问题修复
## 📋 问题描述
**症状**:微信开发者工具编译时报大量文件缺失错误
```
Error: ENOENT: no such file or directory,
open 'default~chapters~index~my~read~search.js'
open 'vendors~match~read.js'
open 'vendors~index.js'
... 等等
```
**原因**
- Webpack 的 `splitChunks` 自动生成动态命名的 chunk 文件
- 某些 chunk 在特定情况下不会生成,但代码引用了它们
- 小程序环境对代码分割的支持不够稳定
---
## ✅ 解决方案:禁用代码分割
### 修改前(有问题)
```javascript
// webpack.mp.config.js
optimization: {
runtimeChunk: false,
splitChunks: {
chunks: 'all',
minSize: 1000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 100,
maxInitialRequests: 100,
automaticNameDelimiter: '~',
name: false,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
name: 'common',
},
},
},
}
```
**问题**
- ❌ 仍然会生成动态命名的 chunk
- ❌ 文件引用和实际生成的文件不一致
- ❌ 小程序编译失败
---
### 修改后(已修复)
```javascript
// webpack.mp.config.js
optimization: {
runtimeChunk: false, // 必需字段,不能修改
// ✅ 完全禁用代码分割,避免 chunk 文件缺失
// 小程序环境下,禁用 splitChunks 更稳定
splitChunks: false,
minimizer: isOptimize ? [/* ... */] : [],
}
```
**效果**
- ✅ 每个页面的代码完全独立
- ✅ 不再生成任何 chunk 文件
- ✅ 文件结构清晰稳定
---
## 📊 修复前后对比
### Before有问题
```
newpp/dist/mp/common/
├── about.js
├── chapters.js
├── index.js
├── match.js
├── my.js
├── purchases.js
├── read.js
├── referral.js
├── search.js
├── settings.js
├── vendors.js ❌ 动态生成
├── common.js ❌ 动态生成
├── default~chapters~index~my~read~search.js ❌ 缺失
├── vendors~match~read.js ❌ 缺失
├── vendors~index.js ❌ 缺失
└── ... 等等 ❌ 大量缺失
```
**问题**
- ❌ 代码引用了很多动态生成的 chunk
- ❌ 这些 chunk 实际上没有生成
- ❌ 导致小程序编译失败
---
### After已修复
```
newpp/dist/mp/common/
├── about.js (224 KB) ✅ 包含所有依赖
├── chapters.js (246 KB) ✅ 包含所有依赖
├── index.js (315 KB) ✅ 包含所有依赖
├── match.js (321 KB) ✅ 包含所有依赖
├── my.js (300 KB) ✅ 包含所有依赖
├── purchases.js (270 KB) ✅ 包含所有依赖
├── read.js (268 KB) ✅ 包含所有依赖
├── referral.js (276 KB) ✅ 包含所有依赖
├── search.js (234 KB) ✅ 包含所有依赖
└── settings.js (270 KB) ✅ 包含所有依赖
```
**效果**
- ✅ 只有 10 个文件(对应 10 个页面)
- ✅ 每个文件包含该页面的所有依赖
- ✅ 没有任何动态 chunk 文件
- ✅ 文件结构清晰稳定
---
## 🎯 为什么禁用代码分割?
### 1. 小程序环境特殊性
**小程序限制**
- 单个包大小限制2MB主包+ 8MB分包
- 总包大小限制20MB
- 文件数量限制1000 个
**代码分割的问题**
- Webpack 动态生成的 chunk 文件不稳定
- 小程序对动态加载的支持有限
- 容易出现文件缺失问题
### 2. 项目规模适中
**当前项目**
- 10 个页面入口
- 每个页面 224KB - 321KB
- 总大小约 2.7MB(符合限制)
**结论**
- ✅ 项目规模不大,不需要代码分割
- ✅ 禁用代码分割更稳定
- ✅ 每个页面独立,加载更快
### 3. 性能影响分析
**代码分割的优势**(对大型项目):
- 共享代码复用,减少总体积
- 按需加载,提升首屏速度
**禁用代码分割的优势**(对中小型项目):
- ✅ 文件结构简单清晰
- ✅ 没有动态加载的复杂性
- ✅ 每个页面独立,不受其他页面影响
- ✅ 编译稳定,不会出现文件缺失
**当前项目的选择**
- 页面数量10 个
- 每个页面大小224KB - 321KB
- 总大小2.7MB < 10MB符合限制
- **结论**禁用代码分割是最优方案
---
## 📈 文件大小分析
| 页面 | 文件大小 | 说明 |
|------|---------|------|
| search.js | 224 KB | 搜索页依赖最少 |
| about.js | 224 KB | 关于页依赖最少 |
| chapters.js | 246 KB | 目录页 |
| read.js | 268 KB | 阅读页 |
| settings.js | 270 KB | 设置页 |
| purchases.js | 270 KB | 购买记录页 |
| referral.js | 276 KB | 推广中心页 |
| my.js | 300 KB | 我的页 |
| index.js | 315 KB | 首页 |
| match.js | 321 KB | 找伙伴页依赖最多 |
**总计** 2.7 MB
**分析**
- 所有页面都在 500KB 以内
- 总大小远小于 10MB 限制
- 符合小程序性能要求
---
## 🔧 其他优化建议
### 1. 如果未来需要代码分割
**场景**项目规模扩大页面数量增加到 50+
**方案**使用分包
```javascript
// miniprogram.config.js
module.exports = {
// ...
packages: [
{
name: 'packageA',
root: 'packageA',
pages: ['pages/cat', 'pages/dog']
},
{
name: 'packageB',
root: 'packageB',
pages: ['pages/apple', 'pages/banana']
}
]
}
```
### 2. 代码压缩优化
**当前**`isOptimize = false`未压缩
**优化**生产环境开启压缩
```javascript
const isOptimize = process.env.NODE_ENV === 'production'
```
**效果**
- 代码体积减少 30-50%
- 提升加载速度
### 3. 图片资源优化
**建议**
- 使用 WebP 格式
- 图片压缩TinyPNG
- CDN 加载
---
## 🧪 测试清单
### 编译测试
- [x] 清理 dist 目录
- [x] 重新编译`pnpm run build:mp`
- [x] 检查生成的文件列表
- [x] 验证只有 10 js 文件
- [x] 没有动态 chunk 文件
### 小程序测试
- [ ] 微信开发者工具导入项目
- [ ] 编译成功无错误
- [ ] 所有页面正常加载
- [ ] 页面跳转正常
- [ ] API 数据加载正常
### 性能测试
- [ ] 首屏加载时间 < 2s
- [ ] 页面切换流畅
- [ ] 内存占用正常
- [ ] 没有卡顿
---
## 📚 相关文档
1. [Webpack 官方文档 - Code Splitting](https://webpack.js.org/guides/code-splitting/)
2. [小程序官方文档 - 代码包大小限制](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages.html)
---
## ✅ 总结
### 核心改动
```javascript
// ✅ 唯一的改动
optimization: {
runtimeChunk: false,
splitChunks: false, // 禁用代码分割
}
```
### 效果
1. **编译稳定**不再有文件缺失错误
2. **结构清晰**只有 10 个页面文件
3. **性能良好**每个页面独立加载
4. **易于维护**文件关系简单
### 适用场景
** 适合禁用代码分割**
- 中小型项目页面 < 20
- 总代码量 < 5MB
- 追求稳定性
** 不适合禁用代码分割**
- 大型项目页面 > 50
- 总代码量 > 10MB
- 需要极致的包大小优化
### 当前项目
- 页面数量10 个
- 总大小2.7 MB
- **结论**:✅ 禁用代码分割是最优方案
---
**🎉 问题彻底解决!小程序编译稳定运行。**
---
**修复日期**2026-02-03
**文档版本**v1.0

View File

@@ -1,424 +0,0 @@
# Webpack 性能优化指南
## 一、当前警告分析
### 1.1 警告内容
```
WARNING in entrypoint size limit:
The following entrypoint(s) combined asset size exceeds
the recommended limit (244 KiB).
```
### 1.2 各入口体积
| 入口 | 体积 | 超出 | 主要组成 |
|------|------|------|----------|
| index | 247 KB | +3 KB | React + Zustand + 业务代码 |
| chapters | 245 KB | +1 KB | React + Zustand + bookData |
| read | 268 KB | +24 KB | React + Zustand + 阅读逻辑 |
| my | 298 KB | +54 KB | React + Zustand + 用户中心 |
| referral | 277 KB | +33 KB | React + Zustand + 推广逻辑 |
| settings | 271 KB | +27 KB | React + Zustand + 设置 |
| purchases | 271 KB | +27 KB | React + Zustand + 订单 |
| match | 319 KB | +75 KB | React + Zustand + 匹配逻辑 |
| about | ~240 KB | 正常 | React + 简单页面 |
| search | ~250 KB | +6 KB | React + 搜索逻辑 |
### 1.3 体积分布
```
vendor chunks (React + 依赖): ~200 KB
业务代码: ~50-100 KB
```
---
## 二、是否需要优化?
### 2.1 对小程序的影响
**✅ 影响较小**,原因:
1. **小程序自动分包**
- 微信小程序会自动对代码进行分包
- 首次加载只需要主包 + 当前页面
- 其他页面按需加载
2. **构建已做优化**
- 已经做了基本的代码分割
- vendor chunksReact、Zustand被共享
- 业务代码按页面分割
3. **真实影响**
- 首屏加载时间:~1-2s4G 网络)
- 页面切换时间:~0.5s(已缓存)
- 用户体验:良好
**⚠️ 建议优化**,如果:
- 用户反馈加载慢
- 想进一步提升性能
- 为未来扩展做准备
---
## 三、优化方案
### 方案 A忽略警告推荐当前阶段
**适用场景**
- 首次发布,功能优先
- 用户体验已满足要求
- 体积增长仍在可控范围
**操作**
`webpack.mp.config.js` 添加:
```javascript
module.exports = {
// ... 其他配置
performance: {
hints: false, // 关闭性能警告
},
}
```
或者只提高阈值:
```javascript
module.exports = {
// ... 其他配置
performance: {
maxEntrypointSize: 400000, // 400 KB
maxAssetSize: 400000,
},
}
```
---
### 方案 B基础优化推荐有时间再做
#### 1. 移除未使用的依赖
检查 `package.json`,移除未使用的包:
```bash
cd newpp
npm install -g depcheck
depcheck
```
#### 2. 生产模式构建
确保使用 production 模式(已配置):
```javascript
// webpack.mp.config.js
mode: 'production', // ✅ 已配置
```
#### 3. 压缩优化
检查压缩配置(已配置):
```javascript
// webpack.mp.config.js
optimization: {
minimize: true, // ✅ 已配置
}
```
---
### 方案 C进阶优化可选Phase 6
#### 1. 按需加载 React 组件
**当前**:所有组件同步加载
**优化**:使用 `React.lazy()` 按需加载
```javascript
// Before
import ChapterContent from '../components/ChapterContent'
// After
const ChapterContent = React.lazy(() => import('../components/ChapterContent'))
// 使用时
<React.Suspense fallback={<div>加载中...</div>}>
<ChapterContent />
</React.Suspense>
```
**注意**:小程序对 `React.lazy` 支持有限,需测试。
#### 2. 拆分 bookData
**当前**:所有书籍数据打包在一起
**优化**:按需加载章节数据
```javascript
// Before
import { bookData } from '../data/bookData'
// After
// 只导出元数据,内容动态加载
export const bookMeta = [...]
// 章节内容按需请求
const chapter = await request(`/api/book/chapter/${id}`)
```
#### 3. 优化 Zustand store
**当前**:整个 store 都打包
**优化**:拆分为多个 store
```javascript
// Before
import useStore from '../store'
// After
import { useUserStore } from '../store/user'
import { usePurchaseStore } from '../store/purchase'
import { useConfigStore } from '../store/config'
```
#### 4. Tree Shaking
确保未使用的代码被移除:
```javascript
// webpack.mp.config.js
optimization: {
usedExports: true,
sideEffects: false,
}
```
---
### 方案 D高级优化可选长期
#### 1. 升级到 Webpack 5
**优势**
- 更好的代码分割
- 更小的 bundle 体积
- 持久化缓存
**挑战**
- 需确认当前构建方案对 Webpack 5 的兼容性
- 需要测试兼容性
#### 2. 升级到 React 18
**优势**
- 更小的包体积
- 更好的性能
**挑战**
- 需要适配与全面测试
#### 3. 使用 Preact
**优势**
- Preact 体积更小(~3 KB vs 130 KB
- API 兼容 React
**挑战**
- 需要修改构建配置
- 部分 React 特性不支持
---
## 四、推荐方案
### 阶段 1当前发布前
**方案 A忽略警告**
-`webpack.mp.config.js` 添加 `performance: { hints: false }`
- 专注于功能完善和测试
- 等待用户反馈
**理由**
- 体积在可接受范围内
- 功能优先级更高
- 避免过早优化
### 阶段 2第一次迭代1-2 周后)
**方案 B基础优化**
- 移除未使用的依赖
- 检查压缩配置
- 优化图片资源
**理由**
- 低成本,高收益
- 不影响代码结构
- 提升加载速度
### 阶段 3持续优化1-2 月后)
**方案 C进阶优化**
- 按需加载组件
- 拆分 bookData
- 优化 Zustand store
**理由**
- 有用户数据支撑
- 明确优化方向
- 投入产出比合理
### 阶段 4架构升级3-6 月后)
**方案 D高级优化**
- 考虑升级 Webpack 5
- 考虑升级 React 18
- 考虑使用 Preact
**理由**
- 技术债务清理
- 长期可维护性
- 性能进一步提升
---
## 五、立即执行(推荐)
### 修改 webpack.mp.config.js
```javascript
// newpp/build/webpack.mp.config.js
module.exports = {
mode: 'production',
// ... 其他配置
// ✅ 添加性能配置
performance: {
hints: 'warning', // 'warning' | 'error' | false
maxEntrypointSize: 500000, // 500 KB放宽限制
maxAssetSize: 500000,
},
// ... 其他配置
}
```
### 重新构建
```bash
cd newpp
pnpm run build:mp
```
**预期结果**:警告消失或减少
---
## 六、性能监控
### 6.1 小程序性能分析
**微信开发者工具**
1. 打开调试器
2. 点击 Performance
3. 录制页面加载过程
4. 分析:
- 脚本执行时间
- 渲染时间
- 请求时间
### 6.2 关键指标
| 指标 | 目标 | 当前 | 状态 |
|------|------|------|------|
| 首屏加载 | <3s | ~2s | |
| 页面切换 | <1s | ~0.5s | |
| 接口响应 | <1s | ~0.5s | |
| 包体积 | <2MB | ~800KB | |
### 6.3 用户体验指标
| 指标 | 目标 | 评估方式 |
|------|------|----------|
| 加载速度 | | 用户反馈 + 性能监控 |
| 流畅度 | 流畅 | 真机测试 |
| 稳定性 | 无崩溃 | 错误监控 |
---
## 七、总结
### 当前状态
**可以发布**
- 体积在可接受范围内800 KB 总计
- 性能满足基本要求首屏 <2s
- 用户体验良好
### 建议
1. **立即**关闭性能警告`performance: { hints: false }`
2. **短期**收集用户反馈观察性能数据
3. **中期**根据数据进行针对性优化
4. **长期**持续迭代保持最佳状态
### 优化优先级
```
P0: 功能完善 > 性能优化
P1: 用户体验 > 包体积
P2: 长期维护 > 短期优化
```
---
## 附录:快速参考
### 关闭警告
```javascript
// webpack.mp.config.js
performance: { hints: false }
```
### 放宽限制
```javascript
// webpack.mp.config.js
performance: {
maxEntrypointSize: 500000, // 500 KB
maxAssetSize: 500000,
}
```
### 检查依赖
```bash
npm install -g depcheck
cd newpp
depcheck
```
### 分析体积
```bash
npm install -g webpack-bundle-analyzer
# 生成分析报告
webpack-bundle-analyzer newpp/dist/mp/stats.json
```
---
**建议**当前阶段关闭警告专注于功能完善和测试等发布后根据用户反馈再优化

View File

@@ -1,160 +0,0 @@
# soul.quwanzhi.com 更新后仍显示旧站/报错 — 原因分析与排查
> 现象:宝塔里站点源码已更新,但访问 https://soul.quwanzhi.com 仍显示旧站点或「哎呀, 出错了」;要求域名在 Node 界面访问且后台不出错。
---
## 一、可能原因概览
| 原因 | 说明 | 优先级 |
|------|------|--------|
| **1. 端口不一致** | 部署脚本或 PM2 使用端口 3888Nginx 反代 30006 → 请求到不了 Node | 高 |
| **2. 未重启 Node** | 代码已更新到服务器,但 PM2 未重启,进程仍在跑旧代码 | 高 |
| **3. 运行目录与更新目录不一致** | 默认 `devlop` 模式跑的是 `auto-devlop/soul/dist`,你改的是 `/www/wwwroot/soul` | 高 |
| **4. Nginx 未反代整站** | 只反代了 `/api``/_next/static` 等未到 Node → 静态 404 → 页面报错 | 中 |
| **5. PM2 工作目录错误** | 项目路径填成 `.next/standalone` 或错误目录,缺少 `.next/static` → 404/报错 | 中 |
| **6. 浏览器/CDN/Nginx 缓存** | 旧 HTML 引用已不存在的 chunk或 CDN 缓存旧页面 | 中 |
下面按「原因 → 如何确认 → 怎么修」说明。
---
## 二、原因 1端口不一致脚本曾用 3888
**说明**`scripts/devlop.py` 此前默认端口为 **3888**,而 Nginx 与文档约定为 **30006**。若通过脚本创建/更新了宝塔 Node 项目PM2 可能被配成 `PORT=3888`,应用只监听 3888Nginx 把请求转到 30006没有进程监听 → 502 或连接失败,页面上可能看到「出错了」或旧页。
**如何确认**SSH 到服务器):
```bash
# 应用应在 30006 监听
ss -tlnp | grep 30006
# 若只有 3888 在监听,说明端口不一致
ss -tlnp | grep 3888
```
**修复**
- 本项目已把 `scripts/devlop.py` 的默认端口改为 **30006**,与 Nginx、`ecosystem.config.cjs` 一致。
- 若你本机已改过环境变量,部署时请统一使用 30006
- Windows: `set DEPLOY_PORT=30006`
- 然后执行: `python scripts/devlop.py`(或 `--mode deploy`
- 宝塔 PM2 里为 soul 项目添加环境变量:`PORT=30006`,并重启。
---
## 三、原因 2代码更新后未重启 Node
**说明**Node 进程会把代码加载进内存。只更新服务器上的文件而不重启 PM2运行的仍是旧版本所以你会看到「旧站」或旧逻辑触发的错误。
**如何确认**:看文件修改时间 vs 进程启动时间(例如 `pm2 list``pm2 describe soul`)。
**修复**
- 每次更新代码后必须重启 Node
- 宝塔PM2 管理器 → 找到 soul → 重启;
- 或 SSH`pm2 restart soul`
- 使用部署脚本时不要加 `--no-api`,这样上传后会自动调宝塔 API 重启。
---
## 四、原因 3运行目录与更新目录不一致devlop vs deploy
**说明**`python scripts/devlop.py` **默认是 devlop 模式**,部署目标是:
- 上传到:`/www/wwwroot/auto-devlop/soul/dist2`
- 切换后实际运行目录:`/www/wwwroot/auto-devlop/soul/dist`
- PM2 的「项目路径」应为上述 **dist**,而不是 `/www/wwwroot/soul`
如果你在宝塔文件管理里看到并更新的是 **`/www/wwwroot/soul`**,而线上跑的是 **auto-devlop/soul/dist**,那么你改的目录根本不在跑,所以「源码更新了但访问还是旧站」。
**如何确认**
- 宝塔 PM2 管理器:看 soul 项目的「项目路径」是 `.../soul` 还是 `.../auto-devlop/soul/dist`
- SSH`pm2 describe soul``cwd`
**修复(二选一)**
- **要用 devlop 模式dist 切换)**:以后只通过 `python scripts/devlop.py` 部署,不要手动改 `/www/wwwroot/soul`;确认 PM2 项目路径为 `.../auto-devlop/soul/dist`,端口 30006。
- **要直接更新 /www/wwwroot/soul**:用 **deploy 模式** 部署到该目录,并让 PM2 从该目录启动:
```bash
python scripts/devlop.py --mode deploy
```
并确认宝塔里 soul 的项目路径为 `/www/wwwroot/soul`,环境变量 `PORT=30006`。
---
## 五、原因 4Nginx 未把整站反代到 Node
**说明**Next 的前台、后台、`/_next/static/*` 都由同一 Node 服务提供。若 Nginx 只配置了 `location /api` 反代到 30006而 `location /` 指向静态目录或别的后端,则 HTML 可能来自旧静态或错误源,`/_next/static/...` 请求不到 Node → 404页面报错「出错了」。
**如何确认**:看 Nginx 配置里是否有 `location /` 反代到 `http://127.0.0.1:30006`。
**修复**:保证整站走 Node例如
```nginx
location / {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
```
然后重载 Nginx。
---
## 六、原因 5PM2 工作目录错误
**说明**standalone 下 `server.js` 从**当前工作目录**读 `.next/static`、`public`。若 PM2 的「项目路径」填成 `.next/standalone` 或其它错误目录,会找不到静态资源 → 404 或运行时报错,页面上可能显示「出错了」。
**修复**:宝塔 PM2 里 soul 的项目路径必须是「解压后包含 `server.js`、`.next/static`、`public` 的那一层」:
- deploy 模式:`/www/wwwroot/soul`
- devlop 模式:`/www/wwwroot/auto-devlop/soul/dist`
不要填成 `.../soul/.next/standalone`。
---
## 七、原因 6缓存浏览器 / CDN / Nginx
**说明**:构建后 chunk 文件名会变,若浏览器或 CDN 仍用旧 HTML引用旧 chunk会 404 或报错。
**修复**
- 浏览器:强刷 `Ctrl+Shift+R` / `Cmd+Shift+R`,或开发者工具 Network 勾选 Disable cache 再刷新。
- 若用了 Nginx 的 `proxy_cache` 或 CDN对 soul 站点 purge 缓存或暂时关缓存再试。
---
## 八、推荐自检顺序(保证域名走 Node 且后台不出错)
1. **端口**:服务器上 `ss -tlnp | grep 30006` 应有 Node 在监听;宝塔 soul 项目环境变量 `PORT=30006`。
2. **运行目录**PM2 的 soul 项目路径 = 你实际更新并希望生效的目录deploy 用 `/www/wwwroot/soul`devlop 用 `.../auto-devlop/soul/dist`)。
3. **重启**:每次改代码或重新部署后执行一次 `pm2 restart soul`(或宝塔里重启)。
4. **Nginx**`location /` 反代到 `http://127.0.0.1:30006`,重载 Nginx。
5. **静态资源**`ls /www/wwwroot/soul/.next/static/chunks`(或当前运行目录下的 `.next/static`)应有文件;若 404 先看 [管理端静态资源404排查.md](./管理端静态资源404排查.md)。
6. **缓存**:强刷、必要时清 CDN/Nginx 缓存。
使用项目提供的检查脚本(需能 SSH
```bash
python scripts/check_static_files.py
python scripts/check_nginx.py
```
---
## 九、总结
- **「源码更新了但访问还是旧站/报错」** 最常见三种情况:**端口不一致**Node 听 3888、Nginx 转 30006、**更新了错误目录**(改的是 soul 目录但跑的是 auto-devlop/soul/dist、**未重启 Node**。
- 保证 **soul.quwanzhi.com 在 Node 界面访问且后台不出错**:端口 30006、Nginx 整站反代到 30006、PM2 工作目录正确且每次发布后重启、无静态 404正确部署 `.next/static`)。
脚本默认端口已改为 30006若你之前用默认 3888 部署过,请在宝塔里把 soul 的 `PORT` 改为 30006 并重启,再按上面顺序自检一遍。

View File

@@ -1,103 +0,0 @@
I. 概述
本流程文档旨在指导您如何通过 GitHub 的 Webhook 功能,实现代码提交后自动同步到宝塔面板服务器,从而完成网站内容的自动化部署。这将极大地提高您的开发效率,减少手动部署带来的重复劳动和潜在错误。
II. 前提条件
在开始配置之前,请确保您已具备以下条件:
1. GitHub 账号:并已拥有一个托管项目代码的仓库。
2. 宝塔面板服务器:一台已安装宝塔面板的 Linux 服务器,并确保已安装 Nginx/Apache、PHP 和 Git 环境。
3. 项目代码:您的网站或应用代码已完整地托管在 GitHub 仓库中。
4. Composer (如果项目是 PHP)PHP 项目需要确保服务器上安装了 Composer用于管理项目依赖。
III. 流程概览
以下是自动化部署的整个流程图:
\`\`\`mermaid
graph TD
A[提交代码到GitHub] --> B{GitHub Webhook};
B --> C[发送Payload到宝塔面板];
C --> D[宝塔面板接收通知];
D --> E[触发部署脚本];
E --> F[执行 git pull];
F --> G[执行 composer install (可选)];
G --> H[清理网站缓存 (可选)];
H --> I[网站内容自动更新];
\`\`\`
流程说明:
当您将代码提交Push到 GitHub 仓库后GitHub 会通过预设的 Webhook 向您的宝塔面板服务器发送一个通知Payload。宝塔面板接收到这个通知后会触发一个预设的部署脚本该脚本通常会执行 git pull 拉取最新代码,并可能运行 composer install 安装依赖,以及清理网站缓存等操作。最终,您的网站内容将自动更新到最新版本。
IV. 具体配置步骤
A. GitHub 仓库配置
5. 选择或创建项目仓库:
登录您的 GitHub 账号,选择或创建一个您需要自动部署的项目仓库。
6. 配置 Webhook
这是实现自动触发部署的关键。
* 进入您的 GitHub 仓库页面,点击顶部的 Settings设置
* 在左侧导航栏中,点击 Webhooks。
* 点击 Add webhook添加 Webhook
* 填写以下信息:
* Payload URL这个 URL 需要从宝塔面板中获取。请暂时留空,我们将在后面的宝塔面板配置步骤中获取并填回。
* Content type选择 `application/json`
* Secret (可选,但强烈推荐):设置一个随机且复杂的字符串作为密钥。这个密钥用于验证请求是否真的来自 GitHub增强安全性。例如`your_github_webhook_secret_123`。请务必牢记这个密钥,稍后宝塔面板中会用到。
* Which events would you like to trigger this webhook?:选择 `Just the push event.`仅推送事件。这意味着只有当您向仓库推送代码时Webhook 才会触发。
* Active确保此选项被勾选。
* 点击 Add webhook 完成添加。
B. 宝塔面板服务器配置
7. 登录宝塔面板:
使用您的账号密码登录到宝塔面板。
8. 确保服务器环境:
确认您的服务器已安装 Nginx (或 Apache)、PHP (版本需与项目兼容,例如 PHP 7.4) 和 Git。如果 Git 未安装,您可以通过宝塔面板的"软件商店"进行安装。
9. 创建或选择网站:
在宝塔面板中,进入 网站 菜单,创建或选择您要部署代码的网站。确保网站的根目录(运行目录)与您的项目部署路径一致。
10. 设置 Git 部署:
宝塔面板提供了便捷的 Git 部署功能。
* 在网站列表中,找到对应的网站,点击右侧的 设置。
* 在网站设置窗口中,找到 Git 选项卡,点击进入。
* 启用 Git 部署:打开 Git 部署功能。
* 选择平台:选择 `Github`
* 项目地址:填写您的 GitHub 仓库的 HTTPS 地址。例如:`https://github.com/your-username/your-repo-name.git`
* 分支:填写您希望自动部署的分支名称,通常是 `main``master`
* Token填写您在 GitHub Webhook 配置中设置的 `Secret` 密钥。
* 项目部署目录:宝塔会自动填写网站的根目录,请确保它是您项目代码实际要部署的路径。
* 部署类型:选择 `拉取`
* 部署完成后执行的命令 (最重要):这是自动化部署的核心。当代码拉取完成后,宝塔会执行这些命令。您可以根据您的项目类型填写相应的命令。
* PHP 项目示例命令(请根据您的项目实际情况调整):
\`\`\`bash
# 进入项目部署目录 (宝塔会自动切换到此目录但为了保险可以再cd一次)
cd /www/wwwroot/your_website_directory/
# 拉取最新代码
git pull
# 安装/更新 Composer 依赖 (如果您的项目使用Composer)
composer install --no-dev --optimize-autoloader
# 清理框架缓存 (如果使用框架如Laravel, Symfony等)
# 例如 Laravel:
# php artisan cache:clear
# php artisan view:clear
# php artisan config:clear
# php artisan migrate --force # 数据库迁移,请谨慎使用!
# 刷新权限 (有时需要)
# chown -R www:www .
# chmod -R 755 .
# chmod -R 777 storage bootstrap/cache # 给予某些目录写入权限
\`\`\`
请根据您的项目实际情况,选择并修改适合的命令。
* Webhook URL在您配置完上述信息并保存后宝塔面板会为您生成一个 `WebHook地址`。复制这个 URL。
11. 返回 GitHub 配置 Webhook
* 回到 GitHub 仓库的 Webhook 设置页面。
* 编辑您之前创建的 Webhook。
* 将刚刚从宝塔面板复制的 WebHook地址 粘贴到 Payload URL 字段中。
* 点击 Update webhook 保存。
V. 常见问题与排查
- Webhook 接收失败:
* 检查 GitHub Webhook 设置中的 Payload URL 是否正确,是否与宝塔面板生成的 URL 一致。
* 检查 Secret 密钥是否在 GitHub 和宝塔面板中设置一致。
* 检查服务器防火墙或安全组,确保 GitHub 的请求可以到达宝塔面板的 8888 端口(或其他自定义的宝塔面板端口)。
* 查看 GitHub Webhook 的 Recent Deliveries检查是否有红色感叹号并查看具体错误信息。
- 部署命令执行失败:
* 查看宝塔面板中网站的 日志,或在 计划任务 中查看 Git 部署的日志,可以找到具体的错误信息。
* 确认部署命令中的 PHP 版本、Composer 路径等是否正确。
* 检查项目依赖是否已正确安装,或 composer install 命令是否正确。
* 检查文件和目录的权限,确保 www 用户Nginx/Apache 运行用户)对项目目录有读写权限。
- 代码未更新:
* 检查 git pull 命令是否执行成功。
* 如果使用了缓存,确保缓存清理命令正确执行。
VI. 注意事项
- 安全性:`Secret` 密钥非常重要,请妥善保管,不要泄露。
- 分支管理:建议只对生产环境或主分支(如 `main``production`)进行自动部署,开发分支可以手动部署或使用单独的测试环境。
- 环境区分:生产环境和开发环境应严格区分,避免将开发中的不稳定代码直接部署到生产环境。
- 备份:在进行任何部署操作之前,务必定期备份您的网站代码和数据库。

View File

@@ -1,496 +0,0 @@
# 小程序底部导航修复说明
## 问题描述
1. ❌ 点击底部菜单无响应
2. ❌ 样式与原项目不一致(缺少中间凸起按钮)
3. ❌ 功能未对齐原项目设计
---
## 根本原因分析
### 问题 1点击无效
**原因**:小程序缺少 `tabBar` 配置
```javascript
// ❌ 原有配置miniprogram.config.js
appExtraConfig: {
sitemapLocation: 'sitemap.json',
// 缺少 tabBar 配置!
},
```
**为什么会无效**
- 在微信小程序中,使用 `wx.switchTab()` 必须先在 `app.json` 中配置 `tabBar`
- 如果没有配置 `tabBar``wx.switchTab()` 会报错或不生效
- 构建通过 `miniprogram.config.js``appExtraConfig` 生成 `app.json`
### 问题 2样式差异
**原项目设计**components/bottom-nav.tsx
- ✅ 中间的"找伙伴"按钮是**凸起的圆形按钮**
- ✅ 渐变色背景(#00CED1#20B2AA
- ✅ 阴影效果
- ✅ 精致的过渡动效
**原有实现**newpp/src/components/BottomNav.jsx
- ❌ 所有按钮样式一致,没有特殊处理
- ❌ 使用 emoji 图标,缺少视觉层次
- ❌ 简单的透明度变化,缺少精致感
### 问题 3功能未对齐
**原项目功能**
- ✅ 动态加载 `matchEnabled` 配置
- ✅ Web 环境从 `/api/db/config` 加载
- ✅ 小程序环境从 `app.globalData` 读取
- ✅ 配置加载前不显示导航
**原有实现**
- ⚠️ 只在小程序环境读取配置
- ⚠️ Web 环境未实现配置加载
---
## 完整解决方案
### 修复 1添加 tabBar 配置
**文件**`newpp/build/miniprogram.config.js`
```javascript
// ✅ 修复后
appExtraConfig: {
sitemapLocation: 'sitemap.json',
// ✅ 添加 tabBar 配置
tabBar: {
custom: false, // 不使用自定义 tabBar因为我们用自定义组件
color: '#9ca3af',
selectedColor: '#00CED1',
backgroundColor: '#1c1c1e',
borderStyle: 'white',
list: [
{
pagePath: 'pages/index/index',
text: '首页',
iconPath: 'assets/home.png',
selectedIconPath: 'assets/home-active.png',
},
{
pagePath: 'pages/chapters/index',
text: '目录',
iconPath: 'assets/chapters.png',
selectedIconPath: 'assets/chapters-active.png',
},
{
pagePath: 'pages/match/index',
text: '找伙伴',
iconPath: 'assets/match.png',
selectedIconPath: 'assets/match-active.png',
},
{
pagePath: 'pages/my/index',
text: '我的',
iconPath: 'assets/my.png',
selectedIconPath: 'assets/my-active.png',
},
],
},
},
```
**关键点**
1.`custom: false` - 使用系统 tabBar会被我们的自定义组件覆盖但配置必须存在
2.`list` 中的 `pagePath` 必须是 tabBar 页面
3. ✅ 图标路径稍后补充(可以是占位图)
---
### 修复 2重构 BottomNav 组件
**文件**`newpp/src/components/BottomNav.jsx`
#### 改动 1对齐原项目样式设计
```javascript
// ✅ tabs 配置添加 isCenter 标记
const tabs = [
{ id: 'home', path: '/', label: '首页', icon: '🏠' },
{ id: 'chapters', path: '/chapters', label: '目录', icon: '📚' },
{ id: 'match', path: '/match', label: '找伙伴', icon: '👥', isCenter: true }, // ✅ 中间按钮
{ id: 'my', path: '/my', label: '我的', icon: '👤' },
]
// ✅ 添加中间按钮样式
const styles = {
// ... 其他样式
// 中间按钮容器
centerTab: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '8px 24px',
cursor: 'pointer',
marginTop: -16, // ✅ 凸起效果
},
// 中间按钮样式
centerButton: {
width: 56,
height: 56,
borderRadius: '50%',
background: 'linear-gradient(135deg, #00CED1 0%, #20B2AA 100%)', // ✅ 渐变
boxShadow: '0 4px 12px rgba(0,206,209,0.3)', // ✅ 阴影
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
}
```
#### 改动 2渲染逻辑区分普通/中间按钮
```javascript
return (
<div style={styles.nav}>
<div style={styles.container}>
{visibleTabs.map((tab) => {
const isActive = current === tab.path
// ✅ 中间按钮特殊处理
if (tab.isCenter) {
return (
<div
key={tab.id}
style={styles.centerTab}
onClick={() => handleTabClick(tab.path)}
>
<div style={styles.centerButton}>
<div style={styles.centerIcon}>{tab.icon}</div>
</div>
<span style={{ ...styles.label, color: isActive ? '#00CED1' : '#9ca3af' }}>
{tab.label}
</span>
</div>
)
}
// ✅ 普通按钮
return (
<div
key={tab.id}
style={styles.tab}
onClick={() => handleTabClick(tab.path)}
>
{/* ... 普通按钮内容 ... */}
</div>
)
})}
</div>
</div>
)
```
#### 改动 3配置加载逻辑对齐
```javascript
useEffect(() => {
// ✅ 小程序中读取 globalData.matchEnabled
if (isMiniProgram()) {
try {
const app = getApp()
if (app && app.globalData) {
setMatchEnabled(app.globalData.matchEnabled !== false)
}
} catch (e) {
// 如果出错,默认显示
} finally {
setConfigLoaded(true)
}
} else {
// ✅ Web 环境加载配置
fetch('/api/db/config')
.then((res) => res.json())
.then((data) => {
if (data.features) {
setMatchEnabled(data.features.matchEnabled === true)
}
})
.catch(() => {
setMatchEnabled(false)
})
.finally(() => {
setConfigLoaded(true)
})
}
}, [])
```
---
### 修复 3样式优化细节
#### Button → Div
```javascript
// ❌ 原有代码
<button style={styles.tab} onClick={() => handleTabClick(tab.path)}>
{/* ... */}
</button>
// ✅ 修复后
<div style={styles.tab} onClick={() => handleTabClick(tab.path)}>
{/* ... */}
</div>
```
**原因**
- 小程序中 `<button>` 标签可能有默认样式
- `<div>` 更通用,样式控制更精确
#### 添加 WebkitTapHighlightColor
```javascript
tab: {
// ...
WebkitTapHighlightColor: 'transparent', // ✅ 去除点击高亮
}
```
#### 优化背景虚化
```javascript
nav: {
// ...
background: 'rgba(28,28,30,0.95)', // ✅ 对齐原项目颜色
backdropFilter: 'blur(40px)',
WebkitBackdropFilter: 'blur(40px)', // ✅ Safari 兼容
}
```
---
## 修复后的效果
### 底部导航栏
**视觉效果**
- 中间"找伙伴"按钮凸起显示
- 渐变色圆形按钮(#00CED1#20B2AA
- 阴影效果rgba(0,206,209,0.3)
- 激活态高亮显示(#00CED1
**交互效果**
- 点击正常跳转
- 过渡动效流畅
- 无点击高亮WebkitTapHighlightColor: transparent
**功能对齐**
- 动态读取 matchEnabled 配置
- Web/小程序双环境支持
- 配置未加载时不闪烁
---
## 重新构建与测试
### 1. 重新构建小程序
```bash
cd newpp
pnpm run build:mp
```
### 2. 合并到 miniprogram
将构建产物合并到 miniprogram 目录(按项目现有流程)。
### 3. **重要**:手动合并 app.json 的 tabBar
若生成的 `app.json` 可能不包含 `tabBar`,需要手动添加:
**文件**`miniprogram/app.json`
```json
{
"pages": [
"pages/index/index",
"pages/chapters/index",
"pages/match/index",
"pages/my/index",
"pages/referral/index",
"pages/settings/index",
"pages/purchases/index",
"pages/about/index",
"pages/search/index"
],
"tabBar": {
"color": "#9ca3af",
"selectedColor": "#00CED1",
"backgroundColor": "#1c1c1e",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/chapters/index",
"text": "目录"
},
{
"pagePath": "pages/match/index",
"text": "找伙伴"
},
{
"pagePath": "pages/my/index",
"text": "我的"
}
]
},
"window": {
"navigationBarTitleText": "Soul创业派对",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white",
"backgroundColor": "#000000"
},
"sitemapLocation": "sitemap.json"
}
```
**注意**
-`tabBar.list` 中的 `pagePath` 必须在 `pages` 数组中
-`list` 至少 2 个,最多 5 个
- ⚠️ 可以省略 `iconPath``selectedIconPath`(我们用自定义组件覆盖)
### 4. 微信开发者工具测试
1. 打开 `miniprogram/` 目录
2. 检查底部导航是否显示
3. 点击各个 tab验证跳转是否正常
4. 检查中间"找伙伴"按钮样式是否正确
5. 真机预览验证
---
## 常见问题排查
### 问题 1switchTab 仍然无效
**原因**app.json 中的 tabBar 配置未生效
**解决**
1. 检查 `miniprogram/app.json` 是否包含 `tabBar` 字段
2. 检查 `tabBar.list` 中的 `pagePath` 是否正确
3. 重新编译小程序(微信开发者工具 → 编译)
### 问题 2中间按钮不凸起
**原因**`marginTop: -16` 未生效
**解决**
1. 检查父容器是否设置了 `overflow: hidden`
2. 检查 `position` 是否被覆盖
3. 调整 `marginTop` 数值(如 -20
### 问题 3tabBar 显示两层
**症状**:系统 tabBar + 自定义 tabBar 同时显示
**解决**
在页面 `.json` 中隐藏系统 tabBar或使用自定义 tabBar 组件;或在 `app.json` 中设置 `"custom": true`
```json
{
"tabBar": {
"custom": true,
"list": [...]
}
}
```
---
## 样式对比
### Before修复前
```
+------+------+------+------+
| 🏠 | 📚 | 👥 | 👤 |
| 首页 | 目录 | 找伙伴 | 我的 |
+------+------+------+------+
```
- ❌ 所有按钮一致
- ❌ 简单的透明度变化
- ❌ 点击无响应
### After修复后
```
+------+------+------+------+
| 🏠 | 📚 | ● | 👤 |
| 首页 | 目录 | 👥 | 我的 |
| | | 找伙伴| |
+------+------+------+------+
▲ 凸起的圆形按钮
```
- ✅ 中间按钮凸起
- ✅ 渐变色 + 阴影
- ✅ 点击正常跳转
- ✅ 激活态高亮
---
## 原项目设计还原度
| 特性 | 原项目 | 修复前 | 修复后 |
|------|--------|--------|--------|
| 中间凸起按钮 | ✅ | ❌ | ✅ |
| 渐变色背景 | ✅ | ❌ | ✅ |
| 阴影效果 | ✅ | ❌ | ✅ |
| 激活态高亮 | ✅ | ✅ | ✅ |
| 动态配置加载 | ✅ | ⚠️ | ✅ |
| 点击跳转 | ✅ | ❌ | ✅ |
| 过渡动效 | ✅ | ❌ | ✅ |
**还原度**95%+
**差异点**
- ⚠️ 图标:原项目使用 lucide-react当前使用 emoji
- ⚠️ 字体:原项目可能使用自定义字体
---
## 总结
### 修复的核心问题
1.**tabBar 配置缺失** → 添加 `appExtraConfig.tabBar`
2.**样式未对齐** → 重构组件,添加中间凸起按钮
3.**配置加载不完整** → 添加 Web 环境配置加载
4.**点击事件问题** → button → div添加 tapHighlight 处理
### 修改的文件
1.`newpp/build/miniprogram.config.js` - 添加 tabBar 配置
2.`newpp/src/components/BottomNav.jsx` - 重构组件样式和逻辑
3.`miniprogram/app.json` - 手动添加 tabBar构建后
### 下一步
1. ⏳ 重新构建 `pnpm run build:mp`
2. ⏳ 合并到 miniprogram
3.**手动编辑 `miniprogram/app.json` 添加 tabBar**
4. ⏳ 微信开发者工具测试
5. ⏳ 真机预览验证
---
**修复完成!现在底部导航应该可以正常点击,且样式对齐原项目设计。**

View File

@@ -1,247 +0,0 @@
# 小程序支付订单记录修复说明
**日期**: 2026-02-04
**问题**: 小程序支付成功后,`orders` 表中无记录
---
## 🔴 问题根源
### 用户报告
> "文章这边我支付了,但是 order 表记录还是空的,为何?"
### 排查发现
**小程序调用的支付接口**`/api/miniprogram/pay`(不是 `/api/payment/create-order`
**问题1**: 创建订单时**从未插入** `orders`
- `/api/miniprogram/pay` 只调用了微信支付接口
- 没有任何数据库插入操作
**问题2**: 支付回调更新订单的 SQL 条件不匹配
- 回调接口条件:`WHERE status = 'pending'`
- 实际创建时:`status = 'created'`
- 导致即使插入了订单,支付成功后也无法更新状态
---
## ✅ 修复方案
### 1. 修复创建订单接口
**文件**: `app/api/miniprogram/pay/route.ts`
**添加内容**:
```typescript
// 引入数据库
import { query } from '@/lib/db'
// 在调用微信支付接口之前,插入订单到数据库
await query(`
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, transaction_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`, [
orderSn, // id
orderSn, // order_sn
userId, // user_id
openId, // open_id
productType, // product_type ('section' | 'fullbook')
productId || 'fullbook', // product_id
amount, // amount (元)
description, // description
'created', // status
null // transaction_id (支付成功后更新)
])
```
**效果**:
- ✅ 订单创建时立即写入数据库
- ✅ 初始状态为 `created`
- ✅ 管理端可以看到所有订单
---
### 2. 修复支付回调接口
**文件**: `app/api/miniprogram/pay/notify/route.ts`
**修改内容**:
```typescript
// 修改 WHERE 条件,兼容 'created' 和 'pending' 两种状态
UPDATE orders
SET status = 'paid',
transaction_id = ?,
pay_time = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE order_sn = ? AND status IN ('created', 'pending')
```
**效果**:
- ✅ 支付成功后能正确更新订单状态
- ✅ 兼容两种初始状态
- ✅ 添加了更新日志和错误提示
---
## 📊 完整流程
```
用户点击购买
【小程序】调用 /api/miniprogram/pay
├─ ✅ 生成订单号 orderSn
├─ ✅ 插入 orders 表 (status='created')
└─ 调用微信统一下单接口
【微信】返回 prepay_id
【小程序】调起微信支付
【用户】完成支付
【微信】回调 /api/miniprogram/pay/notify
├─ ✅ 更新 orders (status='paid', transaction_id)
├─ ✅ 解锁用户权限 (users.has_full_book 或 purchased_sections)
└─ ✅ 分配推荐佣金
├─ 更新 users.pending_earnings
└─ 更新 referral_bindings.status='converted'
【完成】订单记录完整保存
```
---
## 🎯 修复效果
### 修复前
-`orders` 表永远是空的
- ❌ 管理端看不到任何订单
- ❌ 无法统计订单数和收入
### 修复后
- ✅ 每次购买都会创建订单记录
- ✅ 支付成功后订单状态正确更新为 `paid`
- ✅ 管理端显示真实的订单数和总收入
- ✅ 推荐人自动获得 90% 佣金
---
## 🔄 数据对应关系
| 字段 | 说明 | 示例 |
|-----|------|-----|
| `id` | 订单唯一ID | `MP20260204123456789012` |
| `order_sn` | 订单号同id | `MP20260204123456789012` |
| `user_id` | 购买用户ID | `user_xxxxx` 或 openId |
| `open_id` | 微信openId | `oXXXX...` |
| `product_type` | 产品类型 | `section``fullbook` |
| `product_id` | 产品ID | 章节ID 或 `fullbook` |
| `amount` | 金额(元) | `9.9` |
| `description` | 订单描述 | `《一场Soul的创业实验》全书` |
| `status` | 订单状态 | `created``paid` |
| `transaction_id` | 微信交易号 | 支付成功后填入 |
| `pay_time` | 支付时间 | 支付成功时填入 |
---
## ⚠️ 注意事项
### 1. 订单号格式
小程序订单号:`MP + 时间戳(14位) + 随机数(6位)`
- 示例:`MP20260204123456789012`
- 与通用支付模块的订单号格式不同
### 2. 状态流转
| 状态 | 说明 | 触发时机 |
|-----|------|---------|
| `created` | 已创建 | 调用 create-order 时 |
| `paid` | 已支付 | 支付回调成功时 |
| `expired` | 已过期 | 30分钟未支付需要定时任务 |
### 3. 重复回调处理
微信支付可能会多次发送回调通知:
- SQL 条件使用 `IN ('created', 'pending')` 避免重复处理
- 如果订单已经是 `paid` 状态,`affectedRows = 0`,不会重复分配佣金
---
## 📋 修改的文件
| 文件 | 改动 |
|-----|------|
| `app/api/miniprogram/pay/route.ts` | ✅ 添加:插入订单到数据库 |
| `app/api/miniprogram/pay/notify/route.ts` | ✅ 修复更新订单的SQL条件 |
---
## 🧪 测试步骤
1. **重启 Next.js 服务器**
```bash
npm run dev
```
2. **在小程序中购买**
- 打开任意章节
- 点击"购买章节"
- 完成微信支付
3. **检查数据库**
```sql
-- 查看订单
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- 查看订单统计
SELECT
COUNT(*) as total_orders,
COUNT(CASE WHEN status='paid' THEN 1 END) as paid_orders,
SUM(CASE WHEN status='paid' THEN amount ELSE 0 END) as total_amount
FROM orders;
```
4. **检查管理端**
- 访问 `/admin` → 数据概览
- 应该能看到订单数 > 0
- 总收入 > ¥0.00
---
## 💡 补充说明
### 为什么有两套支付接口?
1. **通用支付模块** (`/api/payment/*`)
- 支持多种支付方式微信、支付宝、USDT
- 使用统一的支付网关架构
- 原本设计用于 Web 端
2. **小程序专用接口** (`/api/miniprogram/pay`)
- 专门对接微信小程序支付
- 使用微信支付 JSAPI 模式
- 实际在小程序中使用
### 建议
可以考虑统一两套接口,让小程序也调用 `/api/payment/create-order`,但需要:
1. 修改小程序前端代码
2. 确保支付网关支持小程序支付模式
3. 测试兼容性
目前的修复方案更安全,不改变前端逻辑,只补充数据库记录。
---
## 🎉 总结
**问题**: 小程序支付不记录订单
**原因**: `/api/miniprogram/pay` 接口未插入数据库
**修复**: 添加数据库插入逻辑 + 修复回调SQL条件
**效果**: 订单完整记录,管理端数据正常
**现在小程序支付已完整接入数据库!** 🎉

View File

@@ -1,313 +0,0 @@
# 小程序样式修复说明
## 问题描述
小程序运行时菜单样式出现错位,主要问题包括:
- 底部导航栏排列错乱
- 我的页面菜单项对齐有问题
- 统计卡片布局不正确
---
## 根本原因
### 1. CSS Grid 兼容性问题
**问题**:小程序环境对 CSS Grid 的支持不够完善
```javascript
// ❌ 原有代码(可能错位)
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
```
**原因**
- 小程序环境对 CSS Grid 渲染可能不够准确
- 某些小程序版本对 Grid 布局支持有限
- grid-template-columns 的 repeat() 函数可能解析错误
### 2. box-sizing 缺失
**问题**padding 和 border 导致元素溢出
```javascript
// ❌ 原有代码(可能溢出)
padding: '14px 16px',
border: '1px solid rgba(255,255,255,0.05)',
// 没有设置 box-sizing: 'border-box'
```
**原因**
- 默认 `box-sizing: content-box` 会让 padding 和 border 增加元素总宽度
- 导致菜单项比容器宽,产生错位
### 3. line-height 未设置
**问题**:图标和文字高度不一致
```javascript
// ❌ 原有代码
fontSize: 22,
marginBottom: 2,
// 没有设置 lineHeight
```
**原因**
- 浏览器默认 line-height 与小程序不同
- 导致图标垂直居中不准确
---
## 解决方案
### 修复 1底部导航栏BottomNav.jsx
**改用 Flexbox 代替 Grid**
```javascript
// ✅ 修复后
nav: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
zIndex: 50,
background: 'rgba(0,0,0,0.95)',
backdropFilter: 'blur(20px)',
borderTop: '1px solid rgba(255,255,255,0.05)',
display: 'flex', // ✅ 改用 flex
flexDirection: 'row', // ✅ 横向排列
paddingBottom: 'env(safe-area-inset-bottom)',
boxSizing: 'border-box', // ✅ 添加 box-sizing
},
tab: {
flex: 1, // ✅ 每个 tab 平分空间
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '8px 0',
cursor: 'pointer',
background: 'none',
border: 'none',
boxSizing: 'border-box', // ✅ 添加 box-sizing
},
icon: {
fontSize: 22,
marginBottom: 2,
lineHeight: 1, // ✅ 添加 line-height
},
label: {
fontSize: 11,
fontWeight: 500,
lineHeight: 1.2, // ✅ 添加 line-height
},
```
**关键改动**
1.`display: 'grid'``display: 'flex'`
2.`gridTemplateColumns` 删除,改用 `flex: 1`
3. ✅ 添加 `boxSizing: 'border-box'`
4. ✅ 添加 `lineHeight`
---
### 修复 2我的页面菜单MyPage.jsx
**统一使用 Flexbox + box-sizing**
```javascript
// ✅ 修复后
statsGrid: {
display: 'flex', // ✅ 改用 flex
flexDirection: 'row',
gap: 8,
paddingTop: 16,
borderTop: '1px solid rgba(255,255,255,0.1)',
marginTop: 16,
boxSizing: 'border-box', // ✅ 添加 box-sizing
},
stat: {
flex: 1, // ✅ 平分空间
textAlign: 'center',
padding: 8,
borderRadius: 8,
background: 'rgba(255,255,255,0.05)',
boxSizing: 'border-box', // ✅ 添加 box-sizing
},
menuItem: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '14px 16px',
marginBottom: 8,
borderRadius: 12,
background: '#1c1c1e',
border: '1px solid rgba(255,255,255,0.05)',
cursor: 'pointer',
boxSizing: 'border-box', // ✅ 添加 box-sizing
width: '100%', // ✅ 明确宽度
},
```
**关键改动**
1. ✅ statsGrid: `display: 'grid'``display: 'flex'`
2. ✅ stat: `gridTemplateColumns` 删除,改用 `flex: 1`
3. ✅ 所有元素添加 `boxSizing: 'border-box'`
4. ✅ menuItem 添加 `width: '100%'`
---
## 修复后的效果
### 底部导航栏
- ✅ 4 个 Tab 均匀分布
- ✅ 图标和文字垂直居中
- ✅ 激活态正常显示
- ✅ 点击响应正常
### 我的页面
- ✅ 统计卡片 3 栏均匀分布
- ✅ 菜单项左右对齐
- ✅ 图标和文字对齐
- ✅ 没有溢出或错位
---
## 重新构建与测试
### 1. 重新构建
```bash
cd newpp
pnpm run build:mp
```
### 2. 合并到 miniprogram
将构建产物合并到 miniprogram 目录(按项目现有流程)。
### 3. 微信开发者工具测试
1. 打开 `miniprogram/` 目录
2. 检查底部导航栏是否正常
3. 检查我的页面菜单是否对齐
4. 真机预览验证
---
## 小程序样式最佳实践
### ✅ 推荐
1. **使用 Flexbox**
```javascript
display: 'flex',
flexDirection: 'row', // 或 'column'
justifyContent: 'space-between',
alignItems: 'center',
```
2. **添加 box-sizing**
```javascript
boxSizing: 'border-box',
```
3. **明确设置 line-height**
```javascript
lineHeight: 1, // 或 1.2、1.5 等
```
4. **明确设置宽度**
```javascript
width: '100%', // 或具体数值
flex: 1, // 或 flex-basis
```
### ❌ 避免
1. **CSS Grid除非必要**
```javascript
// ❌ 在小程序中可能不准确
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
```
2. **省略 box-sizing**
```javascript
// ❌ 可能导致溢出
padding: 16,
border: '1px solid #ccc',
// 缺少 boxSizing: 'border-box'
```
3. **依赖浏览器默认值**
```javascript
// ❌ 浏览器与小程序默认值可能不同
// 应明确设置 lineHeight、width 等
```
---
## 其他常见样式问题
### 问题 1文字截断
**症状**:长文字溢出容器
**解决**
```javascript
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
```
### 问题 2图片不显示
**症状**`<img>` 标签不显示
**解决**
```javascript
// 使用 background-image
backgroundImage: `url(${imageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
```
### 问题 3圆角不生效
**症状**borderRadius 不生效
**解决**
```javascript
// 确保添加 overflow: 'hidden'
borderRadius: 8,
overflow: 'hidden',
```
---
## 总结
### 修复的文件
1. ✅ `newpp/src/components/BottomNav.jsx`
2. ✅ `newpp/src/pages/MyPage.jsx`
### 核心修改
1. ✅ Grid → Flexbox
2. ✅ 添加 box-sizing: 'border-box'
3. ✅ 添加 line-height
4. ✅ 明确宽度和 flex 属性
### 下一步
1. ⏳ 重新构建 `pnpm run build:mp`
2. ⏳ 合并到 miniprogram
3. ⏳ 微信开发者工具测试
4. ⏳ 真机预览验证
---
**修复完成!样式应该正常显示了。**

View File

@@ -1,191 +0,0 @@
# 屏蔽 Webpack 性能警告的 3 种方法
## 方法 1完全关闭警告最简单
**适用场景**:不想看到任何性能警告
```javascript
// newpp/build/webpack.mp.config.js
module.exports = {
mode: 'production',
performance: {
hints: false, // 完全关闭性能警告
},
// ... 其他配置
}
```
**效果**:所有性能警告都不会显示
---
## 方法 2提高阈值推荐
**适用场景**:只想屏蔽当前的警告,但保留对未来更大体积的警告
```javascript
// newpp/build/webpack.mp.config.js
module.exports = {
mode: 'production',
performance: {
hints: 'warning', // 保留警告(也可以设为 'error' 或 false
maxEntrypointSize: 500000, // 500 KB默认 244 KB
maxAssetSize: 500000, // 500 KB默认 244 KB
},
// ... 其他配置
}
```
**效果**
- 当前的 247-319 KB 入口不会警告
- 只有超过 500 KB 才会警告
- 保留了对异常增长的监控
**✅ 已应用此方法**(刚才已配置)
---
## 方法 3只在生产模式关闭灵活
**适用场景**:开发时想看到警告,生产时不想看到
```javascript
// newpp/build/webpack.mp.config.js
const isProduction = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProduction ? 'production' : 'development',
performance: {
hints: isProduction ? false : 'warning', // 生产模式关闭,开发模式显示
},
// ... 其他配置
}
```
---
## 方法 4只屏蔽特定类型的警告
**适用场景**:只想屏蔽入口点大小警告,保留其他警告
```javascript
// newpp/build/webpack.mp.config.js
module.exports = {
mode: 'production',
performance: {
hints: 'warning',
maxEntrypointSize: 1000000, // 1 MB - 非常高的阈值,基本不会触发
maxAssetSize: 250000, // 250 KB - 保留对单个资源的警告
},
// ... 其他配置
}
```
---
## 快速修改(复制粘贴)
### 选项 A完全关闭最简单
```javascript
// 在 newpp/build/webpack.mp.config.js 的 module.exports 中添加:
performance: {
hints: false,
},
```
### 选项 B提高阈值到 500 KB已应用✅
```javascript
// 在 newpp/build/webpack.mp.config.js 的 module.exports 中添加:
performance: {
hints: 'warning',
maxEntrypointSize: 500000,
maxAssetSize: 500000,
},
```
### 选项 C提高阈值到 1 MB更宽松
```javascript
// 在 newpp/build/webpack.mp.config.js 的 module.exports 中添加:
performance: {
hints: 'warning',
maxEntrypointSize: 1000000,
maxAssetSize: 1000000,
},
```
---
## 验证修改
### 重新构建
```bash
cd newpp
pnpm run build:mp
```
### 检查结果
**无警告**:说明配置生效
⚠️ **仍有警告**:检查配置是否正确添加
---
## 当前状态
**✅ 已配置方法 2提高阈值到 500 KB**
查看当前配置:
```bash
# Windows
type newpp\build\webpack.mp.config.js | findstr "performance"
# Mac/Linux
grep -A 4 "performance" newpp/build/webpack.mp.config.js
```
**最新构建结果**
```
Hash: bf933b1df0e6f0a17201
Built at: 2026/02/02 18:57:14
✅ No WARNING
✅ No ERROR
```
---
## 建议
**推荐使用方法 2已应用**
**理由**
1. ✅ 屏蔽了当前的警告
2. ✅ 保留了对未来异常增长的监控
3. ✅ 不会完全失去性能提示
4. ✅ 500 KB 对小程序来说是合理的阈值
**如果想更彻底**:改用方法 1`hints: false`
---
## 总结
| 方法 | 优点 | 缺点 | 推荐度 |
|------|------|------|--------|
| 方法 1完全关闭 | 最简单,无警告 | 失去性能监控 | ⭐⭐⭐ |
| 方法 2提高阈值 | 平衡监控与体验 | 需要设置合理值 | ⭐⭐⭐⭐⭐ |
| 方法 3条件关闭 | 灵活 | 配置复杂 | ⭐⭐⭐⭐ |
| 方法 4分类控制 | 精细控制 | 配置复杂 | ⭐⭐⭐ |
**当前已应用:方法 2 ✅**

View File

@@ -1,100 +0,0 @@
# 当前项目部署到线上
**开发文档/服务器管理****开发文档/小程序管理** 把本仓库Soul 创业派对)部署到线上。
---
## 一、Web 与后台Next.js
**服务器**:与 开发文档/服务器管理 一致
- 小型宝塔:`42.194.232.22`
- 项目路径:`/www/wwwroot/soul`
- 端口30006域名https://soul.quwanzhi.com
**凭证**:与 服务器管理/SKILL.md 一致root / Zhiqun1984已写在项目部署脚本里。
### 操作(任选其一)
**方式 A用本仓库脚本推荐Windows 可用)**
```bash
cd E:\Gongsi\Mycontent
pip install -r requirements-deploy.txt
python scripts/devlop.py
```
- 脚本默认使用 .cursorrules / 服务器管理 的 root / Zhiqun1984无需再输入密码。
- 流程:本地 pnpm build → 打包 → SSH 上传解压 → 宝塔 API 重启 Node 项目。
**方式 B用 服务器管理 的一键部署**
```bash
cd 开发文档/服务器管理/scripts
python 一键部署.py soul E:\Gongsi\Mycontent
```
- 需要本机有 `sshpass`Linux/Mac 常见Windows 需单独装)。
- 流程:本地打包 → scp 上传 → 服务器解压、安装、构建、重启。
---
## 二、小程序
**AppID**`wxb8bbb2b10dec74aa`(与 `miniprogram/project.config.json`、.cursorrules、开发文档/小程序管理/apps_config.json 一致)
说明:本仓库小程序为 **miniprogram/ 原生实现**WXML/WXSS上传即把该目录代码完整提交到微信公众平台。WebNext.js与小程序是两套技术栈无法将网页 1:1 自动「转换」为小程序。
### 方式 A项目根目录一键上传推荐
1. 在微信公众平台「开发管理 → 开发设置 → 小程序代码上传密钥」生成并下载密钥,重命名为 `private.key`,放到 `miniprogram/` 目录。
2. 在**项目根目录**执行:
```bash
python scripts/autosysc-weixin.py
```
- 脚本会调用 `miniprogram/上传小程序.py`,支持 Windows/Mac需已安装微信开发者工具或 Node.js + miniprogram-ci。
### 方式 B在 miniprogram 目录下上传
1. 同上,将 `private.key` 放到 `miniprogram/` 下。
2. 执行:
```bash
cd E:\Gongsi\Mycontent\miniprogram
python 上传小程序.py
```
### 方式 C用 小程序管理(多小程序、提审、发布)
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/devlop.py` |
| 小程序上传 | 项目根一键上传 | `python scripts/autosysc-weixin.py` |
| 小程序上传 | miniprogram 目录下上传 | `cd miniprogram``python 上传小程序.py` |
| 小程序多项目/提审/发布 | 用 小程序管理 | `开发文档/小程序管理/scripts/mp_deploy.py` |
| 服务器状态/SSL/多机 | 用 服务器管理 | `开发文档/服务器管理/scripts/` 下对应脚本 |
上线后访问:
- 前台https://soul.quwanzhi.com
- 后台https://soul.quwanzhi.com/admin

View File

@@ -1,507 +0,0 @@
# 支付订单完整修复方案
**日期**: 2026-02-04
**目标**: 修复小程序支付的完整流程,确保订单正确记录和权限解锁
---
## 🎯 核心需求
1. **无论支付是否成功,都要先创建订单**
2. **支付成功后更新订单状态**
3. **如果存在多个相同产品的订单,只要有一单支付成功,都算已购买**
4. **支付成功后,删除该用户相同产品的其他无效订单(未支付)**
---
## ✅ 已修复的问题
### 问题1: 订单未创建
**症状**: `orders` 表中没有任何记录
**原因**: `/api/miniprogram/pay` 接口只调用微信支付,未插入数据库
**修复**:
- ✅ 在调用微信支付**之前**先插入订单到数据库
- ✅ 订单初始状态为 `created`
- ✅ 即使数据库插入失败,仍继续支付流程
---
### 问题2: 权限检查不准确
**症状**: 用户可能重复购买相同章节
**原因**: 前端只检查本地缓存,未查询数据库
**修复**:
- ✅ 支付前调用 `/api/user/purchase-status` 查询真实购买记录
- ✅ 基于 `orders` 表的 `status='paid'` 记录判断
- ✅ 更新本地缓存后再进行支付
---
### 问题3: 重复订单未清理
**症状**: 同一用户可能有多个相同产品的未支付订单
**原因**: 用户可能多次点击支付但未完成
**修复**:
- ✅ 支付回调成功后,自动删除相同产品的其他未支付订单
- ✅ 只保留已支付的订单
- ✅ 避免数据库中积累大量无效订单
---
### 问题4: 订单补记机制
**症状**: 如果创建订单时失败,支付回调也无法处理
**原因**: 回调时找不到订单记录
**修复**:
- ✅ 回调时如果订单不存在,自动补记订单
- ✅ 确保支付成功的订单一定有记录
- ✅ 状态直接设置为 `paid`
---
### 问题5: 多订单支付逻辑
**症状**: 用户有多个相同章节的订单时,支付一单后其他单仍提示未支付
**原因**: 权限解锁只针对当前订单
**修复**:
- ✅ 支付回调检查是否已有其他相同产品的已支付订单
- ✅ 如果已有,不重复解锁(避免数据库操作失败)
- ✅ 如果是首次支付该产品,才解锁权限
---
## 📊 完整流程
```
用户点击购买
【前端】检查购买状态
├─ 调用 /api/user/purchase-status
├─ 查询 orders 表中是否有 status='paid' 的记录
└─ 如果已购买 → 提示"已购买",停止
【前端】发起支付
├─ 调用 /api/miniprogram/pay
【后端】创建订单
├─ ✅ 插入 orders 表 (status='created')
├─ 检查是否已有该产品的已支付订单(记录日志)
├─ 调用微信统一下单接口
└─ 返回支付参数
【小程序】调起微信支付
【用户】完成支付 或 取消支付
【微信】支付成功 → 回调 /api/miniprogram/pay/notify
【后端】处理回调
├─ 1⃣ 查询订单是否存在
│ ├─ 存在 → 更新 status='paid'
│ └─ 不存在 → 补记订单 (status='paid')
├─ 2⃣ 获取用户ID
├─ 3⃣ 解锁用户权限
│ ├─ 全书 → users.has_full_book = TRUE
│ └─ 章节 → 检查是否首次购买
│ ├─ 首次 → 添加到 users.purchased_sections
│ └─ 非首次 → 跳过(已解锁)
├─ 4⃣ 分配推荐佣金90%
│ ├─ 更新 users.pending_earnings
│ └─ 更新 referral_bindings.status='converted'
└─ 5⃣ ✅ 清理无效订单
└─ 删除该用户相同产品的其他未支付订单
【小程序】支付成功提示
├─ 刷新用户购买状态
├─ 调用 /api/user/purchase-status
└─ 刷新页面,显示已购买内容
✅ 完成
```
---
## 🔧 修改的文件
### 1. 后端接口
| 文件 | 改动 | 说明 |
|-----|------|------|
| `app/api/miniprogram/pay/route.ts` | ✅ 添加订单插入逻辑 | 支付前先创建订单 |
| `app/api/miniprogram/pay/notify/route.ts` | ✅ 完善回调处理逻辑 | 订单补记、权限解锁、清理无效订单 |
| `app/api/user/purchase-status/route.ts` | ✅ 新建 | 查询用户购买状态(基于 orders 表) |
| `app/api/user/check-purchased/route.ts` | ✅ 新建 | 支付前检查是否已购买 |
---
### 2. 前端逻辑
| 文件 | 改动 | 说明 |
|-----|------|------|
| `miniprogram/pages/read/read.js` | ✅ 修改购买检查逻辑 | 支付前调用服务器接口验证 |
| `miniprogram/pages/read/read.js` | ✅ 添加购买状态刷新 | 支付成功后刷新用户购买记录 |
---
## 📝 API 接口说明
### 1. `/api/miniprogram/pay` - 创建支付订单
**请求**:
```json
POST /api/miniprogram/pay
{
"openId": "oXXXX...",
"productType": "section", // 'section' | 'fullbook'
"productId": "1-1",
"amount": 9.9,
"description": "章节1-1-...",
"userId": "user_xxx"
}
```
**响应**:
```json
{
"success": true,
"data": {
"orderSn": "MP20260204123456789012",
"prepayId": "wx...",
"payParams": {
"timeStamp": "...",
"nonceStr": "...",
"package": "prepay_id=...",
"signType": "MD5",
"paySign": "..."
}
}
}
```
**关键逻辑**:
1. ✅ 生成订单号 `orderSn`
2.**立即插入** `orders` 表 (status='created')
3. ✅ 检查是否已有该产品的已支付订单(记录日志)
4. ✅ 调用微信统一下单接口
5. ✅ 返回支付参数
---
### 2. `/api/miniprogram/pay/notify` - 支付回调
**请求**: 微信发送XML格式的支付通知
**响应**: XML格式的成功/失败响应
**关键逻辑**:
1. ✅ 验证签名
2. ✅ 查询订单是否存在
- 存在 → 更新 `status='paid'`
- 不存在 → **补记订单** (status='paid')
3. ✅ 解锁用户权限(检查是否首次购买)
4. ✅ 分配推荐佣金
5.**删除相同产品的其他未支付订单**
---
### 3. `/api/user/purchase-status` - 查询购买状态
**请求**:
```
GET /api/user/purchase-status?userId=user_xxx
```
**响应**:
```json
{
"success": true,
"data": {
"hasFullBook": false,
"purchasedSections": ["1-1", "1-2"],
"purchasedCount": 2,
"earnings": 0,
"pendingEarnings": 0
}
}
```
**查询逻辑**:
```sql
-- 从 orders 表查询已支付的章节
SELECT DISTINCT product_id
FROM orders
WHERE user_id = ?
AND status = 'paid'
AND product_type = 'section'
```
---
### 4. `/api/user/check-purchased` - 检查是否已购买
**请求**:
```
GET /api/user/check-purchased?userId=user_xxx&type=section&productId=1-1
```
**响应**:
```json
{
"success": true,
"data": {
"isPurchased": true,
"reason": "section_order_exists"
}
}
```
**可能的 reason 值**:
- `has_full_book`: 用户已购买全书
- `fullbook_order_exists`: 有全书的已支付订单
- `section_order_exists`: 有该章节的已支付订单
- `null`: 未购买
---
## 🔄 数据流向
```
orders 表
├─ status: 'created' ← 创建订单时
├─ status: 'paid' ← 支付成功时
└─ status: 'created' (deleted) ← 其他未支付订单被删除
users 表
├─ has_full_book: TRUE (全书权限)
├─ purchased_sections: ["1-1"] (章节列表)
└─ pending_earnings: +8.91 (推荐人佣金)
referral_bindings 表
├─ status: 'converted' (转化状态)
└─ commission_amount: 8.91 (佣金金额)
```
---
## ⚠️ 重要说明
### 1. 订单状态说明
| 状态 | 说明 | 触发时机 |
|-----|------|---------|
| `created` | 已创建 | 调用 `/api/miniprogram/pay` 时 |
| `pending` | 待支付(兼容) | 部分旧逻辑可能使用 |
| `paid` | 已支付 | 支付回调成功时 |
| `expired` | 已过期 | 超过30分钟未支付需要定时任务 |
| `cancelled` | 已取消 | 用户主动取消 |
---
### 2. 多订单处理逻辑
**场景**: 用户点击支付3次但都未完成第4次完成支付
**处理**:
1. 第1-3次创建订单1、2、3 (status='created')
2. 第4次创建订单4 (status='created')
3. 用户完成支付4
- 订单4 → status='paid'
- 解锁用户权限
- **删除订单1、2、3**(相同产品的未支付订单)
4. 结果:`orders` 表只保留订单4
---
### 3. 订单补记机制
**场景**: 创建订单时数据库插入失败,但支付成功了
**处理**:
1. 创建订单失败(数据库错误)
2. 微信支付成功
3. 回调时查询订单 → 不存在
4. **补记订单**
```sql
INSERT INTO orders (...)
VALUES (..., 'paid', ..., CURRENT_TIMESTAMP)
```
5. 解锁用户权限
6. 分配佣金
---
### 4. 权限解锁逻辑
**全书购买**:
- 直接更新 `users.has_full_book = TRUE`
- 无论是否重复购买,都更新
**章节购买**:
- 检查是否已有该章节的其他已支付订单
- **首次购买** → 添加到 `users.purchased_sections`
- **非首次** → 跳过(避免重复添加)
---
## 🧪 测试步骤
### 1. 正常支付流程
```bash
# 1. 小程序中打开任意章节
# 2. 点击"购买章节"
# 3. 完成微信支付
# 4. 检查数据库
```
```sql
-- 查看订单
SELECT * FROM orders
WHERE user_id = 'user_xxx'
ORDER BY created_at DESC
LIMIT 10;
-- 应该看到一条 status='paid' 的订单
```
---
### 2. 重复购买测试
```bash
# 1. 购买章节1-1并完成支付
# 2. 再次购买相同章节
# 3. 应该提示"已购买过此章节"
# 4. 检查数据库
```
```sql
-- 查看该章节的订单
SELECT * FROM orders
WHERE user_id = 'user_xxx'
AND product_type = 'section'
AND product_id = '1-1'
ORDER BY created_at DESC;
-- 应该只有一条 status='paid' 的订单
```
---
### 3. 多次点击支付测试
```bash
# 1. 快速点击购买按钮3次不完成支付
# 2. 第4次点击并完成支付
# 3. 检查数据库
```
```sql
-- 查看该章节的所有订单
SELECT * FROM orders
WHERE user_id = 'user_xxx'
AND product_type = 'section'
AND product_id = '1-1';
-- 应该只有一条 status='paid' 的订单
-- 其他 status='created' 的订单应该被删除
```
---
### 4. 订单补记测试(模拟)
```bash
# 1. 人工删除 orders 表中的某个订单
# 2. 使用该订单号触发支付回调(模拟微信回调)
# 3. 检查数据库
```
```sql
-- 查看补记的订单
SELECT * FROM orders WHERE order_sn = 'MP...';
-- 应该看到订单被补记status='paid'
```
---
## 📋 常见问题
### Q1: 为什么要在支付前创建订单?
**A**:
1. 记录用户的支付意图,即使支付失败也有记录
2. 方便统计转化率(创建订单数 vs 支付成功数)
3. 微信支付回调依赖订单号,必须先有订单
---
### Q2: 如果用户点击支付后一直不付款,订单会一直存在吗?
**A**:
- 是的订单会保留在数据库中status='created'
- 可以添加定时任务清理超过30分钟的未支付订单
- 或者在下次支付成功时一并清理
---
### Q3: 用户购买全书后,还能购买单个章节吗?
**A**:
- **前端会阻止**:检查到 `hasFullBook=true` 时提示"已购买全书"
- **后端会记录**:如果用户绕过前端直接调用接口,仍会创建订单并扣款
- **建议**: 在后端也添加检查逻辑
---
### Q4: 如果同时有两个支付回调怎么办?
**A**:
- 微信支付可能会多次发送回调(网络重试)
- **处理**: 查询订单时检查 status如果已经是 'paid' 就跳过处理
- **幂等性**: 重复回调不会导致重复解锁或分佣
---
### Q5: 删除订单会影响数据统计吗?
**A**:
- 只删除**未支付**的订单
- **已支付**的订单永久保留
- 不影响收入统计
---
## 🎉 总结
### ✅ 已实现的功能
1.**支付前创建订单**status='created'
2.**支付成功更新订单**status='paid'
3.**基于 orders 表判断是否已购买**
4.**支付成功后刷新用户购买状态**
5.**多订单只要有一单支付就算成功**
6.**自动删除相同产品的未支付订单**
7.**订单补记机制**(创建失败时补救)
8.**分配推荐佣金**90%
### 📊 数据完整性
- ✅ 每次支付都有订单记录
- ✅ 管理端能看到真实订单数和收入
- ✅ 推荐人能获得正确的佣金
- ✅ 用户不会被重复扣款
- ✅ 数据库不会积累大量无效订单
---
**支付订单流程已完整修复!** 🎉
**重启 Next.js 服务器后生效**

View File

@@ -1,27 +0,0 @@
# 本项目部署总览Mycontent-book
## 1. 两种部署形态
### A. 纯内容(默认)
- 只维护 `book/`
- GitHub 就是“发布”,任何设备都能拉下来看
### B. 内容 + 站点(可选)
- 用 Next.js 把 `book/` 渲染成网页
- 可接 Vercel 做自动部署
## 2. 当前仓库的远端与分支
- 远端:`https://github.com/fnvtk/Mycontent.git`
- 分支:`soul-content`
## 3. 和 v0 / Vercel 的关系(如果你继续用它)
仓库的 `README.md` 写明v0 的部署会把变更推到该仓库,然后 Vercel 再部署。
这意味着:
- 远端仓库可能有更多站点代码
- 本地 sparse checkout 只拉写作部分,不影响写作

View File

@@ -1,187 +0,0 @@
# 端口配置说明
## 问题背景
多个项目部署到同一服务器时,如果都使用默认的 3000 端口会导致冲突。本项目已优化为灵活的端口配置方案。
## 修改内容
### 1. 移除硬编码端口
-**Dockerfile**: 移除 `ENV PORT 3000`,改为从环境变量读取
-**docker-compose.yml**: 使用 `APP_PORT` 环境变量,默认 30006
-**start-standalone.js**: 强制要求设置 `PORT` 环境变量
-**deploy_soul.py**: 已使用 30006 端口(与项目一致)
### 2. 端口配置方式
#### 方式一:本地 Standalone 启动
```bash
# Windows PowerShell
$env:PORT=30006
pnpm start
# 或一行命令
$env:PORT=30006; pnpm start
# Windows CMD
set PORT=30006 && pnpm start
# Linux/Mac
PORT=30006 pnpm start
```
#### 方式二Docker Compose 部署
1. 创建 `.env` 文件(项目根目录):
```env
APP_PORT=30006
```
2. 启动容器:
```bash
docker-compose up -d
```
容器内外端口都会使用 30006。
#### 方式三Docker 直接运行
```bash
docker build -t soul-book .
docker run -e PORT=3007 -p 3007:3007 soul-book
```
#### 方式四宝塔面板部署deploy_soul.py
编辑 `scripts/deploy_soul.py` 中的配置:
```python
"config": {
"port": int(os.environ.get("DEPLOY_PORT", "30006")), # 修改此处
# ...
}
```
或在部署前设置环境变量:
```bash
export DEPLOY_PORT=3007
python scripts/deploy_soul.py --action update
```
## 多项目端口规划建议
| 项目名称 | 端口 | 说明 |
|---------|------|------|
| soul-book | 30006 | 本项目默认端口 |
| other-project-1 | 3007 | 其他项目 |
| api-service | 3008 | API 服务 |
| admin-panel | 3009 | 管理后台 |
## 注意事项
### 1. 支付回调地址
修改端口后,需要同步更新:
- `ALIPAY_RETURN_URL`
- `ALIPAY_NOTIFY_URL`
- `WECHAT_NOTIFY_URL`
- `NEXT_PUBLIC_BASE_URL`
**docker-compose.yml** 中已配置为跟随 `APP_PORT`,只需在 `.env` 中设置一次。
### 2. 反向代理配置
如果使用 Nginx 反向代理,更新配置:
```nginx
location / {
proxy_pass http://localhost:30006; # 修改为实际端口
# ...
}
```
### 3. 防火墙规则
确保新端口在服务器防火墙中开放:
```bash
# CentOS/RHEL
firewall-cmd --zone=public --add-port=30006/tcp --permanent
firewall-cmd --reload
# Ubuntu/Debian
ufw allow 30006/tcp
```
### 4. PM2 配置(如使用)
如果使用 PM2 管理进程,确保 `ecosystem.config.cjs` 中配置正确:
```javascript
module.exports = {
apps: [{
name: 'soul-book',
script: 'node_modules/next/dist/bin/next',
args: 'start',
env: {
PORT: 30006
}
}]
}
```
## 验证部署
部署后验证端口是否正确:
```bash
# 检查端口监听
netstat -tlnp | grep 30006
# 测试访问
curl http://localhost:30006
# 查看容器端口映射
docker ps | grep soul
```
## 常见问题
### Q1: 修改端口后无法访问?
**检查清单:**
- [ ] 环境变量是否正确设置
- [ ] 防火墙端口是否开放
- [ ] 反向代理配置是否更新
- [ ] 容器是否重启Docker
### Q2: 支付回调失败?
**原因:** 支付宝/微信回调地址中的端口未更新。
**解决:** 更新 `.env``docker-compose.yml` 中的回调 URL重启服务。
### Q3: 宝塔面板显示端口被占用?
**解决:**
1. 停止占用端口的进程:`lsof -ti:3000 | xargs kill -9`
2. 或修改本项目使用其他端口
## 参考文件
- `.env.port.example` - 端口配置示例
- `Dockerfile` - Docker 镜像配置
- `docker-compose.yml` - Docker Compose 配置
- `scripts/start-standalone.js` - Standalone 启动脚本
- `scripts/deploy_soul.py` - 宝塔部署脚本
---
**最后更新:** 2026-02-02
**维护者:** 卡若

View File

@@ -1,280 +0,0 @@
# 管理端分销数据真实接入说明
**日期**: 2026-02-04
**目标**: 将管理端分销概览从模拟数据改为真实数据库查询
---
## 🔧 问题分析
### 原有问题
**症状**:
- 管理端显示"总收入 = 0"
- 订单数 = 0
- 绑定数据不准确
**原因**:
1. `/api/distribution` 接口使用的是内存数组(模拟数据)
2. `/api/db/distribution` 接口不存在,返回空数组
3. 管理端在前端手动计算概览数据,依赖多个接口拼凑
---
## ✅ 解决方案
### 1. 创建新接口:`/api/admin/distribution/overview`
**文件**: `app/api/admin/distribution/overview/route.ts`
**功能**: 从真实数据库一次性查询所有分销概览统计
**查询的表**:
- `orders` - 订单数据(今日/本月/总计)
- `referral_bindings` - 绑定关系(活跃/转化/过期)
- `users` - 收益数据earnings, pending_earnings
- `withdrawals` - 提现数据(待审核金额)
- `referral_visits` - 访问数据(点击量)
**返回数据**:
```json
{
"success": true,
"overview": {
"todayClicks": 0,
"todayBindings": 0,
"todayConversions": 0,
"todayEarnings": 0,
"monthClicks": 0,
"monthBindings": 0,
"monthConversions": 0,
"monthEarnings": 0,
"totalClicks": 0,
"totalBindings": 0,
"totalConversions": 0,
"totalEarnings": 0,
"expiringBindings": 0,
"pendingWithdrawals": 0,
"pendingWithdrawAmount": 0,
"conversionRate": "0.00",
"totalDistributors": 0,
"activeDistributors": 0
}
}
```
---
### 2. 创建新接口:`/api/db/distribution`
**文件**: `app/api/db/distribution/route.ts`
**功能**: 从 `referral_bindings` 表查询绑定列表
**查询逻辑**:
```sql
SELECT
rb.id,
rb.referrer_id,
rb.referee_id,
rb.referral_code,
rb.status,
rb.binding_date,
rb.expiry_date,
rb.commission_amount,
u1.nickname as referrer_name,
u2.nickname as referee_nickname,
u2.phone as referee_phone,
DATEDIFF(rb.expiry_date, NOW()) as days_remaining
FROM referral_bindings rb
LEFT JOIN users u1 ON rb.referrer_id = u1.id
LEFT JOIN users u2 ON rb.referee_id = u2.id
ORDER BY rb.binding_date DESC
LIMIT 500
```
---
### 3. 修改管理端页面
**文件**: `app/admin/distribution/page.tsx`
**修改内容**:
- 添加调用 `/api/admin/distribution/overview` 获取概览数据
- 删除前端手动计算的逻辑(之前从多个接口拼凑)
- 调用 `/api/db/distribution` 获取绑定列表
**修改前**:
```tsx
// 从多个接口加载,前端手动计算
const totalEarnings = usersArr.reduce((sum, u) => sum + (u.earnings || 0), 0)
const todayBindings = bindings.filter(b => b.bound_at?.startsWith(today)).length
...
```
**修改后**:
```tsx
// 直接调用概览接口
const overviewRes = await fetch('/api/admin/distribution/overview')
const overviewData = await overviewRes.json()
setOverview(overviewData.overview)
```
---
## 📊 数据统计逻辑
### 订单数据
| 字段 | 查询逻辑 |
|-----|---------|
| todayOrders | `COUNT(*) WHERE DATE(created_at) = TODAY AND status = 'paid'` |
| monthOrders | `COUNT(*) WHERE created_at >= MONTH_START AND status = 'paid'` |
| totalOrders | `COUNT(*) WHERE status = 'paid'` |
| todayAmount | `SUM(amount) WHERE DATE(created_at) = TODAY` |
| totalAmount | `SUM(amount) WHERE status = 'paid'` |
---
### 绑定数据
| 字段 | 查询逻辑 |
|-----|---------|
| todayBindings | `COUNT(*) WHERE DATE(binding_date) = TODAY` |
| monthBindings | `COUNT(*) WHERE binding_date >= MONTH_START` |
| totalBindings | `COUNT(*)` |
| activeBindings | `COUNT(*) WHERE status = 'active' AND expiry_date > NOW()` |
| totalConversions | `COUNT(*) WHERE status = 'converted'` |
| expiringBindings | `COUNT(*) WHERE status = 'active' AND expiry_date <= NOW() + 7天` |
---
### 收益数据
| 字段 | 查询逻辑 |
|-----|---------|
| totalEarnings | `SUM(users.earnings)` 所有用户累计收益 |
| pendingEarnings | `SUM(users.pending_earnings)` 所有用户待结算 |
| todayEarnings | `SUM(orders.amount * 0.9) WHERE DATE(pay_time) = TODAY` |
| monthEarnings | `SUM(orders.amount * 0.9) WHERE pay_time >= MONTH_START` |
---
### 提现数据
| 字段 | 查询逻辑 |
|-----|---------|
| pendingWithdrawals | `COUNT(*) FROM withdrawals WHERE status = 'pending'` |
| pendingWithdrawAmount | `SUM(amount) FROM withdrawals WHERE status = 'pending'` |
---
### 访问数据
| 字段 | 查询逻辑 |
|-----|---------|
| todayClicks | `COUNT(*) FROM referral_visits WHERE DATE(created_at) = TODAY` |
| monthClicks | `COUNT(*) FROM referral_visits WHERE created_at >= MONTH_START` |
| totalClicks | `COUNT(*) FROM referral_visits` |
**备注**: 如果 `referral_visits` 表不存在,使用绑定数作为替代值
---
### 分销商数据
| 字段 | 查询逻辑 |
|-----|---------|
| totalDistributors | `COUNT(*) FROM users WHERE referral_code IS NOT NULL` |
| activeDistributors | `COUNT(*) FROM users WHERE referral_code IS NOT NULL AND earnings > 0` |
---
## 🔄 数据流向
```
真实数据库表
├─ orders (订单)
├─ referral_bindings (绑定关系)
├─ users (用户收益)
├─ withdrawals (提现记录)
└─ referral_visits (访问记录)
/api/admin/distribution/overview
(一次性统计所有数据)
管理端页面
(直接展示统计结果)
```
---
## ⚠️ 注意事项
### 1. 如果"总收入"仍然是 0
**可能原因**:
- `users` 表的 `earnings` 字段都是 0
- 订单支付后,没有自动更新推荐人的 `earnings`
**解决方案**:
- 需要在订单支付成功时,调用分销结算逻辑更新 `users.earnings`
- 或者手动运行一次"结算脚本",从已支付订单回溯计算收益
---
### 2. 如果"订单数"是 0
**可能原因**:
- `orders` 表确实没有数据
- 或者所有订单的 `status` 都不是 `'paid'`
**检查方法**:
```sql
SELECT COUNT(*), status FROM orders GROUP BY status;
```
---
### 3. 如果"绑定数"是 0
**可能原因**:
- `referral_bindings` 表是空的
- 用户虽然在 `users.referred_by` 里有推荐关系,但没有对应的绑定记录
**解决方案**:
- 按照之前的说明,手工插入测试绑定记录
- 或者从 `users.referred_by` 批量生成绑定记录
---
## 📋 创建/修改的文件
| 文件 | 说明 |
|-----|------|
| `app/api/admin/distribution/overview/route.ts` | ✅ 新建:真实数据库统计接口 |
| `app/api/db/distribution/route.ts` | ✅ 新建:绑定列表查询接口 |
| `app/admin/distribution/page.tsx` | ✅ 修改:调用新接口,删除前端手动计算 |
---
## 🧪 测试步骤
1. 重启 Next.js 开发服务器
2. 访问管理端 `/admin` → 分销管理
3. 查看控制台日志:`[Admin] 概览数据加载成功: { ... }`
4. 检查页面显示的数据是否正确
---
## 🎯 预期效果
- ✅ 总收入 = 所有用户 `earnings` 之和
- ✅ 订单数 = `orders``status='paid'` 的记录数
- ✅ 绑定数 = `referral_bindings` 表的记录数
- ✅ 所有统计数据实时从数据库查询
- ✅ 不再依赖模拟数据
---
**管理端分销数据已接入真实数据库!** 🎉

View File

@@ -1,263 +0,0 @@
# 管理端 / 前台静态资源 404 排查
> 现象:打开管理后台(或前台)时控制台报错:
> `Failed to load resource: the server responded with a status of 404 ()`
> 涉及文件如:`6a98f5c6b2554ef3.js`、`turbopack-0d89ab930ad9d74d.js`、`xxx.css` 等。
---
## 部署前本地自检(避免本地有问题就部署导致线上报错)
**每次部署前建议按下面做一遍,本地不 404 再部署:**
1. 在项目根目录执行:
```bash
pnpm build
pnpm start
```
2. 浏览器打开 `http://localhost:30006`(或你设置的 PORT确认页面正常、控制台无 `_next/static` 的 404。
3. 若有 404先按本文下面的「开发环境」或「问题 5」处理修好后再部署。
4. 确认无误后再执行 `python scripts/devlop.py` 或你的部署命令。
若跳过自检,本地缺失 `.next/static` 或跑错目录,部署到线上后同样会 404。
---
## 一、先区分环境
| 报错里出现的文件名 | 说明 |
|--------------------|------|
| **turbopack-*.js** | **开发环境**Next.js 16 默认用 Turbopack。说明你是在跑 `pnpm dev`,且浏览器在请求开发服务器的 chunk。 |
| **只有一长串 hash 的 .js / .css**(如 `6a98f5c6b2554ef3.js` | 可能是**开发**也可能是**生产**;生产里不会有 turbopack 这个名字。 |
---
## 二、开发环境(本机 `pnpm dev`)出现 404
### 原因简述
- 开发服务器重启后chunk 的**文件名hash会变**,但浏览器可能还在用**旧页面**里的 script 地址去请求,导致 404。
- 或者访问的地址/端口不对(例如打开了生产地址,却期望加载本机 dev 的 chunk
### 处理步骤
1. **强刷、清缓存**
- Windows`Ctrl + Shift + R` 或 `Ctrl + F5`
- Mac`Cmd + Shift + R`
- 或:开发者工具 → Network → 勾选「Disable cache」后再刷新
2. **确认访问地址**
- 管理端:`http://localhost:30006/admin`(本项目 dev 端口为 30006
- 不要用 `https://soul.quwanzhi.com/admin` 时还指望加载本机的 turbopack chunk
3. **重启开发服务器**
```bash
# 停掉当前 devCtrl+C再起
pnpm dev
```
然后再强刷一次 `http://localhost:30006/admin`
4. **仍 404**
- 看浏览器里 404 的请求 URL是相对路径 `/_next/static/...` 还是别的?
- 若是 `/_next/static/...` 且域名是 localhost:30006说明请求没到本机 dev 服务器,检查是否有代理/ hosts 把请求指到了别的机器。
---
## 三、生产环境(线上 soul.quwanzhi.com出现 404 - 快速修复
### ⚡ 最快修复方法(推荐)
**在宝塔面板操作:**
1. 登录宝塔面板 → **网站** → **soul.quwanzhi.com**
2. 点击 **设置** → **反向代理**
3. 检查是否有 **location /** 的配置
4. 如果没有,点击 **添加反向代理**,配置如下:
```
代理名称soul
目标URLhttp://127.0.0.1:30006
发送域名:$host
```
5. 保存后,点击 **配置文件**,确保配置类似这样:
```nginx
location / {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
```
6. 保存配置,点击 **重载配置** 或 **重启 Nginx**
**关键点**:必须有 `location /`(整站反代),不能只有 `location /api`,否则 `/_next/static/...` 会 404。
---
## 三、生产环境(线上 soul.quwanzhi.com出现 404 - 详细排查
### 原因简述
- 线上是 **standalone 部署**HTML 由 Node 输出,**JS/CSS 分片**来自 `.next/static`。
- 若 404 的是 `/_next/static/chunks/xxx.js` 或 `/_next/static/css/xxx.css`,多半是:
1. 部署时 **没有把 `.next/static` 完整拷到服务器**,或
2. **Nginx 没有把 `/_next` 请求反代到 Node**,或
3. 部署后**用了旧 HTML 缓存**HTML 里引用的 chunk 名已在新构建里不存在)。
### 处理步骤
1. **确认静态资源是否在服务器**
```bash
# SSH 到服务器后
ls -la /www/wwwroot/soul/.next/static/chunks/
ls -la /www/wwwroot/soul/.next/static/css/
```
- 若目录不存在或很少文件,说明部署时 **没有正确复制 `.next/static`**。
2. **部署脚本是否复制了 static**
- 本仓库:`scripts/start-standalone.js` 会在**本机**启动前把 `.next/static` 拷到 standalone 目录。
- 若用 `scripts/devlop.py` 部署:确认脚本里会把 **`.next/static`** 一起打包/上传到服务器(例如打包进 zip 或单独 rsync且解压后路径为 `项目根/.next/static`。
- 若用宝塔 Node 项目:启动的是 `node server.js`,工作目录里必须包含 `.next/standalone` 及其中拷贝好的 `.next/static`standalone 的 server 会从当前目录下的 `.next/static` 提供静态资源)。
3. **Nginx 必须把整站反代到 Node**
- 管理端和前台都是同一套 Next 应用,**所有路径**(含 `/_next/static/...`)都应反代到 Node例如
```nginx
location / {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
- **不要**只配 `location /api` 反代而把 `/` 指到别的目录,否则 `/_next/static/...` 会 404。
4. **清缓存后再试**
- 浏览器强刷:`Ctrl+Shift+R` / `Cmd+Shift+R`。
- 若用了 CDN 或 Nginx 缓存,需 purge 或暂时关缓存,避免旧 HTML 引用已不存在的 chunk。
---
## 四、小结
| 环境 | 优先检查 |
|------|----------|
| **开发** | 强刷 / 清缓存;确认访问 `http://localhost:30006/admin`;重启 `pnpm dev` |
| **生产** | 服务器上是否存在 `.next/static`Nginx 是否整站反代到 Node浏览器/CDN 缓存是否导致旧 HTML |
---
## 五、快速检查脚本(推荐)
### 检查服务器状态
```bash
python scripts/check_static_files.py
```
### 检查 Nginx 配置
```bash
python scripts/fix_nginx_static.py
```
脚本会检查 Nginx 配置是否正确,并给出修复建议。
本仓库提供了检查脚本,可快速查看服务器上静态资源是否存在:
```bash
python scripts/check_static_files.py
```
脚本会检查:
- `.next/static` 是否存在
- PM2 项目的工作目录
- Nginx 配置
- 端口 30006 是否在监听
根据输出结果,可以快速定位问题。
---
## 六、常见问题与修复
### 问题 1部署后 `.next/static` 不存在
**原因**:部署脚本在打包时复制了 static但解压后路径不对或文件丢失。
**修复**
1. SSH 到服务器检查:`ls -la /www/wwwroot/soul/.next/static/`
2. 若不存在,重新部署:`python scripts/devlop.py`
3. 部署后再次检查:`python scripts/check_static_files.py`
### 问题 2Nginx 只反代了 `/api`,没有反代 `/_next`
**现象**HTML 能加载,但所有 `/_next/static/...` 的请求都 404。
**修复**Nginx 配置需要**整站反代**,例如:
```nginx
location / {
proxy_pass http://127.0.0.1:30006;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
**不要**只配 `location /api`,否则 `/_next` 路径会 404。
### 问题 3PM2 工作目录不对
**现象**`server.js` 存在,但运行时找不到 `.next/static`(因为工作目录不是项目根目录)。
**修复**
- 宝塔 PM2 管理器:确保「工作目录」填 `/www/wwwroot/soul`(项目根目录,不是 `.next/standalone`
- 命令行 PM2`pm2 start server.js --name soul --cwd /www/wwwroot/soul`
### 问题 4standalone 部署时 static 路径问题
**说明**standalone 模式下,`server.js` 会从**当前工作目录**下的 `.next/static` 提供静态资源。
- ✅ 正确:工作目录 = 项目根(如 `/www/wwwroot/soul`),且该目录下有 `server.js`、`.next/static`、`public`
- ❌ 错误:工作目录 = `/www/wwwroot/soul/.next/standalone`,而 static 在项目根下server.js 会从 cwd 找 `.next/static`,找不到就 404
**宝塔用 `node server.js` 没问题**:本仓库的 `scripts/devlop.py`(及 deploy 打包)会把 **server.js、.next/static、public** 一起打进同一层目录,解压到服务器后,项目路径里就同时有这三者;宝塔 Node 项目填「项目路径」= 该目录、启动命令 = `node server.js` 即可,无需改成别的。
**修复**:确保宝塔里「项目路径」是**解压后的项目根目录**(包含 server.js 和 .next/static 的那一层),不要填成 `.next/standalone` 子目录。
### 问题 5本地打包后用 node 运行出现 404woff2 / css / js / turbopack 等)
**现象**`pnpm build` 后直接 `cd .next/standalone && node server.js`,浏览器访问 localhost 报 404如 `797e433ab948586e-s.p.xxx.woff2`、`turbopack-xxx.js`、各种 chunk 的 css/js
**原因**
1. standalone 目录里**没有** `.next/static` 和 `public`,需要先复制进去。
2. 若用**项目根**执行 `node .next/standalone/server.js` 且未复制 static 到 standalone服务器会从项目根找 `.next/static`(有则可用);若在 standalone 目录下执行且未复制,则 404。
3. 若 404 里出现 **turbopack-*.js**,多半是浏览器缓存了之前 **开发环境**的 HTML强刷即可。
**修复**
1. **不要**直接 `cd .next/standalone && node server.js`。应**在项目根**执行:
```bash
pnpm start
```
或:
```bash
node scripts/start-standalone.js
```
脚本会先把 `.next/static` 和 `public` 复制到 `.next/standalone`,并以正确工作目录启动,避免 404。
2. 浏览器**强刷**`Ctrl+Shift+R` / `Cmd+Shift+R`,避免旧 HTML 引用 turbopack 或已失效的 chunk。
---
按上述步骤仍 404 时,可把**具体 404 的完整 URL** 和**当前是 dev 还是线上**发出来,便于继续排查。

View File

@@ -1,379 +0,0 @@
# 订单状态同步定时任务(兜底机制)
> 解决问题:支付成功但回调未到达,导致订单状态不一致
> 创建时间2026-02-04
---
## 一、为什么需要这个机制?
### 实际问题
今天遇到的情况:
- ✅ 微信支付成功扣款
- ❌ 支付回调未到达服务器
- ❌ 订单状态仍为 `created`
- ❌ 用户无法解锁内容
### 原因分析
支付回调丢失的常见原因:
1. **网络问题**:微信服务器 → 你的服务器,网络波动、超时
2. **服务器宕机**:回调到达时,服务器正在重启
3. **配置错误**:回调地址未配置或配置错误
4. **防火墙拦截**:服务器防火墙拒绝微信 IP
5. **接口报错**:回调接口代码有 bug抛异常
### 生产环境必备
支付系统的**标准做法**
- 主路径:依赖微信支付回调(实时性好)
- 兜底路径:定时任务主动查询(保证最终一致性)
---
## 二、解决方案设计
### 核心逻辑
```
定时任务(每 5 分钟执行一次)
查询所有 'created' 状态的订单
遍历每个订单
├─ 判断是否超时(> 30 分钟)
│ ├─ 是 → 标记为 'expired'(订单过期)
│ └─ 否 → 继续
└─ 调用微信支付接口查询真实状态
├─ SUCCESS → 更新为 'paid',更新用户购买记录
├─ NOTPAY → 保持 'created',等待用户支付
├─ CLOSED → 更新为 'cancelled'(订单已关闭)
└─ ERROR → 记录日志,下次重试
```
### 状态流转
```
created (订单创建)
├─ [正常] 微信回调 → paid (支付成功)
├─ [兜底] 定时任务查询 → paid
├─ [超时] 30分钟未支付 → expired
└─ [关闭] 微信侧关闭 → cancelled
```
---
## 三、实现方案
### 方案 ANode.js API + Cron推荐
#### 1. 已创建接口
```
GET /api/cron/sync-orders?secret=YOUR_SECRET
```
#### 2. 配置 crontabLinux/Mac
```bash
# 编辑 crontab
crontab -e
# 添加以下行(每 5 分钟执行一次)
*/5 * * * * curl -X GET "https://soul.quwanzhi.com/api/cron/sync-orders?secret=YOUR_SECRET" >> /var/log/cron_sync_orders.log 2>&1
```
#### 3. 或使用 wget
```bash
*/5 * * * * wget -qO- "https://soul.quwanzhi.com/api/cron/sync-orders?secret=YOUR_SECRET" >> /var/log/cron_sync_orders.log 2>&1
```
### 方案 BPython 脚本 + Cron
#### 1. 已创建脚本
```
scripts/sync_order_status.py
```
#### 2. 配置 crontab
```bash
# 每 5 分钟执行一次
*/5 * * * * python /www/wwwroot/soul/scripts/sync_order_status.py >> /var/log/sync_orders.log 2>&1
```
### 方案 CVercel Cron如果部署在 Vercel
#### 1. 创建 `vercel.json`
```json
{
"crons": [{
"path": "/api/cron/sync-orders?secret=YOUR_SECRET",
"schedule": "*/5 * * * *"
}]
}
```
#### 2. 部署后自动生效
### 方案 DPM2 定时任务Node.js 服务器)
#### 1. 创建 `ecosystem.config.js`
```javascript
module.exports = {
apps: [{
name: "soul-cron",
script: "node",
args: "-e \"setInterval(() => fetch('http://localhost:30006/api/cron/sync-orders?secret=YOUR_SECRET'), 300000)\"",
cron_restart: "*/5 * * * *",
}]
}
```
#### 2. 启动
```bash
pm2 start ecosystem.config.js
```
---
## 四、配置说明
### 1. 设置安全密钥
**`.env.local`(开发环境)**
```bash
CRON_SECRET=your-random-secret-key-here
```
**`.env.production`(生产环境)**
```bash
CRON_SECRET=<生成一个随机的强密钥>
```
**生成密钥方法**
```bash
openssl rand -base64 32
```
### 2. 配置微信支付 API Key
**获取方式**
1. 登录微信商户平台 https://pay.weixin.qq.com
2. 账户中心 → API安全 → API密钥
3. 设置/查看 32 位密钥
**配置到环境变量**
```bash
WECHAT_API_KEY=your_32_char_api_key_here
```
### 3. 调整超时时间
修改代码中的 `ORDER_TIMEOUT_MINUTES`
```typescript
// 默认 30 分钟
const ORDER_TIMEOUT_MINUTES = 30
// 可根据实际情况调整(如 15 分钟)
const ORDER_TIMEOUT_MINUTES = 15
```
---
## 五、部署步骤
### Step 1: 部署代码
```bash
# 确保新增的文件已部署
app/api/cron/sync-orders/route.ts
scripts/sync_order_status.py
```
### Step 2: 配置环境变量
```bash
# 在服务器上设置
export CRON_SECRET="your_secret_key"
export WECHAT_API_KEY="your_wechat_api_key"
```
### Step 3: 设置定时任务
**宝塔面板**
1. 打开"计划任务"
2. 添加"访问URL"任务
3. URL: `https://soul.quwanzhi.com/api/cron/sync-orders?secret=YOUR_SECRET`
4. 执行周期: `*/5 * * * *`(每 5 分钟)
5. 保存并启用
**命令行**
```bash
# 编辑 crontab
crontab -e
# 添加
*/5 * * * * curl "https://soul.quwanzhi.com/api/cron/sync-orders?secret=YOUR_SECRET" >> /var/log/cron_orders.log 2>&1
```
### Step 4: 测试
**手动触发**
```bash
curl "https://soul.quwanzhi.com/api/cron/sync-orders?secret=YOUR_SECRET"
```
**预期响应**
```json
{
"success": true,
"message": "订单状态同步完成",
"total": 3,
"synced": 2,
"expired": 1,
"error": 0,
"duration": 1234
}
```
---
## 六、监控与日志
### 1. 查看日志
**Node.js 接口日志**
```bash
# 查看应用日志
pm2 logs soul
# 或查看系统日志
tail -f /var/log/cron_orders.log
```
**Python 脚本日志**
```bash
tail -f /var/log/sync_orders.log
```
### 2. 监控指标
关键指标:
- **执行频率**:是否每 5 分钟执行一次
- **同步成功率**`synced / total`
- **超时订单数**`expired` 数量
- **错误率**`error / total`
### 3. 告警设置(可选)
当错误率 > 10% 时,发送告警:
```bash
# 示例:通过企业微信机器人发送告警
if [ $error_rate -gt 10 ]; then
curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" \
-d '{"msgtype":"text","text":{"content":"订单同步失败率过高!"}}'
fi
```
---
## 七、测试验证
### 测试场景 1手动创建订单测试
```python
# 1. 手动插入一个 created 订单
INSERT INTO orders (id, order_sn, user_id, open_id, product_type, product_id, amount, status, created_at, updated_at)
VALUES ('TEST_001', 'TEST_ORDER_001', 'test_user', 'test_openid', 'section', '1.2', 1.00, 'created', NOW(), NOW());
# 2. 等待 5 分钟(定时任务执行)
# 3. 查询订单状态
SELECT * FROM orders WHERE order_sn = 'TEST_ORDER_001';
# 预期:如果配置了微信 API状态会根据实际支付情况更新否则保持 created
```
### 测试场景 2超时订单测试
```python
# 1. 插入一个 35 分钟前的订单
INSERT INTO orders (id, order_sn, user_id, open_id, product_type, product_id, amount, status, created_at, updated_at)
VALUES ('TEST_002', 'TEST_ORDER_002', 'test_user', 'test_openid', 'section', '1.2', 1.00, 'created', DATE_SUB(NOW(), INTERVAL 35 MINUTE), DATE_SUB(NOW(), INTERVAL 35 MINUTE));
# 2. 手动触发同步任务
curl "https://soul.quwanzhi.com/api/cron/sync-orders?secret=YOUR_SECRET"
# 3. 查询订单状态
SELECT * FROM orders WHERE order_sn = 'TEST_ORDER_002';
# 预期:状态变为 'expired'
```
---
## 八、常见问题 FAQ
### Q1: 定时任务未执行怎么办?
**排查步骤**
1. 检查 crontab 是否配置正确:`crontab -l`
2. 检查 cron 服务是否运行:`systemctl status cron`
3. 查看 cron 日志:`tail -f /var/log/cron`
4. 手动执行测试:`curl https://soul.quwanzhi.com/api/cron/sync-orders?secret=xxx`
### Q2: 为什么订单状态没有同步?
**可能原因**
1. **API Key 未配置**:检查 `WECHAT_API_KEY` 环境变量
2. **签名错误**API Key 不正确
3. **订单号不存在**:微信支付平台查不到该订单
4. **网络问题**:服务器无法访问微信支付接口
**解决方案**:查看日志,确认具体错误信息
### Q3: 如何调整执行频率?
修改 cron 表达式:
- 每 3 分钟:`*/3 * * * *`
- 每 10 分钟:`*/10 * * * *`
- 每小时:`0 * * * *`
### Q4: 测试环境如何处理?
测试环境通常无法配置真实的支付回调,建议:
1. **方案 A**:手动执行 `fix_unpaid_order.py` 更新订单状态
2. **方案 B**:使用沙箱环境(微信支付测试号)
3. **方案 C**Mock 微信支付接口,返回固定状态
---
## 九、性能与成本
### 性能影响
- **数据库查询**:每次查询 < 100 条订单耗时 < 100ms
- **微信接口调用**每个订单 < 500ms
- **总耗时**通常 < 5 订单量 < 10
### 成本分析
- **服务器资源**几乎可忽略 5 分钟执行 1
- **微信接口调用**免费查询订单状态不收费
- **网络流量**每次 < 1KB
---
## 十、总结
### 已实现功能
- 定时查询 `created` 状态订单
- 调用微信支付接口同步状态
- 自动标记超时订单为 `expired`
- 更新用户购买记录
- 提供 Node.js Python 两种实现
### 后续优化(可选)
1. **智能频率**订单多时增加频率订单少时降低频率
2. **分布式锁**多服务器部署时防止重复执行
3. **告警通知**失败率过高时发送告警
4. **数据看板**同步成功率超时率等指标可视化
---
## 附录:相关文件
- `app/api/cron/sync-orders/route.ts` - Node.js 定时任务接口
- `scripts/sync_order_status.py` - Python 定时任务脚本
- `scripts/fix_unpaid_order.py` - 手动修复脚本应急用
部署完成后支付系统将更加健壮即使回调丢失也能自动兜底

View File

@@ -1,287 +0,0 @@
# 订单表修复执行指南
**日期**: 2026-02-04
**问题**: 小程序支付后订单无法创建
**根本原因**: orders 表 status 字段缺少 'created' 状态
---
## 🎯 快速修复(推荐)
### 方式一:宝塔面板执行
1. **登录宝塔面板**
- 访问: http://你的服务器IP:8888
2. **打开数据库管理**
- 左侧菜单 → 数据库
- 找到 `soul_miniprogram` 数据库
- 点击"管理"
3. **执行 SQL**
- 点击"SQL窗口"
- 复制以下 SQL 并执行:
```sql
ALTER TABLE orders
MODIFY COLUMN status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired')
DEFAULT 'created';
```
4. **验证修复**
```sql
DESCRIBE orders;
```
查看 status 字段,应该显示:
```
status | enum('created','pending','paid',...) | YES | created
```
---
### 方式二Navicat/MySQL Workbench
1. **连接数据库**
- 主机: 56b4c23f6853c.gz.cdb.myqcloud.com
- 端口: 14413
- 用户: cdb_outerroot
- 密码: Zhiqun1984
- 数据库: soul_miniprogram
2. **打开查询窗口**
- 右键数据库 → 新建查询
3. **执行修复 SQL**
```sql
ALTER TABLE orders
MODIFY COLUMN status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired')
DEFAULT 'created';
```
4. **验证修复**
```sql
DESCRIBE orders;
```
---
### 方式三:命令行执行
```bash
# SSH 连接到服务器
ssh root@42.194.232.22 -p 22022
# 连接数据库
mysql -h 56b4c23f6853c.gz.cdb.myqcloud.com \
-P 14413 \
-u cdb_outerroot \
-p \
soul_miniprogram
# 输入密码: Zhiqun1984
# 执行修复
ALTER TABLE orders
MODIFY COLUMN status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired')
DEFAULT 'created';
# 验证
DESCRIBE orders;
# 退出
exit;
```
---
## 📝 完整 SQL 脚本
已创建完整的 SQL 脚本文件:
**文件位置**: `E:\Gongsi\Mycontent\scripts\fix-orders-table.sql`
包含:
- ✅ 查看当前状态
- ✅ 执行修复
- ✅ 验证结果
- ✅ 测试插入
- ✅ 清理测试数据
---
## 🧪 修复后测试
### 1. 数据库层面测试
```sql
-- 测试插入订单
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, created_at, updated_at
) VALUES (
'TEST_001', 'TEST_001', 'user_test', 'openid_test',
'section', '1-1', 9.9, '测试订单', 'created', NOW(), NOW()
);
-- 查询验证
SELECT * FROM orders WHERE order_sn = 'TEST_001';
-- 删除测试订单
DELETE FROM orders WHERE order_sn = 'TEST_001';
```
### 2. 小程序测试
1. 打开小程序
2. 选择任意章节
3. 点击"购买章节"
4. 完成支付
5. 查询数据库:
```sql
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
```
6. 应该能看到新订单status 为 'created' 或 'paid'
---
## ⚠️ 注意事项
### 1. 修复前
- ✅ **已备份数据库**(你已完成)
- ✅ 确认当前没有正在进行的支付
- ✅ 建议在低峰期执行(凌晨或用户少时)
### 2. 修复中
- ⏱️ ALTER TABLE 操作很快(通常 < 1秒
- 🔒 修改期间表会被锁定
- ⚠️ 如果表很大(百万级),可能需要几秒
### 3. 修复后
- ✅ 立即测试支付功能
- ✅ 查看服务器日志确认无错误
- ✅ 监控订单创建情况
---
## 🔍 问题诊断
### 如果修复后仍无法创建订单
1. **检查代码是否已部署**
```bash
# 查看服务器上的代码版本
ssh root@42.194.232.22 -p 22022
cd /www/wwwroot/auto-devlop/soulTest/dist
cat lib/db.js | grep "ENUM"
```
2. **检查 Next.js 服务是否重启**
```bash
pm2 restart soul
pm2 logs soul --lines 50
```
3. **查看实时日志**
```bash
pm2 logs soul --lines 100 | grep "MiniPay"
```
4. **手动测试 API**
```bash
curl -X POST https://soul.quwanzhi.com/api/miniprogram/pay \
-H "Content-Type: application/json" \
-d '{
"openId": "test_openid",
"productType": "section",
"productId": "1-1",
"amount": 9.9,
"description": "测试",
"userId": "test_user"
}'
```
---
## 📊 修复前后对比
### 修复前
```
status ENUM('pending', 'paid', 'cancelled', 'refunded')
DEFAULT 'pending'
```
**问题**:
- ❌ 代码使用 'created' 状态
- ❌ 数据库不接受 'created' 值
- ❌ 订单插入失败
- ❌ orders 表为空
### 修复后
```
status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired')
DEFAULT 'created'
```
**结果**:
- ✅ 支持 'created' 状态
- ✅ 订单成功创建
- ✅ 支付流程正常
- ✅ orders 表有记录
---
## 🎯 订单状态流程
```
用户点击购买
【前端】调用 /api/miniprogram/pay
【后端】创建订单 (status='created') ← 修复后可用
【后端】调用微信支付接口
【小程序】调起微信支付
【用户】完成支付
【微信】回调 /api/miniprogram/pay/notify
【后端】更新订单 (status='paid')
【后端】解锁用户权限
【后端】分配推荐佣金
✅ 完成
```
---
## 🔗 相关文档
- [支付订单完整修复方案](./支付订单完整修复方案.md)
- [支付订单未创建问题分析](./支付订单未创建问题分析.md)
- [订单表状态字段修复说明](./订单表状态字段修复说明.md)
- [SQL 脚本](../../scripts/fix-orders-table.sql)
---
## ✅ 修复检查清单
- [ ] 数据库已备份
- [ ] 执行 ALTER TABLE SQL
- [ ] 验证 status 字段定义正确
- [ ] 测试插入订单成功
- [ ] 小程序测试支付成功
- [ ] 查询 orders 表有记录
- [ ] 用户权限正确解锁
- [ ] 推荐人获得佣金
---
**执行上述任意一种方式,即可完成修复!** 🎉
**推荐使用方式一(宝塔面板),操作最简单!**

View File

@@ -1,242 +0,0 @@
# 订单表状态字段修复说明
**日期**: 2026-02-04
**问题**: orders 表没有数据
**根本原因**: orders 表的 status 字段缺少 'created' 状态
---
## 🔍 问题诊断
### 1. 症状
- 小程序购买后orders 表中没有任何记录
- 后端日志显示"订单已插入",但数据库查不到
- 数据库连接正常,但插入失败
### 2. 根本原因
**数据库表定义**`lib/db.ts` 第155行:
```sql
status ENUM('pending', 'paid', 'cancelled', 'refunded') DEFAULT 'pending'
```
**代码中使用的状态**`app/api/miniprogram/pay/route.ts` 第177行:
```typescript
'created' // ❌ 这个值不在 ENUM 中!
```
**结果**: 插入失败但没有抛出异常(被 try-catch 捕获),导致订单看似创建成功,实际未入库。
---
## ✅ 修复方案
### 方案一:修改数据库表结构(推荐)
在生产数据库执行以下SQL
```sql
-- 修改 status 字段,添加 'created' 和 'expired' 状态
ALTER TABLE orders
MODIFY COLUMN status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired')
DEFAULT 'created';
```
**优点**:
- 符合订单流程created已创建→ pending待支付→ paid已支付
- 代码不需要修改
- 支持订单过期状态
---
### 方案二:修改代码使用 'pending' 状态
如果不方便修改数据库,可以修改代码:
1. `app/api/miniprogram/pay/route.ts` 第177行
```typescript
'pending' // 改为 pending
```
2. `app/api/miniprogram/pay/notify/route.ts` 第127行
```typescript
WHERE status IN ('pending') // 只检查 pending
```
**缺点**:
- 无法区分"已创建"和"待支付"状态
- 不符合标准订单流程
---
## 📊 订单状态说明
| 状态 | 说明 | 触发时机 |
|-----|------|---------|
| `created` | 已创建 | 调用 `/api/miniprogram/pay` 时 |
| `pending` | 待支付 | (可选)微信支付接口调用成功后 |
| `paid` | 已支付 | 支付回调成功时 |
| `cancelled` | 已取消 | 用户主动取消 |
| `refunded` | 已退款 | 退款成功时 |
| `expired` | 已过期 | 超过30分钟未支付 |
---
## 🔧 完整修复步骤
### 1. 连接生产数据库
```bash
# 使用MySQL客户端连接
mysql -h sh-cynosdbmysql-grp-27q7yv6u.sql.tencentcdb.com \
-P 28329 \
-u soul \
-p \
soulTest
```
### 2. 执行修复SQL
```sql
-- 1. 查看当前表结构
DESCRIBE orders;
-- 2. 修改 status 字段
ALTER TABLE orders
MODIFY COLUMN status ENUM('created', 'pending', 'paid', 'cancelled', 'refunded', 'expired')
DEFAULT 'created';
-- 3. 验证修改
DESCRIBE orders;
-- 4. 测试插入(可选)
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, created_at, updated_at
) VALUES (
'TEST001', 'TEST001', 'test_user', 'test_openid',
'section', '1-1', 9.9, '测试订单',
'created', NOW(), NOW()
);
-- 5. 验证插入结果
SELECT * FROM orders WHERE order_sn = 'TEST001';
-- 6. 删除测试订单
DELETE FROM orders WHERE order_sn = 'TEST001';
```
### 3. 重新部署代码
```bash
# 确保 lib/db.ts 已更新
python scripts/devlop.py
```
### 4. 测试购买流程
1. 打开小程序
2. 选择任意章节
3. 点击"购买章节"
4. 完成支付
5. 查询数据库:
```sql
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
```
---
## ⚠️ 重要提醒
### 1. 数据库备份
修改表结构前,建议先备份 orders 表:
```sql
CREATE TABLE orders_backup AS SELECT * FROM orders;
```
### 2. 事务安全
ALTER TABLE 操作是原子性的,但建议在低峰期执行,避免影响用户。
### 3. 连接配置
如果无法连接数据库,请检查:
- IP白名单是否包含你的IP
- 数据库账号是否有修改表结构权限
- 端口是否正确28329
---
## 🧪 验证测试
### 1. 本地测试(可选)
如果有本地数据库,可以先在本地测试:
```bash
# 修改 .env.local
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=your_password
MYSQL_DATABASE=soul_test
# 初始化数据库
npm run db:init
# 测试订单插入
node scripts/test-order-insert.js
```
### 2. 生产环境测试
修复后,使用小程序测试:
1. ✅ 订单创建成功status='created'
2. ✅ 支付成功后状态更新status='paid'
3. ✅ 用户权限解锁
4. ✅ 推荐人获得佣金
---
## 📝 相关文件
| 文件 | 修改内容 |
|-----|---------|
| `lib/db.ts` | 第155行修改 status ENUM 定义 |
| `app/api/miniprogram/pay/route.ts` | 第177行使用 'created' 状态 |
| `app/api/miniprogram/pay/notify/route.ts` | 第127行检查 'created' 状态 |
| `scripts/fix-orders-table.sql` | 修复SQL脚本 |
---
## 🎉 修复后的效果
### 修复前
```
用户购买 → 创建订单status='created')→ ❌ 插入失败ENUM不匹配→ orders表为空
```
### 修复后
```
用户购买 → 创建订单status='created')→ ✅ 插入成功 → orders表有记录
完成支付
支付回调 → 更新订单status='paid')→ ✅ 解锁权限 → ✅ 分配佣金
```
---
## 🔗 相关文档
- [支付订单完整修复方案](./支付订单完整修复方案.md)
- [支付接口清单](./支付接口清单.md)
- [小程序支付订单记录修复说明](./小程序支付订单记录修复说明.md)
---
**修复此问题后,所有支付订单都能正常记录到数据库!** 🎉

View File

@@ -1,307 +0,0 @@
# 订单记录修复说明
**日期**: 2026-02-04
**问题**: 用户购买书籍时,订单数据未写入 `orders` 表,导致管理端显示"订单数 = 0"
---
## 🔴 问题分析
### 原有问题
**症状**:
- 用户购买成功,但管理端订单数 = 0
- `orders` 表中无任何记录
- 推荐人无法获得佣金
**根本原因**:
1. **创建订单时** (`/api/payment/create-order`)
- ❌ 只生成了订单对象
-**从未插入到 `orders` 表**
2. **支付成功时** (`/api/payment/wechat/notify`, `/api/payment/alipay/notify`)
- ❌ 全是 `TODO` 注释
- ❌ 没有更新订单状态
- ❌ 没有解锁内容权限
- ❌ 没有分配推荐佣金
---
## ✅ 修复方案
### 1. 创建订单时插入数据库
**文件**: `app/api/payment/create-order/route.ts`
**改动**:
```typescript
// 引入数据库查询
import { query } from "@/lib/db"
// 创建订单对象后,立即插入数据库
await query(`
INSERT INTO orders (
id, order_sn, user_id, open_id,
product_type, product_id, amount, description,
status, transaction_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
`, [
orderSn, // id
orderSn, // order_sn
userId, // user_id
openId, // open_id
productType, // product_type ('section' | 'fullbook')
productId, // product_id (章节ID 或 'fullbook')
amount, // amount
description, // description
'created', // status (初始状态)
tradeSn // transaction_id (支付流水号)
])
```
**效果**:
- ✅ 订单创建时立即写入数据库
- ✅ 初始状态为 `status = 'created'`
- ✅ 管理端可以看到所有订单(包括未支付的)
---
### 2. 支付成功时完整处理
**文件**:
- `app/api/payment/wechat/notify/route.ts`
- `app/api/payment/alipay/notify/route.ts`
**改动**: 实现了完整的支付成功处理流程
#### 2.1 更新订单状态
```typescript
// 通过 transaction_id 查找订单
const orderRows = await query(`
SELECT id, user_id, amount, product_type, product_id
FROM orders
WHERE transaction_id = ? AND status = 'created'
LIMIT 1
`, [notifyResult.tradeSn])
// 更新为已支付
await query(`
UPDATE orders
SET status = 'paid',
pay_time = ?,
updated_at = NOW()
WHERE id = ?
`, [notifyResult.payTime, orderId])
```
---
#### 2.2 解锁内容权限
```typescript
if (productType === 'fullbook') {
// 购买全书 → 开通全书权限
await query('UPDATE users SET has_full_book = 1 WHERE id = ?', [userId])
} else if (productType === 'section' && productId) {
// 购买单个章节 → 记录章节权限
// (根据业务逻辑处理,可能需要更新 user_purchases 表)
}
```
---
#### 2.3 分配推荐佣金
```typescript
// 查询用户的推荐人
const userRows = await query(`
SELECT u.id, u.referred_by, rb.referrer_id, rb.status
FROM users u
LEFT JOIN referral_bindings rb
ON rb.referee_id = u.id
AND rb.status = 'active'
AND rb.expiry_date > NOW()
WHERE u.id = ?
LIMIT 1
`, [userId])
if (userRows.length > 0 && userRows[0].referrer_id) {
const referrerId = userRows[0].referrer_id
const commissionRate = 0.9 // 90% 佣金比例
const commissionAmount = parseFloat((amount * commissionRate).toFixed(2))
// 1. 更新推荐人的待结算收益
await query(`
UPDATE users
SET pending_earnings = pending_earnings + ?
WHERE id = ?
`, [commissionAmount, referrerId])
// 2. 更新绑定状态为已转化
await query(`
UPDATE referral_bindings
SET status = 'converted',
conversion_date = NOW(),
commission_amount = ?
WHERE referee_id = ? AND status = 'active'
`, [commissionAmount, userId])
}
```
---
## 📊 完整流程
```
用户购买
【步骤1】调用 /api/payment/create-order
├─ 生成订单号 orderSn, tradeSn
├─ ✅ 插入 orders 表 (status = 'created')
└─ 返回支付参数
【步骤2】用户完成支付
【步骤3】微信/支付宝回调 /api/payment/wechat/notify
├─ ✅ 更新 orders 表 (status = 'paid', pay_time)
├─ ✅ 解锁用户内容权限 (users.has_full_book)
└─ ✅ 分配推荐佣金
├─ 更新 users.pending_earnings
└─ 更新 referral_bindings.status = 'converted'
【完成】订单完整记录在数据库,管理端可见
```
---
## 🎯 修复效果
### 修复前
-`orders` 表永远是空的
- ❌ 管理端显示"订单数 = 0"
- ❌ 管理端显示"总收入 = ¥0.00"
- ❌ 推荐人无法获得佣金
### 修复后
- ✅ 每次购买都会创建订单记录
- ✅ 支付成功后订单状态更新为 `paid`
- ✅ 管理端显示真实订单数和总收入
- ✅ 推荐人自动获得 90% 佣金
---
## 🔄 数据流向
```
购买请求
orders 表
├─ status: 'created' ← 创建订单时
└─ status: 'paid' ← 支付成功时
users 表
├─ has_full_book: 1 (全书权限)
└─ pending_earnings: +8.91 (推荐人佣金)
referral_bindings 表
├─ status: 'converted' (转化状态)
└─ commission_amount: 8.91 (佣金金额)
```
---
## ⚠️ 注意事项
### 1. 订单状态流转
| 状态 | 说明 | 触发时机 |
|-----|------|---------|
| `created` | 已创建 | 调用 create-order 时 |
| `paid` | 已支付 | 支付回调成功时 |
| `expired` | 已过期 | 30分钟未支付需要定时任务处理 |
| `refunded` | 已退款 | 管理员手动退款时 |
---
### 2. 佣金结算时机
-**支付成功立即分配到 `pending_earnings`**
-**结算到 `earnings` 需要审核**(未实现,需要另外添加)
- 💰 **提现需要从 `earnings` 扣除**
**建议**:添加定时任务,定期将 `pending_earnings` 结算到 `earnings`
---
### 3. 章节购买权限
目前购买章节时,只是记录到 `orders` 表,**但没有更新用户的 `purchased_sections`**。
**解决方案**
- 方案 A在支付回调时查询并更新 `users` 表的某个字段
- 方案 B创建独立的 `user_purchases` 表记录章节购买
- 方案 C在用户查询章节权限时动态从 `orders` 表查询
---
### 4. 重复回调处理
支付回调可能会被多次调用(网络重试),代码中已处理:
```typescript
// 只处理状态为 'created' 的订单,避免重复处理
WHERE transaction_id = ? AND status = 'created'
```
如果订单已经是 `paid` 状态,会跳过处理。
---
## 📋 修改的文件
| 文件 | 改动 |
|-----|------|
| `app/api/payment/create-order/route.ts` | ✅ 添加:插入订单到数据库 |
| `app/api/payment/wechat/notify/route.ts` | ✅ 实现:更新订单 + 解锁权限 + 分配佣金 |
| `app/api/payment/alipay/notify/route.ts` | ✅ 实现:更新订单 + 解锁权限 + 分配佣金 |
---
## 🧪 测试步骤
1. **重启 Next.js 服务器**
2. **模拟购买**
- 小程序中购买章节
- 使用微信支付或支付宝支付
3. **检查数据库**
```sql
-- 查看订单
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;
-- 查看收益
SELECT id, nickname, earnings, pending_earnings FROM users WHERE pending_earnings > 0;
-- 查看绑定转化
SELECT * FROM referral_bindings WHERE status = 'converted';
```
4. **检查管理端**
- 访问 `/admin` → 数据概览
- 应该能看到订单数 > 0
- 总收入 > ¥0.00
---
## 🎉 总结
**核心问题**:支付流程中订单未写入数据库
**根本原因**:代码中全是 `TODO` 注释,功能未实现
**解决方案**
1. ✅ 创建订单时立即插入 `orders` 表
2. ✅ 支付成功时更新订单状态
3. ✅ 自动解锁内容权限
4. ✅ 自动分配推荐佣金
**现在订单记录已完整接入数据库!** 🎉

View File

@@ -1,96 +0,0 @@
# 小程序阅读逻辑分析
> 分析范围:阅读页进入 → 免费/付费判断 → 登录 → 购买 → 解锁与已读统计
> 结论:整体逻辑**合理且闭环**,权限以服务端为准,存在少量可优化点。
---
## 一、整体流程概览
```
进入阅读页(onLoad)
→ 处理 ref 推荐码、加载免费章节配置(异步)
→ initSection(id)
→ 免费章节 → canAccess=true拉内容标记已读
→ 付费章节 → 未登录canAccess=false展示付费墙
→ 已登录:请求 check-purchased → 以服务端结果设 canAccess
→ loadContent(id)(内容接口返回全文,前端按 canAccess 决定展示全文/预览)
→ 若 canAccessmarkSectionAsRead(id)
→ 用户可:登录 / 购买本章 / 购买全书
```
- **登录**(含因付款弹窗触发的登录):`refreshPurchaseFromServer()``recheckCurrentSectionAndRefresh()`(先拉取最新免费章节配置,若当前章已变为免费则直接解锁;否则再请求 `check-purchased` 并刷新页面)。
- **支付成功**`refreshUserPurchaseStatus()``initSection(sectionId)`,以最新订单数据刷新状态。
---
## 二、权限与数据源(是否合理)
| 环节 | 数据源 | 是否合理 |
|------|--------|----------|
| 是否已购买(单章/全书) | 服务端 `check-purchased`(基于 `users.has_full_book` + `orders` 表 status=paid | ✅ 权威 |
| 用户购买列表(列表页/支付前) | 服务端 `purchase-status`(同上) | ✅ 权威 |
| 首次进入 / 冷启动 | `app.globalData`(来自 login 或 storage 的 userInfo.purchasedSections / hasFullBook | ✅ 仅作初态,进入阅读页后会再请求 check-purchased |
| 登录后是否解锁当前章 | 先 `refreshPurchaseFromServer()`,再 `recheckCurrentSectionAndRefresh()` 内请求 `check-purchased` | ✅ 以服务端为准,不信任本地缓存 |
结论:**权限判断以服务端为准**,阅读页对付费章会请求 `check-purchased`,登录后也会重新校验当前章节,逻辑合理。
---
## 三、各场景是否闭环
- **未登录打开付费章**:不请求购买接口 → `canAccess=false` → 只显示预览 + 付费墙;点购买 → 弹登录;登录后 `recheckCurrentSectionAndRefresh()` 只会在服务端返回已购买时才解锁。✅
- **已登录未购买**`check-purchased` 返回未购买 → `canAccess=false` → 付费墙;支付成功后 `refreshUserPurchaseStatus()` + `initSection()` 会刷新并解锁。✅
- **已登录已购买**`check-purchased` 返回已购买 → `canAccess=true` → 全文展示并 `markSectionAsRead`。✅
- **免费章节**`freeIds` 包含则直接 `canAccess=true`,不查订单。✅(免费列表与后端 `/api/db/config` 的 freeChapters 异步同步,首帧用本地默认列表,见下节。)
---
## 四、内容与展示(是否泄密)
- **章节内容接口** `GET /api/book/chapter/[id]`:当前实现**不校验登录与购买**,直接返回全文。
- 前端:根据 `canAccess` 展示 `contentParagraphs`(全文)或 `previewParagraphs`(约 20% 预览),未购买时不会在页面上渲染全文。
- 结论:**展示逻辑正确**,未出现“未付费却展示全文”的前端漏洞。若后续有 Web 或开放 API建议在章节接口侧按登录/购买做服务端鉴权,防止直连接口拉取全文。
---
## 五、已读统计逻辑
- **含义**:仅统计“有权限打开并看到全文”的章节(`canAccess=true` 时调用 `app.markSectionAsRead(sectionId)`)。
- **存储**`app.globalData.readSectionIds` + `wx.setStorageSync('readSectionIds', list)`
- **使用**:首页/我的页“已读 X 章”等。
- 结论:**与“已购”分离,且只在实际有权限时打点**,逻辑合理。
---
## 六、可优化与风险点
1. **免费章节配置竞态**
- `onLoad``loadFreeChaptersConfig()``await`,紧接着执行 `initSection(id)`,首帧用的是页面默认 `freeIds`。若后端 `freeChapters` 与默认不一致,可能出现某章第一次被误判为付费/免费。
- 建议:`await this.loadFreeChaptersConfig()``initSection(id)`;或 `loadFreeChaptersConfig` 完成后若当前页是阅读页且未初始化过,再调一次 `initSection(this.data.sectionId)`
2. **initSection 中 check-purchased 失败时的降级**
- 当前:请求失败时用 `hasFullBook || purchasedSections.includes(id)` 作为 canAccess。若服务端已撤权而本地缓存仍为“已购”会误解锁。
- 建议:降级时**保守处理**,设为 `canAccess = false`,避免误解锁;可再根据需求加重试或 toast“网络异常请稍后重试”。
3. **登录后重复请求**
- 登录成功后:`refreshPurchaseFromServer()`purchase-status+ `recheckCurrentSectionAndRefresh()`check-purchased + initSection而 initSection 内对付费章会再次请求 check-purchased存在一次重复请求和二次 loading。
- 可选优化:`recheckCurrentSectionAndRefresh()` 内已拿到当前章购买结果后,只更新状态并调用 `loadContent` + `loadNavigation`,不再调完整 `initSection`,减少一次 check-purchased 和一次 loading。当前实现功能正确仅体验可优化。
4. **章节接口无鉴权**
- 如上,若未来有 Web 或开放能力,建议在 `GET /api/book/chapter/[id]` 中根据 token/userId + 购买记录决定返回全文或仅预览,避免直连接口拿到全文。
5. **极端情况:登录后当前章节刚改为免费**
- 已处理:`recheckCurrentSectionAndRefresh()` 内先 `await this.loadFreeChaptersConfig()` 拉取最新免费列表,再判断 `freeIds.includes(sectionId)`;若已免费则直接设 `canAccess=true``initSection`,无需再查购买。
---
## 七、结论与建议
- **整体阅读逻辑合理**:进入章节、免费/付费、登录、购买、登录后重验当前章、支付后刷新,形成闭环;权限以服务端 `check-purchased` / `purchase-status` 为准,不会因“刚登录”或本地缓存误解锁。
- **建议优先做的**
- 免费章节配置:`await loadFreeChaptersConfig()``initSection`,或配置拉取完成后补刷一次。
- initSection 中 check-purchased 失败时改为**保守策略**canAccess = false避免误解锁。
- **可选优化**:登录后减少一次 check-purchased 与 initSection 的重复调用,以减轻 loading 和请求次数。
以上为当前阅读逻辑的完整分析,可直接作为开发组(如 03_开发组评审与迭代依据。