新增订单推荐人和邀请码功能,优化支付流程中的订单插入逻辑,确保订单记录准确。更新小程序支付请求,支持传递邀请码以便于分销归属和对账。同时,调整数据库结构以支持新字段,提升系统的稳定性和用户体验。

This commit is contained in:
乘风
2026-02-05 14:50:25 +08:00
parent b96acadf91
commit d83f3d8419
15 changed files with 163 additions and 1620 deletions

View File

@@ -1,166 +0,0 @@
# Web 转小程序 - 完整流程提示词
> **用法**:在对话中 @ 本文件(`scripts/Web转小程序并上传-提示词.md`),请 AI 按本提示词执行:**先枚举要转换的页面 → 按规则 100% 转为小程序 → 自检清单逐项通过 → 可选打开微信开发者工具**。
---
## 一、你的任务(当被 @ 本文件时)
按**顺序**执行:
1. **枚举页面**:扫描 `app/` 下所有 `**/page.tsx`(排除 `app/api/``app/admin/`),得到需上小程序的页面列表及对应小程序路径(见二)。
2. **转换**:将上述每个 Web 页面**完整、一致**转为小程序代码到 `miniprogram/`**样式、按钮、布局、交互、图标零丢失**(见三、四)。
3. **检查**:按「六、自检清单」逐项自检,确保可运行且与 Web 逐页对照无遗漏。
4. **打开微信开发者工具**(可选):若用户未说「仅转换」,则执行 `start miniprogram`Windows`open miniprogram`Mac或调用微信开发者工具 CLI 打开 `miniprogram/`
**触发约定**:用户只说「转换」或「只转」→ 只做 13用户说「完整流程」或仅 @ 本文件 → 做 14。
---
## 二、项目结构对照(按规则推导,不写死页面)
**先扫描 `app/` 得到页面列表,再按规则生成小程序路径与四件套;新增 Web 页面时同样适用。**
1. **Web 路由 → 小程序页面路径**
- 通用:`app/<path>/page.tsx``pages/<name>/<name>``<name>` 取该路由**最后一层目录名**。
- 特例:
- `app/page.tsx``pages/index/index`
- 动态路由 `app/<a>/[id]/page.tsx``pages/<a>/<a>`,参数在 `onLoad(options)` 中取 `options.id` 等。
- 嵌套路由(如 `app/my/referral/page.tsx`)→ 单层页面 `pages/referral/referral`,避免深层路径。
- **新增**:每增加一个需上小程序的 `app/xxx/page.tsx`,就在 `miniprogram/pages/` 下新增 `pages/xxx/xxx` 四件套(.js/.json/.wxml/.wxss并在 `app.json``pages` 中追加 `"pages/xxx/xxx"`
2. **枚举要转换的页面**
- 遍历 `app/` 下所有含 `page.tsx` 的路径,**排除**`app/api/``app/admin/` 及仅 Web/后台用的路由。
- 对每个需上小程序的页面,按上条得到路径,确保 `miniprogram/pages/<name>/` 存在四件套且已在 `app.json``pages` 中注册。
3. **API**
- `app/api/*` 不转为小程序代码;小程序用 `wx.request` 调**同域名**接口(与 `miniprogram/utils`、baseURL 一致),**不写死 localhost**。接口路径、参数、返回格式与 `app/api/` 保持一致。
4. **tabBar**
- 仅首页、目录、找伙伴、我的等需底部 tab 的页面配置 `app.json``tabBar.list`;其余为普通页面。新增 tab 时在 `tabBar.list``pages` 中同步追加。
---
## 三、转换规则Web → 小程序)
**原则**:以 Web 为唯一真相来源,逐块对照,不猜测、不省略;无法 1:1 处用最接近实现并注释说明。
### 3.1 完整性要求(零丢失)
- **样式**:颜色、字体、字号、行高、间距、圆角、阴影、背景、边框与 Web 一致,在 WXSS 中完整实现。
- **按钮与可点击**:每个按钮、链接、可点击区域保留,文案、图标、跳转/弹窗/提交与 Web 一致;禁用态、加载态需体现。
- **布局与结构**:区块划分、顺序、折叠/展开、列表/卡片与 Web 一致,不漏模块。
- **图片与图标**Web 中出现的图片、图标、占位图在小程序侧存在并正确引用;**菜单、列表、统计、标签、按钮等处图标逐项对照补全**(可用 emoji 或图片),路径用 `miniprogram/images/` 或 assets。
- **表单**:输入框、选择器、校验、提交与 Web 一致,不丢字段与校验。
### 3.2 组件与语法
- **React/JSX → WXML**`wx:if``wx:for``bindtap` 等;**禁止在 WXML 中写 JS 方法**(见 4.1)。
- **Tailwind/CSS → WXSS**:逐条对照 Web 样式,可保留 class 名,视觉效果一致;主题色/字体与 `globals.css` 或设计一致。
- **状态与生命周期**`useState`/`useEffect` → Page 的 `data``onLoad``onShow` 等。
- **路由**`useRouter`/`Link``wx.navigateTo``wx.switchTab`tab 页用 switchTab
- **接口**`wx.request` + 项目 baseURL路径、参数、返回与 `app/api/` 一致。图片放 `miniprogram/images/` 或 assets引用用相对路径或 `/images/xxx`
---
## 四、踩坑与必做项(必须遵守)
以下为实际转换中的踩坑总结,转换与检查时**必须**按此处理,否则会出现编译错误、模拟器启动失败或界面被遮挡。
### 4.1 WXML 禁止在模板里调用 JS 方法
- **禁止**WXML 中不得出现任何 JS 方法调用,例如:`{{ (user.earnings || 0).toFixed(2) }}``{{ user.nickname.charAt(0) }}``{{ user.id.slice(-8) }}``{{ authorInfo.name.charAt(0) }}`会报「unexpected token」等编译错误。
- **必须**:在对应页的 `.js` 中(`onLoad``onShow``syncUser`、数据更新处)**预先计算**展示用字符串,写入 `data`WXML 只引用 data 变量。推荐命名示例:
- 金额两位小数 → `earningsText``balanceText` 等;
- 用户/作者首字 → `userInitial``authorInitial`
- 用户 ID 后几位 → `userIdSuffix`
### 4.2 启动不阻塞、不因网络报错导致模拟器启动失败
- **问题**`App.onLaunch` 里若依赖异步请求(如 `loadFeatureConfig``loadBookData`)且未处理好,或对无返回值的函数链式调用 `.catch()`,会导致「模拟器启动失败 / TypeError: Failed to fetch」或「Cannot read property 'catch' of undefined」。
- **做法**
- `onLaunch` 中**不要 await** 异步请求;只调用 `loadFeatureConfig().catch(() => {})`(仅对**返回 Promise** 的方法链式 catch`loadBookData()` 内部已有 catch 则**不要**写 `loadBookData().catch()`(避免对 undefined 调 catch
- `request``fail` 回调里统一打日志并 reject 友好错误,不把未捕获异常抛到启动流程。
- 本地调试可在 `project.config.json``setting` 中设 `"urlCheck": false`,避免域名未配置时请求被拦截;正式发布前再按需改回。
### 4.3 底部「找伙伴」Tab 默认不显示,避免闪一下再隐藏
- **问题**:若 custom-tab-bar 里「找伙伴」初始为 `hidden: false`,等接口返回 `matchEnabled: false` 后再隐藏,会先显示再消失,观感差。
- **做法**custom-tab-bar 的 `list` 里「找伙伴」项**默认 `hidden: true`**;在 `attached` 里先执行一次 `syncMatchEnabled()`(用当前 `globalData.matchEnabled`),再 `app.loadFeatureConfig().then(() => this.syncMatchEnabled())`,仅当接口返回 `matchEnabled === true` 时把该项设为 `hidden: false`
### 4.4 顶部安全区:状态栏 + 胶囊会遮挡,必须预留
- **问题**`navigationStyle: "custom"` 时,**状态栏**和**胶囊按钮**会覆盖页面顶部,标题、返回、按钮被遮挡或点不到。
- **必须**
- **占位高度**:统一使用 **`navBarHeight`**(不用固定 `statusBarHeight + 44`)。在 `App.getSystemInfo` 中用 `wx.getSystemInfoSync()` + `wx.getMenuButtonBoundingClientRect()` 计算 `navBarHeight`(状态栏 + 胶囊区域总高),无菜单按钮时回退 `statusBarHeight + 44`。每页占位条高度设为 `{{ navBarHeight }}px``onLoad`/`onShow``getApp().globalData``navBarHeight``statusBarHeight` 写入页面 `data`
- **头部右侧留白**:所有带标题/按钮的头部容器加 `.safe-header-right``app.wxss` 中定义 `padding-right: 200rpx; box-sizing: border-box;`),或使用 `globalData.capsulePaddingRight` 内联,避免被胶囊遮挡。
- **占位页统一模板**:无复杂导航的页面(如 `address-edit``address-list``purchases``referral``settings`)必须使用**同一套**顶部安全区:顶部占位条高度 `navBarHeight`,其内 `padding-top: {{ statusBarHeight || 44 }}px`,导航容器使用 `display: flex; flex-direction: column; justify-content: flex-end; box-sizing: border-box;`,并加 `safe-header-right`,保证标题与返回按钮不被遮挡且右侧留白一致。
### 4.5 图标与样式逐页对照,不得遗漏
- **问题**:转换后容易漏掉搜索图标、菜单图标、统计/标签图标、返回箭头等,导致与 Web 不一致。
- **做法**:逐页对照 Web`components/bottom-nav.tsx`、各 `app/**/page.tsx`
- **底部 Tab**:每个 tab 有图标(如 🏠📋👥👤 或图片),「找伙伴」若居中凸起需保留样式。
- **首页**搜索栏左侧有搜索图标Banner/卡片/列表中的箭头、标签与 Web 一致。
- **我的**:用户卡片「创业伙伴」旁有星标;收益卡片有收益图标;菜单项(订单、推广、关于、设置)各有图标;概览/我的足迹 Tab 及阅读统计、最近阅读、匹配记录等区块有对应图标或标题图标。
- **关于**:作者首字用 data 中的 `authorInitial`;标签(直播时间、平台)带图标;统计四项带图标;「加入派对群」按钮带图标;返回为「← 返回」。
- **阅读/搜索/目录/找伙伴**:返回、分享、锁、类型图标等与 Web 一致。所有导航返回统一用「← 返回」等可识别样式。
### 4.6 卡片与按钮布局错位(如「我的收益」与「推广中心」按钮)
- **问题**:卡片内标题行(如「我的收益」+「推广中心 ›」)与底部全宽按钮(如「推广中心 / 提现」)出现错位、溢出或与卡片边缘不对齐,与 Web 不一致。
- **做法**
- **盒子模型**:页面根容器与所有卡片统一加 `box-sizing: border-box`,避免 padding 导致总宽度超出或视觉偏移。
- **卡片内标题行**:若为 flex 布局(如左侧标题 + 右侧链接),给容器加 `gap``min-width: 0`;左侧标题区加 `flex-shrink: 0``min-width: 0`,标题与链接加 `white-space: nowrap`,右侧链接加 `flex-shrink: 0``white-space: nowrap`,防止挤压、换行或重叠错位。
- **全宽按钮**:卡片内的「全宽」按钮使用 `display: block``width: 100%``box-sizing: border-box`,保证与卡片内容区同宽、与上方内容左右对齐;不得因缺 box-sizing 或未 block 导致宽度计算错误而错位。
### 4.7 「我的」-「我的足迹」-「匹配记录」需随 matchEnabled 控制
- **要求**:与 Web 一致,当全局配置 `matchEnabled === false`「我的」页「我的足迹」Tab 下的「匹配记录」区块**不展示**;仅当 `matchEnabled === true` 时展示。小程序侧从 `getApp().globalData.matchEnabled` 读取,在 WXML 中用 `wx:if="{{ matchEnabled }}"` 控制该区块显隐,并在 `onShow` 或数据刷新时同步该值到页面 `data`
---
## 五、必须保留的小程序配置
- **AppID**`wxb8bbb2b10dec74aa`(见 `miniprogram/project.config.json``.cursorrules`)。
- **app.json**`pages``window``tabBar`(含 `custom: true` 时保留 `custom-tab-bar`)、`permission``requiredPrivateInfos` 等按现有或微信规范保留。
- **project.config.json**:保持现有编译与项目配置,不随意改 appid本地调试可设 `urlCheck: false`,见 4.2。
- **custom-tab-bar**:若使用自定义 tabBar保留 `custom-tab-bar` 组件实现,并遵守 4.3 的「找伙伴」默认隐藏规则。
---
## 六、转换完成后的自检清单
- [ ] **页面注册**`app.json``pages``miniprogram/pages/` 下目录、四件套一一对应,无缺页。
- [ ] **组件引用**:各页 `.json``usingComponents` 与自定义组件路径正确(若有)。
- [ ] **WXML 合规**:无语法错误;**WXML 中无 `.toFixed()``.charAt()``.slice()` 等 JS 方法**,展示用数值/字符串均已预先写入 data 并在模板中引用。
- [ ] **接口**baseURL 为线上或配置项,非 localhost仅本地调试可例外
- [ ] **tabBar**:与 Web 一级入口一致;**「找伙伴」项默认 `hidden: true`,仅当接口返回 `matchEnabled === true` 后显示**(见 4.3)。
- [ ] **顶部安全区**:所有自定义头部页使用 `navBarHeight` 占位,头部容器加 `safe-header-right`占位页address-edit、address-list、purchases、referral、settings使用统一顶部安全区模板见 4.4)。
- [ ] **卡片与按钮**:含标题行+链接+全宽按钮的卡片使用 `box-sizing: border-box`,标题行防挤压/换行,全宽按钮 `display: block``width: 100%``box-sizing: border-box`,与内容区左右对齐无错位(见 4.6)。
- [ ] **「我的足迹」-「匹配记录」**:该区块随 `globalData.matchEnabled` 显隐,`matchEnabled === false` 时不展示(见 4.7)。
- [ ] **完整性**:逐页对照 Web样式、按钮、链接、图片、**图标**、表单、列表/卡片无遗漏;无法 1:1 处已用最接近实现并注释说明。
---
## 七、转换完成后的步骤
转换完成后,**打开微信开发者工具**
- **方式一**:执行 `start miniprogram`Windows`open miniprogram`Mac打开文件夹`miniprogram` 文件夹拖入微信开发者工具导入项目。
- **方式二**:若已安装微信开发者工具 CLI可直接调用其打开项目如 Windows`"C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat" open --project miniprogram`)。
在微信开发者工具中可预览、调试;需要上传时,使用工具内的「上传」功能,或执行 `python scripts/autosysc-weixin.py`
---
## 八、参考文件位置
- **Web 对照**`app/**/page.tsx``components/`;接口路径、参数、返回格式参照 `app/api/`,保证与 Web 一致。
- **小程序结构**`miniprogram/app.json``miniprogram/pages/``miniprogram/utils/``miniprogram/custom-tab-bar/``miniprogram/app.js`
- **上传**`scripts/autosysc-weixin.py`(项目根运行,需先配置 `miniprogram/private.key`)。
- **配置说明**`miniprogram/小程序快速配置指南.md``miniprogram/小程序部署说明.md`
---
**当你被 @ 本文件时**:按「一、你的任务」顺序执行(枚举页面 → 转换 → 自检 → 可选打开开发者工具),并**严格遵循**二(结构对照)、三(转换规则)、四(踩坑必做项)、五(配置保留)、六(自检清单);四、六中的条目为必做项,不可省略。

