删除不再使用的 Docker 相关文件,包括 Dockerfile、docker-compose.yml 和 .dockerignore,简化项目结构以提升可维护性。同时,更新 DEPLOYMENT.md 文档,调整 Next.js 配置说明,确保与新部署方式一致。
This commit is contained in:
@@ -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
|
||||
@@ -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`。
|
||||
|
||||
|
||||
63
Dockerfile
63
Dockerfile
@@ -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"]
|
||||
@@ -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" }, // 合并:分销+订单+提现
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
2
next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
51
scripts/clean-standalone.js
Normal file
51
scripts/clean-standalone.js
Normal 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);
|
||||
});
|
||||
92
scripts/prepare-standalone.js
Normal file
92
scripts/prepare-standalone.js
Normal 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)');
|
||||
|
||||
@@ -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);
|
||||
|
||||
24
scripts/write-standalone-warning.js
Normal file
24
scripts/write-standalone-warning.js
Normal 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 (_) {}
|
||||
}
|
||||
146
开发文档/8、部署/Next.js宝塔部署方案.md
Normal file
146
开发文档/8、部署/Next.js宝塔部署方案.md
Normal 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 |
|
||||
| **devlop(dist 切换)** | `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. 添加 Node(PM2)项目
|
||||
|
||||
- 宝塔 → **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。
|
||||
@@ -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. **团队协作**:减少环境配置问题
|
||||
|
||||
|
||||
@@ -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[网站内容自动更新]
|
||||
\`\`\`
|
||||
@@ -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
|
||||
@@ -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 chunks(React、Zustand)被共享
|
||||
- 业务代码按页面分割
|
||||
|
||||
3. **真实影响**
|
||||
- 首屏加载时间:~1-2s(4G 网络)
|
||||
- 页面切换时间:~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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**建议**:当前阶段关闭警告,专注于功能完善和测试。等发布后根据用户反馈再优化。
|
||||
@@ -1,160 +0,0 @@
|
||||
# soul.quwanzhi.com 更新后仍显示旧站/报错 — 原因分析与排查
|
||||
|
||||
> 现象:宝塔里站点源码已更新,但访问 https://soul.quwanzhi.com 仍显示旧站点或「哎呀, 出错了」;要求域名在 Node 界面访问且后台不出错。
|
||||
|
||||
---
|
||||
|
||||
## 一、可能原因概览
|
||||
|
||||
| 原因 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **1. 端口不一致** | 部署脚本或 PM2 使用端口 3888,Nginx 反代 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`,应用只监听 3888;Nginx 把请求转到 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`。
|
||||
|
||||
---
|
||||
|
||||
## 五、原因 4:Nginx 未把整站反代到 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。
|
||||
|
||||
---
|
||||
|
||||
## 六、原因 5:PM2 工作目录错误
|
||||
|
||||
**说明**: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 并重启,再按上面顺序自检一遍。
|
||||
@@ -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`)进行自动部署,开发分支可以手动部署或使用单独的测试环境。
|
||||
- 环境区分:生产环境和开发环境应严格区分,避免将开发中的不稳定代码直接部署到生产环境。
|
||||
- 备份:在进行任何部署操作之前,务必定期备份您的网站代码和数据库。
|
||||
@@ -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. 真机预览验证
|
||||
|
||||
---
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
### 问题 1:switchTab 仍然无效
|
||||
|
||||
**原因**:app.json 中的 tabBar 配置未生效
|
||||
|
||||
**解决**:
|
||||
1. 检查 `miniprogram/app.json` 是否包含 `tabBar` 字段
|
||||
2. 检查 `tabBar.list` 中的 `pagePath` 是否正确
|
||||
3. 重新编译小程序(微信开发者工具 → 编译)
|
||||
|
||||
### 问题 2:中间按钮不凸起
|
||||
|
||||
**原因**:`marginTop: -16` 未生效
|
||||
|
||||
**解决**:
|
||||
1. 检查父容器是否设置了 `overflow: hidden`
|
||||
2. 检查 `position` 是否被覆盖
|
||||
3. 调整 `marginTop` 数值(如 -20)
|
||||
|
||||
### 问题 3:tabBar 显示两层
|
||||
|
||||
**症状**:系统 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. ⏳ 真机预览验证
|
||||
|
||||
---
|
||||
|
||||
**修复完成!现在底部导航应该可以正常点击,且样式对齐原项目设计。**
|
||||
@@ -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条件
|
||||
**效果**: 订单完整记录,管理端数据正常
|
||||
|
||||
**现在小程序支付已完整接入数据库!** 🎉
|
||||
@@ -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. ⏳ 真机预览验证
|
||||
|
||||
---
|
||||
|
||||
**修复完成!样式应该正常显示了。**
|
||||
@@ -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 ✅**
|
||||
@@ -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),上传即把该目录代码完整提交到微信公众平台。Web(Next.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
|
||||
@@ -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 服务器后生效**
|
||||
@@ -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 只拉写作部分,不影响写作
|
||||
@@ -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
|
||||
**维护者:** 卡若
|
||||
@@ -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` 表的记录数
|
||||
- ✅ 所有统计数据实时从数据库查询
|
||||
- ✅ 不再依赖模拟数据
|
||||
|
||||
---
|
||||
|
||||
**管理端分销数据已接入真实数据库!** 🎉
|
||||
@@ -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
|
||||
# 停掉当前 dev(Ctrl+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
|
||||
目标URL:http://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`
|
||||
|
||||
### 问题 2:Nginx 只反代了 `/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。
|
||||
|
||||
### 问题 3:PM2 工作目录不对
|
||||
|
||||
**现象**:`server.js` 存在,但运行时找不到 `.next/static`(因为工作目录不是项目根目录)。
|
||||
|
||||
**修复**:
|
||||
- 宝塔 PM2 管理器:确保「工作目录」填 `/www/wwwroot/soul`(项目根目录,不是 `.next/standalone`)
|
||||
- 命令行 PM2:`pm2 start server.js --name soul --cwd /www/wwwroot/soul`
|
||||
|
||||
### 问题 4:standalone 部署时 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 运行出现 404(woff2 / 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 还是线上**发出来,便于继续排查。
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、实现方案
|
||||
|
||||
### 方案 A:Node.js API + Cron(推荐)
|
||||
|
||||
#### 1. 已创建接口
|
||||
```
|
||||
GET /api/cron/sync-orders?secret=YOUR_SECRET
|
||||
```
|
||||
|
||||
#### 2. 配置 crontab(Linux/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
|
||||
```
|
||||
|
||||
### 方案 B:Python 脚本 + 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
|
||||
```
|
||||
|
||||
### 方案 C:Vercel Cron(如果部署在 Vercel)
|
||||
|
||||
#### 1. 创建 `vercel.json`
|
||||
```json
|
||||
{
|
||||
"crons": [{
|
||||
"path": "/api/cron/sync-orders?secret=YOUR_SECRET",
|
||||
"schedule": "*/5 * * * *"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 部署后自动生效
|
||||
|
||||
### 方案 D:PM2 定时任务(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` - 手动修复脚本(应急用)
|
||||
|
||||
部署完成后,支付系统将更加健壮,即使回调丢失,也能自动兜底!
|
||||
@@ -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 表有记录
|
||||
- [ ] 用户权限正确解锁
|
||||
- [ ] 推荐人获得佣金
|
||||
|
||||
---
|
||||
|
||||
**执行上述任意一种方式,即可完成修复!** 🎉
|
||||
|
||||
**推荐使用方式一(宝塔面板),操作最简单!**
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
**修复此问题后,所有支付订单都能正常记录到数据库!** 🎉
|
||||
@@ -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. ✅ 自动分配推荐佣金
|
||||
|
||||
**现在订单记录已完整接入数据库!** 🎉
|
||||
@@ -1,96 +0,0 @@
|
||||
# 小程序阅读逻辑分析
|
||||
|
||||
> 分析范围:阅读页进入 → 免费/付费判断 → 登录 → 购买 → 解锁与已读统计
|
||||
> 结论:整体逻辑**合理且闭环**,权限以服务端为准,存在少量可优化点。
|
||||
|
||||
---
|
||||
|
||||
## 一、整体流程概览
|
||||
|
||||
```
|
||||
进入阅读页(onLoad)
|
||||
→ 处理 ref 推荐码、加载免费章节配置(异步)
|
||||
→ initSection(id)
|
||||
→ 免费章节 → canAccess=true,拉内容,标记已读
|
||||
→ 付费章节 → 未登录:canAccess=false,展示付费墙
|
||||
→ 已登录:请求 check-purchased → 以服务端结果设 canAccess
|
||||
→ loadContent(id)(内容接口返回全文,前端按 canAccess 决定展示全文/预览)
|
||||
→ 若 canAccess:markSectionAsRead(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_开发组)评审与迭代依据。
|
||||
Reference in New Issue
Block a user