View File

@@ -1,46 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15)
print("=== 检查 chunks 目录文件前20个===")
cmd = "ls -la /www/wwwroot/soul/.next/static/chunks/ 2>/dev/null | head -25"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "目录不存在")
print("\n=== 是否有 turbopack 文件 ===")
cmd = "find /www/wwwroot/soul/.next/static -name '*turbopack*' 2>/dev/null"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "无 turbopack 文件(正常,这是生产模式)")
print("\n=== 检查请求的具体文件 ===")
files_to_check = [
"a954454d2ab1d3ca.css",
"6a98f5c6b2554ef3.js",
"turbopack-0d89ab930ad9d74d.js",
]
for f in files_to_check:
cmd = "find /www/wwwroot/soul/.next/static -name '%s' 2>/dev/null" % f
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace').strip()
status = "[OK] 存在" if result else "[X] 不存在"
print("%s: %s" % (f, status))
print("\n=== 检查实际可用的 css 文件 ===")
cmd = "ls /www/wwwroot/soul/.next/static/css/ 2>/dev/null | head -10"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "无 css 文件")
print("\n=== 构建模式检查 ===")
cmd = "head -5 /www/wwwroot/soul/.next/BUILD_ID 2>/dev/null"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print("BUILD_ID: %s" % (result if result else "不存在"))
client.close()

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15)
print("=== soul.quwanzhi.com.conf ===")
stdin, stdout, stderr = client.exec_command('cat /www/server/panel/vhost/nginx/soul.quwanzhi.com.conf 2>/dev/null', timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "文件不存在")
print("\n=== 检查 include 配置 ===")
stdin, stdout, stderr = client.exec_command('ls -la /www/server/panel/vhost/nginx/ | grep soul', timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "无 soul 相关配置")
print("\n=== node_soul.conf ===")
stdin, stdout, stderr = client.exec_command('cat /www/server/panel/vhost/nginx/node_soul.conf 2>/dev/null', timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "文件不存在")
client.close()

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15)
print("=== PM2 soul 日志(最后 50 行)===")
cmd = "pm2 logs soul --lines 50 --nostream 2>&1 | tail -50"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
print("\n=== PM2 soul 错误日志 ===")
cmd = "pm2 logs soul --err --lines 30 --nostream 2>&1 | tail -30"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
print("\n=== 检查 server.js 文件 ===")
cmd = "ls -lh /www/wwwroot/soul/server.js"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 检查 .next 目录结构 ===")
cmd = "ls -lh /www/wwwroot/soul/.next/ | head -20"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 检查端口 30006 ===")
cmd = "curl -I http://127.0.0.1:30006 2>&1 | head -10"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
client.close()

View File

@@ -1,102 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快速检查服务器上静态资源是否存在
用于排查管理端 404 问题
"""
import os
import sys
try:
import paramiko
except ImportError:
print("请安装: pip install paramiko")
sys.exit(1)
# 配置(与 devlop.py 一致)
DEPLOY_PROJECT_PATH = os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul")
DEVLOP_DIST_PATH = "/www/wwwroot/auto-devlop/soul/dist"
DEFAULT_SSH_PORT = int(os.environ.get("DEPLOY_SSH_PORT", "22022"))
def get_cfg():
return {
"host": os.environ.get("DEPLOY_HOST", "42.194.232.22"),
"user": os.environ.get("DEPLOY_USER", "root"),
"password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"),
"ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""),
"project_path": os.environ.get("DEPLOY_PROJECT_PATH", DEPLOY_PROJECT_PATH),
}
def check_static_files():
cfg = get_cfg()
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
print("正在连接服务器...")
if cfg.get("ssh_key") and os.path.isfile(cfg["ssh_key"]):
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], key_filename=cfg["ssh_key"], timeout=15)
else:
client.connect(cfg["host"], port=DEFAULT_SSH_PORT, username=cfg["user"], password=cfg["password"], timeout=15)
print("\n=== 检查静态资源目录 ===")
# 检查多个可能的路径deploy 模式和 devlop 模式)
checks = [
("%s/.next/static" % DEVLOP_DIST_PATH, "devlop 模式 dist 目录"),
("%s/.next/static" % DEPLOY_PROJECT_PATH, "deploy 模式项目目录"),
("%s/server.js" % DEVLOP_DIST_PATH, "devlop server.js"),
("%s/server.js" % DEPLOY_PROJECT_PATH, "deploy server.js"),
]
for path, desc in checks:
# 检查文件或目录是否存在
cmd = "test -e '%s' && echo 'EXISTS' || echo 'NOT_FOUND'" % path
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode("utf-8", errors="replace").strip()
status = "[OK]" if "EXISTS" in result else "[X]"
print("%s %s" % (status, desc))
print(" 路径: %s" % path)
if "EXISTS" in result and "static" in path:
# 列出文件数量
cmd2 = "find '%s' -type f 2>/dev/null | wc -l" % path
stdin2, stdout2, stderr2 = client.exec_command(cmd2, timeout=10)
file_count = stdout2.read().decode("utf-8", errors="replace").strip()
print(" 文件数: %s" % file_count)
print("\n=== 检查 PM2 项目配置 ===")
cmd = "pm2 describe soul 2>/dev/null | grep -E 'cwd|script|status' | head -5 || echo 'PM2 soul 不存在'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
pm2_info = stdout.read().decode("utf-8", errors="replace").strip()
print(pm2_info)
print("\n=== 检查端口监听 ===")
cmd = "ss -tlnp | grep 30006 || echo '端口 30006 未监听'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
port_info = stdout.read().decode("utf-8", errors="replace").strip()
print(port_info)
print("\n=== 检查 Nginx 反向代理 ===")
cmd = "grep -r 'proxy_pass' /www/server/panel/vhost/nginx/*soul* 2>/dev/null | head -3 || echo '未找到 soul Nginx 配置'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
nginx_info = stdout.read().decode("utf-8", errors="replace").strip()
print(nginx_info)
print("\n" + "=" * 50)
print("诊断建议:")
print("1. devlop 模式部署后PM2 的 cwd 应为: %s" % DEVLOP_DIST_PATH)
print("2. .next/static 必须在 PM2 的 cwd 目录下")
print("3. Nginx 必须整站反代location /),不能只反代 /api")
print("4. 浏览器强刷: Ctrl+Shift+R 清除缓存")
except Exception as e:
print("错误: %s" % str(e))
import traceback
traceback.print_exc()
finally:
client.close()
if __name__ == "__main__":
check_static_files()

View File

@@ -34,9 +34,9 @@ except ImportError:
# ==================== 配置 ====================
# 端口统一从环境变量 DEPLOY_PORT 读取,未设置时使用此默认值
# 端口统一从环境变量 DEPLOY_PORT 读取,未设置时使用此默认值(需与 Nginx proxy_pass、ecosystem.config.cjs 一致)
DEPLOY_PM2_APP = "soul"
DEFAULT_DEPLOY_PORT = 3888
DEFAULT_DEPLOY_PORT = 30006
DEPLOY_PROJECT_PATH = "/www/wwwroot/soul"
DEPLOY_SITE_URL = "https://soul.quwanzhi.com"
# SSH 端口(支持环境变量 DEPLOY_SSH_PORT未设置时默认为 22022

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import paramiko
import time
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15)
print("=== 1. 检查端口占用 ===")
cmd = "ss -tlnp | grep ':300' | head -10"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "无 300x 端口监听")
print("\n=== 2. 检查 server.js 中的端口配置 ===")
cmd = "grep -n 'PORT\\|port\\|3006\\|30006' /www/wwwroot/soul/server.js | head -10"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 3. 检查环境变量配置 ===")
cmd = "cat /www/wwwroot/soul/.env 2>/dev/null | grep -i port || echo '无 .env 文件'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 4. 停止 PM2 soul ===")
cmd = "pm2 stop soul 2>&1"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
time.sleep(2)
print("\n=== 5. 杀死占用 3006 端口的进程 ===")
cmd = "lsof -ti:3006 | xargs kill -9 2>/dev/null || echo '无进程占用 3006'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 6. 杀死占用 30006 端口的进程 ===")
cmd = "lsof -ti:30006 | xargs kill -9 2>/dev/null || echo '无进程占用 30006'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
time.sleep(1)
print("\n=== 7. 确认端口已释放 ===")
cmd = "ss -tlnp | grep ':300'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "[OK] 端口已全部释放")
client.close()
print("\n" + "=" * 60)
print("下一步:修复 server.js 的端口配置为 30006")

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import paramiko
import time
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15)
print("=== PM2 restart soul ===")
stdin, stdout, stderr = client.exec_command('pm2 restart soul 2>&1', timeout=30)
result = stdout.read().decode('utf-8', errors='replace')
# 移除可能导致编码问题的特殊字符
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
print("\n=== 等待 3 秒 ===")
time.sleep(3)
print("=== PM2 status ===")
stdin, stdout, stderr = client.exec_command('pm2 status soul 2>&1', timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
client.close()
print("\n" + "=" * 50)
print("请在浏览器按 Ctrl+Shift+R 强制刷新页面!")

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import paramiko
import time
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('42.194.232.22', port=22022, username='root', password='Zhiqun1984', timeout=15)
print("=== 1. 杀死所有相关进程 ===")
cmd = "kill -9 1822 2>/dev/null || echo 'Process 1822 already killed'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
time.sleep(1)
print("\n=== 2. 确认端口清理完成 ===")
cmd = "ss -tlnp | grep ':300' || echo '[OK] All ports cleared'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 3. 删除 PM2 soul 配置 ===")
cmd = "pm2 delete soul 2>&1"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
time.sleep(1)
print("\n=== 4. 使用正确配置重新启动 ===")
cmd = """cd /www/wwwroot/soul && PORT=30006 pm2 start server.js --name soul --update-env 2>&1"""
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
time.sleep(3)
print("\n=== 5. 检查 PM2 状态 ===")
cmd = "pm2 status soul 2>&1"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
print("\n=== 6. 确认端口 30006 监听 ===")
cmd = "ss -tlnp | grep ':30006'"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result if result else "[X] Port 30006 not listening!")
print("\n=== 7. 测试 HTTP 响应 ===")
cmd = "curl -I http://127.0.0.1:30006 2>&1 | head -5"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
print(result)
print("\n=== 8. 查看最新日志 ===")
cmd = "pm2 logs soul --lines 10 --nostream 2>&1 | tail -15"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
print("\n=== 9. 保存 PM2 配置 ===")
cmd = "pm2 save 2>&1"
stdin, stdout, stderr = client.exec_command(cmd, timeout=10)
result = stdout.read().decode('utf-8', errors='replace')
result = result.encode('ascii', errors='replace').decode('ascii')
print(result)
client.close()
print("\n" + "=" * 60)
print("完成!请在浏览器访问: https://soul.quwanzhi.com")
print("如果仍是空白,按 Ctrl+Shift+R 强制刷新")

View File

@@ -1,240 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
订单状态同步任务(兜底机制)
功能:
1. 定时查询 'created' 状态的订单
2. 调用微信支付接口查询真实状态
3. 同步订单状态paid / expired
4. 更新用户购买记录
运行方式:
- 手动: python scripts/sync_order_status.py
- 定时: crontab -e 添加 "*/5 * * * * python /path/to/sync_order_status.py"
- Node.js: 使用 node-cron 定时调用
"""
import sys
import os
import json
import time
import hashlib
import random
import string
from datetime import datetime, timedelta
try:
import pymysql
import requests
except ImportError:
print("[ERROR] 缺少依赖库,请安装:")
print(" pip install pymysql requests")
sys.exit(1)
# 数据库配置
DB = {
"host": "56b4c23f6853c.gz.cdb.myqcloud.com",
"port": 14413,
"user": "cdb_outerroot",
"password": "Zhiqun1984",
"database": "soul_miniprogram",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
"connect_timeout": 15,
}
# 微信支付配置(从环境变量或配置文件读取)
WECHAT_PAY_CONFIG = {
"appid": os.environ.get("WECHAT_APPID", "wxb8bbb2b10dec74aa"),
"mch_id": os.environ.get("WECHAT_MCH_ID", "1318592501"),
"api_key": os.environ.get("WECHAT_API_KEY", "YOUR_API_KEY_HERE"), # 需要配置真实的 API Key
}
# 订单超时时间(分钟)
ORDER_TIMEOUT_MINUTES = 30
def log(message, level="INFO"):
"""统一日志输出"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] [{level}] {message}")
def generate_nonce_str(length=32):
"""生成随机字符串"""
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
def create_sign(params, api_key):
"""生成微信支付签名"""
# 1. 参数排序
sorted_params = sorted(params.items())
# 2. 拼接字符串
string_a = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
string_sign_temp = f"{string_a}&key={api_key}"
# 3. MD5 加密并转大写
sign = hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()
return sign
def query_wechat_order_status(out_trade_no):
"""
查询微信支付订单状态
文档: https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_2
"""
url = "https://api.mch.weixin.qq.com/pay/orderquery"
params = {
"appid": WECHAT_PAY_CONFIG["appid"],
"mch_id": WECHAT_PAY_CONFIG["mch_id"],
"out_trade_no": out_trade_no,
"nonce_str": generate_nonce_str(),
}
# 生成签名
params["sign"] = create_sign(params, WECHAT_PAY_CONFIG["api_key"])
# 构建 XML 请求体
xml_data = "<xml>"
for key, value in params.items():
xml_data += f"<{key}>{value}</{key}>"
xml_data += "</xml>"
try:
response = requests.post(url, data=xml_data.encode('utf-8'), headers={'Content-Type': 'application/xml'}, timeout=10)
# 解析 XML 响应(简单处理,生产环境建议用 xml.etree.ElementTree
resp_text = response.text
# 提取关键字段
if '<return_code><![CDATA[SUCCESS]]></return_code>' in resp_text:
if '<trade_state><![CDATA[SUCCESS]]></trade_state>' in resp_text:
return 'SUCCESS'
elif '<trade_state><![CDATA[NOTPAY]]></trade_state>' in resp_text:
return 'NOTPAY'
elif '<trade_state><![CDATA[CLOSED]]></trade_state>' in resp_text:
return 'CLOSED'
elif '<trade_state><![CDATA[REFUND]]></trade_state>' in resp_text:
return 'REFUND'
else:
return 'UNKNOWN'
else:
log(f"查询订单失败: {resp_text}", "WARN")
return 'ERROR'
except Exception as e:
log(f"查询微信订单异常: {e}", "ERROR")
return 'ERROR'
def sync_order_status():
"""同步订单状态(主函数)"""
log("========== 订单状态同步任务开始 ==========")
conn = pymysql.connect(**DB)
cursor = conn.cursor()
try:
# 1. 查询所有 'created' 状态的订单(最近 2 小时内创建的)
two_hours_ago = datetime.now() - timedelta(hours=2)
cursor.execute("""
SELECT id, order_sn, user_id, product_type, product_id, amount, created_at
FROM orders
WHERE status = 'created' AND created_at >= %s
ORDER BY created_at DESC
""", (two_hours_ago,))
pending_orders = cursor.fetchall()
if not pending_orders:
log("没有需要同步的订单")
return
log(f"找到 {len(pending_orders)} 个待同步订单")
synced_count = 0
expired_count = 0
for order in pending_orders:
order_sn = order['order_sn']
created_at = order['created_at']
# 2. 判断订单是否超时(超过 30 分钟)
time_diff = datetime.now() - created_at
if time_diff > timedelta(minutes=ORDER_TIMEOUT_MINUTES):
# 超时订单:标记为 expired
log(f"订单 {order_sn} 超时 ({time_diff.seconds // 60} 分钟),标记为 expired")
cursor.execute("""
UPDATE orders
SET status = 'expired', updated_at = NOW()
WHERE order_sn = %s
""", (order_sn,))
expired_count += 1
continue
# 3. 查询微信支付状态(跳过,因为需要真实 API Key
# 生产环境中取消下面的注释
"""
log(f"查询订单 {order_sn} 的微信支付状态...")
wechat_status = query_wechat_order_status(order_sn)
if wechat_status == 'SUCCESS':
# 微信支付成功,更新本地订单为 paid
log(f"订单 {order_sn} 微信支付成功,更新为 paid")
cursor.execute('''
UPDATE orders
SET status = 'paid', updated_at = NOW()
WHERE order_sn = %s
''', (order_sn,))
# 更新用户购买记录
if order['product_type'] == 'fullbook':
cursor.execute('''
UPDATE users
SET has_full_book = 1
WHERE id = %s
''', (order['user_id'],))
synced_count += 1
elif wechat_status == 'NOTPAY':
log(f"订单 {order_sn} 尚未支付,保持 created 状态")
elif wechat_status == 'CLOSED':
log(f"订单 {order_sn} 已关闭,标记为 cancelled")
cursor.execute('''
UPDATE orders
SET status = 'cancelled', updated_at = NOW()
WHERE order_sn = %s
''', (order_sn,))
else:
log(f"订单 {order_sn} 查询失败或状态未知: {wechat_status}", "WARN")
"""
# 测试环境:模拟查询(跳过微信接口)
log(f"[TEST] 订单 {order_sn} 跳过微信查询(需配置 API Key")
conn.commit()
log(f"同步完成: 同步 {synced_count} 个,超时 {expired_count}")
except Exception as e:
conn.rollback()
log(f"同步失败: {e}", "ERROR")
import traceback
traceback.print_exc()
finally:
cursor.close()
conn.close()
log("========== 订单状态同步任务结束 ==========\n")
if __name__ == "__main__":
sync_order_status()