同步数据

This commit is contained in:
乘风
2026-02-02 18:27:48 +08:00
parent 46b052d6b4
commit 2aad5d55fd
26 changed files with 781 additions and 220 deletions

View File

@@ -7,13 +7,13 @@
# 方式1: 本地开发启动pnpm start
# 在终端中设置:
# Windows PowerShell: $env:PORT=3006; pnpm start
# Windows CMD: set PORT=3006 && pnpm start
# Linux/Mac: PORT=3006 pnpm start
# Windows PowerShell: $env:PORT=30006; pnpm start
# Windows CMD: set PORT=30006 && pnpm start
# Linux/Mac: PORT=30006 pnpm start
# 方式2: Docker Compose 部署
# 设置 APP_PORT 变量,容器内外端口都使用此值
APP_PORT=3006
APP_PORT=30006
# 方式3: Docker 直接运行
# docker run -e PORT=3007 -p 3007:3007 soul-book
@@ -21,7 +21,7 @@ APP_PORT=3006
# ========================================
# 多项目端口规划建议
# ========================================
# soul-book: 3006
# soul-book: 30006
# other-project: 3007
# api-service: 3008
# ...

View File

@@ -11,7 +11,7 @@
- ✅ 使用 `standalone` 模式,构建产物独立完整
- ✅ 使用 pnpm 包管理器
- ✅ 已配置 PM2 启动方式(`node server.js`
- ✅ 端口配置为 3006
- ✅ 端口配置为 30006
## 🔧 配置步骤

View File

@@ -83,8 +83,8 @@ python scripts/deploy_baota_pure_api.py --task-id 1 # 触发计划任务 ID=1
若服务器上尚未有代码,需先在宝塔上:
1. 在网站目录(如 `/www/wwwroot/soul`)创建目录,或从本地上传/克隆代码。
2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node server.js`,环境变量 `PORT=3006`
3. 配置 Nginx 反向代理到 `127.0.0.1:3006`,并绑定域名 soul.quwanzhi.com。
2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node server.js`,环境变量 `PORT=30006`
3. 配置 Nginx 反向代理到 `127.0.0.1:30006`,并绑定域名 soul.quwanzhi.com。
4. 之后日常部署执行 `python scripts/devlop.py` 即可。
---

View File

@@ -56,7 +56,7 @@ USER nextjs
# 端口由环境变量指定,不设默认值避免冲突
# 部署时通过 docker run -e PORT=xxxx 或 docker-compose 设置
EXPOSE ${PORT:-3006}
EXPOSE ${PORT:-30006}
ENV HOSTNAME "0.0.0.0"

139
README-Phase4.md Normal file
View File

@@ -0,0 +1,139 @@
# Phase 4 完成总结
## 概述
Phase 4 成功迁移了"找伙伴"、"搜索"页面,实现了底部 TabBar 导航与安全区适配,完成了 Next.js C 端应用的**全量页面迁移**。
---
## 完成的核心功能
### 1. 找伙伴页AI 智能匹配)
- **匹配类型**:创业合伙、资源对接、导师顾问、团队招募
- **匹配逻辑**
- 每日免费 1 次
- 购买章节获得更多次数
- 全书用户无限匹配
- **匹配结果**:匹配度、创业理念、共同兴趣、微信号
- **加入池功能**:提交手机号加入匹配池
- **次数展示**:剩余次数、已用次数、解锁提示
### 2. 搜索页(章节检索)
- **实时搜索**:输入关键词实时过滤章节
- **搜索结果**:显示所属篇章、标题、免费标签
- **跳转阅读**:点击结果直接进入阅读页
- **空态处理**:无搜索词、无结果的友好提示
### 3. 底部 TabBar 导航
- **4 个 Tab**:首页🏠、目录📚、找伙伴👥、我的👤
- **激活态**:当前页 Tab 高亮显示(#00CED1
- **动态显示**:找伙伴 Tab 根据 `matchEnabled` 配置显示/隐藏
- **安全区适配**`paddingBottom = env(safe-area-inset-bottom)`
- **跨端路由**:小程序用 `wx.switchTab`Web 用 `window.location.href`
### 4. 各页面集成底部导航
在以下 4 个 Tab 页添加了 `<BottomNav />` 组件:
- HomePage (current="/")
- ChaptersPage (current="/chapters")
- MatchPage (current="/match")
- MyPage (current="/my")
---
## 文件清单
**页面组件**
- `src/pages/MatchPage.jsx`(找伙伴)
- `src/pages/SearchPage.jsx`(搜索)
**入口文件**
- `src/match.jsx`
- `src/search.jsx`
**公共组件**
- `src/components/BottomNav.jsx`(底部导航)
**配置更新**
- `build/webpack.mp.config.js`(新增 match、search 入口)
- `build/miniprogram.config.js`router.other 新增 /match、/search
---
## 完整页面映射表Phase 1-4
| Next 路由 | 小程序页面 | 入口文件 | 状态 |
|-----------|-----------|----------|------|
| app/page.tsx | pages/index/index | src/index.jsx | ✅ Phase 2 |
| app/chapters/page.tsx | pages/chapters/chapters | src/chapters.jsx | ✅ Phase 2 |
| app/read/[id]/page.tsx | pages/read/read | src/read.jsx | ✅ Phase 2 |
| app/my/page.tsx | pages/my/my | src/my.jsx | ✅ Phase 3 |
| app/my/referral/page.tsx | pages/referral/referral | src/referral.jsx | ✅ Phase 3 |
| app/my/settings/page.tsx | pages/settings/settings | src/settings.jsx | ✅ Phase 3 |
| app/my/purchases/page.tsx | pages/purchases/purchases | src/purchases.jsx | ✅ Phase 3 |
| app/about/page.tsx | pages/about/about | src/about.jsx | ✅ Phase 3 |
| app/match/page.tsx | pages/match/match | src/match.jsx | ✅ Phase 4 |
| app/search/page.tsx | pages/search/search | src/search.jsx | ✅ Phase 4 |
**C 端页面 100% 迁移完成!**
---
## 安全区适配
### 底部安全区
```css
paddingBottom: 'env(safe-area-inset-bottom)'
```
确保在有底部刘海的设备(如 iPhone XTabBar 不被遮挡。
### 顶部安全区
小程序自动处理 statusBar无需额外适配。若使用自定义导航栏可读取 `app.globalData.navBarHeight`
### 右侧安全区(胶囊按钮)
若使用自定义导航栏,需在右侧预留 `capsulePaddingRight`,避免遮挡胶囊按钮。
---
## 测试与验收
1. **构建**`cd newpp && npm run build:mp`
2. **合并**`node scripts/merge-kbone-to-miniprogram.js`
3. **测试路径**
- **TabBar 切换**:首页 ↔ 目录 ↔ 找伙伴 ↔ 我的
- **找伙伴流程**:选择类型 → 开始匹配 → 查看结果 → 复制微信 → 加入池
- **搜索流程**:首页 → 搜索 icon或直接访问 /search→ 输入关键词 → 点击结果 → 阅读
- **底部导航**:各 Tab 页底部导航高亮正确、点击切换正常
---
## 当前进度
-**Phase 1**:搭架子(适配层、构建、首页/目录/阅读占位)
-**Phase 2**核心页阅读页接口、ChapterContent、完整目录
-**Phase 3**我的与子页Zustand、我的、推广、设置、购买记录、关于
-**Phase 4**找伙伴与其余match、search、底部 tabBar、安全区
-**Phase 5**:收尾(全量自检、样式对齐、发布流程)
---
## 下一步
进入 Phase 5 收尾阶段:
1. **全量自检**对照「Web转小程序并上传-提示词」逐项检查
2. **样式对齐**:确保颜色、间距、圆角、阴影与 Web 一致
3. **踩坑修复**
- WXML 不能调用 JS 方法
- 启动不阻塞async onLaunch
- safe-area 边界处理
- TabBar 默认隐藏项逻辑
4. **发布流程**:预览码 → 体验版 → 提审 → 发布
---
**Phase 1-4 已完成 C 端全量迁移,所有核心功能已就位!**

View File

@@ -8,32 +8,32 @@ services:
container_name: soul_book_app
restart: always
ports:
- "${APP_PORT:-3006}:${APP_PORT:-3006}"
- "${APP_PORT:-30006}:${APP_PORT:-30006}"
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
- PORT=${APP_PORT:-3006}
- 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:-3006}/payment/success}
- ALIPAY_NOTIFY_URL=${ALIPAY_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-3006}/api/payment/alipay/notify}
- 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:-3006}/api/payment/wechat/notify}
- 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:-3006}}
- 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:-3006}"]
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:${APP_PORT:-30006}"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -1,7 +1,7 @@
/**
* PM2 配置:用于 standalone 部署的服务器
* 启动方式node server.js不要用 npm start / next startstandalone 无 next 命令)
* 使用pm2 start ecosystem.config.cjs 或 PORT=3006 pm2 start server.js --name soul
* 使用pm2 start ecosystem.config.cjs 或 PORT=30006 pm2 start server.js --name soul
*/
module.exports = {
apps: [
@@ -11,7 +11,7 @@ module.exports = {
interpreter: 'node',
env: {
NODE_ENV: 'production',
PORT: 3006,
PORT: 30006,
},
cwd: undefined, // 以当前目录为准,部署时在 /www/wwwroot/soul
},

View File

@@ -1,68 +1,117 @@
// pages/address-edit/address-edit.js
const app = getApp()
Page({
data: {
statusBarHeight: 44,
navBarHeight: 88
navBarHeight: 88,
id: '',
isEdit: false,
name: '',
phone: '',
province: '',
city: '',
district: '',
detail: '',
isDefault: false,
loading: false,
saving: false
},
onLoad(options) {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
const id = (options && options.id) ? decodeURIComponent(options.id) : ''
this.setData({ statusBarHeight, navBarHeight, id, isEdit: !!id })
if (id) this.loadAddress(id)
},
loadAddress(id) {
this.setData({ loading: true })
app.request('/api/user/addresses/' + encodeURIComponent(id))
.then(res => {
const item = res && res.item ? res.item : null
if (!item) {
this.setData({ loading: false })
return
}
this.setData({
loading: false,
name: item.name || '',
phone: item.phone || '',
province: item.province || '',
city: item.city || '',
district: item.district || '',
detail: item.detail || '',
isDefault: !!item.isDefault
})
})
.catch(() => this.setData({ loading: false }))
},
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
onNameInput(e) { this.setData({ name: (e.detail && e.detail.value) || '' }) },
onPhoneInput(e) { this.setData({ phone: (e.detail && e.detail.value) || '' }) },
onProvinceInput(e) { this.setData({ province: (e.detail && e.detail.value) || '' }) },
onCityInput(e) { this.setData({ city: (e.detail && e.detail.value) || '' }) },
onDistrictInput(e) { this.setData({ district: (e.detail && e.detail.value) || '' }) },
onDetailInput(e) { this.setData({ detail: (e.detail && e.detail.value) || '' }) },
onDefaultChange(e) { this.setData({ isDefault: !!e.detail.value }) },
submit() {
const { id, isEdit, name, phone, province, city, district, detail, isDefault } = this.data
const user = app.globalData.userInfo
if (!user || !user.id) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
if (!name || !name.trim()) {
wx.showToast({ title: '请输入姓名', icon: 'none' })
return
}
if (!phone || !phone.trim()) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
return
}
if (!/^1[3-9]\d{9}$/.test(phone.trim())) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (!detail || !detail.trim()) {
wx.showToast({ title: '请输入详细地址', icon: 'none' })
return
}
this.setData({ saving: true })
const body = {
userId: user.id,
name: name.trim(),
phone: phone.trim(),
province: (province || '').trim(),
city: (city || '').trim(),
district: (district || '').trim(),
detail: detail.trim(),
isDefault: !!isDefault
}
if (isEdit && id) {
app.request('/api/user/addresses/' + encodeURIComponent(id), {
method: 'PUT',
data: body
}).then(() => {
this.setData({ saving: false })
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
}).catch(() => this.setData({ saving: false }))
} else {
app.request('/api/user/addresses', {
method: 'POST',
data: body
}).then(() => {
this.setData({ saving: false })
wx.showToast({ title: '添加成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
}).catch(() => this.setData({ saving: false }))
}
}
})
})

View File

@@ -2,7 +2,49 @@
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">地址编辑</text>
<text class="header-title">{{isEdit ? '编辑地址' : '新增地址'}}</text>
</view>
<view class="placeholder-body">待开发</view>
<block wx:if="{{loading}}">
<view class="empty-wrap">
<text class="empty-desc">加载中...</text>
</view>
</block>
<block wx:else>
<view class="form">
<view class="form-item">
<text class="form-label">姓名</text>
<input class="form-input" placeholder="请输入姓名" value="{{name}}" bindinput="onNameInput" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" type="number" maxlength="11" placeholder="请输入11位手机号" value="{{phone}}" bindinput="onPhoneInput" />
</view>
<view class="form-item">
<text class="form-label">省份</text>
<input class="form-input" placeholder="选填" value="{{province}}" bindinput="onProvinceInput" />
</view>
<view class="form-item">
<text class="form-label">城市</text>
<input class="form-input" placeholder="选填" value="{{city}}" bindinput="onCityInput" />
</view>
<view class="form-item">
<text class="form-label">区/县</text>
<input class="form-input" placeholder="选填" value="{{district}}" bindinput="onDistrictInput" />
</view>
<view class="form-item">
<text class="form-label">详细地址</text>
<textarea class="form-textarea" placeholder="街道、门牌号等" value="{{detail}}" bindinput="onDetailInput" />
</view>
<view class="form-item row">
<text class="form-label">设为默认地址</text>
<switch checked="{{isDefault}}" bindchange="onDefaultChange" color="#00CED1" />
</view>
</view>
<view class="btn-save {{saving ? 'disabled' : ''}}" bindtap="submit">
{{saving ? '保存中...' : '保存'}}
</view>
</block>
</view>

View File

@@ -1,8 +1,20 @@
page { background: #000; color: #fff; }
.page { min-height: 100vh; }
.page { min-height: 100vh; padding-bottom: 80rpx; box-sizing: border-box; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
.container { padding: 32rpx; }
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); }
.form { padding: 32rpx; }
.form-item { margin-bottom: 32rpx; }
.form-item.row { display: flex; align-items: center; justify-content: space-between; }
.form-label { font-size: 28rpx; color: rgba(255,255,255,0.6); display: block; margin-bottom: 16rpx; }
.form-item.row .form-label { margin-bottom: 0; }
.form-input { width: 100%; padding: 24rpx 32rpx; border-radius: 16rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.1); color: #fff; font-size: 28rpx; box-sizing: border-box; }
.form-textarea { width: 100%; min-height: 160rpx; padding: 24rpx 32rpx; border-radius: 16rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.1); color: #fff; font-size: 28rpx; box-sizing: border-box; }
.btn-save { margin: 32rpx; padding: 28rpx; border-radius: 24rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; text-align: center; box-sizing: border-box; }
.btn-save.disabled { opacity: 0.5; }

View File

@@ -1,68 +1,80 @@
// pages/address-list/address-list.js
const app = getApp()
Page({
data: {
statusBarHeight: 44,
navBarHeight: 88
navBarHeight: 88,
user: null,
list: [],
loading: true
},
onLoad(options) {
onLoad() {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
this.syncUser()
this.loadList()
},
onShow() {
this.loadList()
},
syncUser() {
const user = app.globalData.userInfo || null
this.setData({ user })
},
loadList() {
const user = app.globalData.userInfo
if (!user || !user.id) {
this.setData({ list: [], loading: false })
return
}
this.setData({ loading: true })
app.request('/api/user/addresses?userId=' + encodeURIComponent(user.id))
.then(res => {
const list = (res && res.list) ? res.list : []
this.setData({ list, loading: false })
})
.catch(() => this.setData({ loading: false }))
},
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
goAdd() {
wx.navigateTo({ url: '/pages/address-edit/address-edit' })
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
goEdit(e) {
const id = e.currentTarget.dataset.id
if (id) wx.navigateTo({ url: '/pages/address-edit/address-edit?id=' + encodeURIComponent(id) })
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
deleteAddr(e) {
const id = e.currentTarget.dataset.id
if (!id) return
const that = this
wx.showModal({
title: '提示',
content: '确定要删除该收货地址吗?',
success(res) {
if (!res.confirm) return
app.request('/api/user/addresses/' + encodeURIComponent(id), { method: 'DELETE' })
.then(data => {
if (data && data.success) {
const list = that.data.list.filter(item => item.id !== id)
that.setData({ list })
wx.showToast({ title: '已删除', icon: 'success' })
} else {
wx.showToast({ title: (data && data.message) ? data.message : '删除失败', icon: 'none' })
}
})
.catch(() => wx.showToast({ title: '删除失败', icon: 'none' }))
}
})
}
})
})

View File

@@ -2,7 +2,50 @@
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">地址列表</text>
<text class="header-title">收货地址</text>
</view>
<view class="placeholder-body">待开发</view>
<block wx:if="{{!user}}">
<view class="empty-wrap">
<text class="empty-desc">请先登录</text>
<view class="btn-primary" bindtap="goBack">去登录</view>
</view>
</block>
<block wx:elif="{{loading}}">
<view class="empty-wrap">
<text class="empty-desc">加载中...</text>
</view>
</block>
<block wx:elif="{{list.length === 0}}">
<view class="empty-wrap">
<text class="empty-icon">📍</text>
<text class="empty-desc">暂无收货地址</text>
<text class="empty-hint">点击下方按钮添加</text>
</view>
</block>
<block wx:else>
<view class="addr-list">
<view
class="addr-card"
wx:for="{{list}}"
wx:key="id"
>
<view class="addr-row">
<text class="addr-name">{{item.name}}</text>
<text class="addr-phone">{{item.phone}}</text>
<text class="addr-default" wx:if="{{item.isDefault}}">默认</text>
</view>
<text class="addr-full">{{item.fullAddress}}</text>
<view class="addr-actions">
<view class="addr-btn" data-id="{{item.id}}" bindtap="goEdit">编辑</view>
<view class="addr-btn danger" data-id="{{item.id}}" bindtap="deleteAddr">删除</view>
</view>
</view>
</view>
</block>
<view class="btn-add" wx:if="{{user}}" bindtap="goAdd"> 新增收货地址</view>
</view>

View File

@@ -1,8 +1,25 @@
page { background: #000; color: #fff; }
.page { min-height: 100vh; }
.page { min-height: 100vh; padding-bottom: 160rpx; box-sizing: border-box; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
.container { padding: 32rpx; }
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
.empty-icon { font-size: 96rpx; display: block; margin-bottom: 24rpx; opacity: 0.5; }
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 16rpx; }
.empty-hint { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; }
.btn-primary { display: inline-block; padding: 24rpx 64rpx; border-radius: 48rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; }
.addr-list { padding: 32rpx; }
.addr-card { padding: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); margin-bottom: 24rpx; box-sizing: border-box; }
.addr-row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; flex-wrap: wrap; }
.addr-name { font-size: 30rpx; color: #fff; font-weight: 500; }
.addr-phone { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.addr-default { font-size: 22rpx; padding: 4rpx 16rpx; border-radius: 8rpx; background: rgba(0,206,209,0.2); color: #00CED1; }
.addr-full { font-size: 26rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; margin-bottom: 24rpx; }
.addr-actions { display: flex; justify-content: flex-end; gap: 32rpx; padding-top: 24rpx; border-top: 2rpx solid rgba(255,255,255,0.05); }
.addr-btn { font-size: 28rpx; color: #00CED1; }
.addr-btn.danger { color: #f87171; }
.btn-add { position: fixed; bottom: 0; left: 0; right: 0; margin: 32rpx; padding: 28rpx; border-radius: 24rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; text-align: center; box-sizing: border-box; }

View File

@@ -1,68 +1,104 @@
// pages/referral/referral.js
const app = getApp()
Page({
data: {
statusBarHeight: 44,
navBarHeight: 88
navBarHeight: 88,
isLoggedIn: false,
user: null,
totalEarnings: '0.00',
pendingEarnings: '0.00',
referralCode: '',
distributorShare: 90,
canWithdraw: false
},
onLoad(options) {
onLoad() {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
this.syncUser()
},
onShow() {
this.syncUser()
},
syncUser() {
const isLoggedIn = !!app.globalData.isLoggedIn
const user = app.globalData.userInfo || null
if (!user) {
this.setData({ isLoggedIn: false, user: null })
return
}
const total = Number(user.earnings != null ? user.earnings : 0)
const totalEarnings = total.toFixed(2)
const pendingEarnings = Number(user.pendingEarnings != null ? user.pendingEarnings : 0).toFixed(2)
const referralCode = user.referralCode || ''
this.setData({
isLoggedIn: true,
user,
totalEarnings,
pendingEarnings,
referralCode,
canWithdraw: total >= 10
})
},
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
copyLink() {
const user = app.globalData.userInfo
if (!user || !user.referralCode) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const baseUrl = app.globalData.baseUrl || 'https://soul.quwanzhi.com'
const link = baseUrl + '?ref=' + user.referralCode
wx.setClipboardData({
data: link,
success: () => wx.showToast({ title: '链接已复制', icon: 'success' })
})
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
shareToMoments() {
const user = app.globalData.userInfo
if (!user || !user.referralCode) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const baseUrl = app.globalData.baseUrl || 'https://soul.quwanzhi.com'
const link = baseUrl + '?ref=' + user.referralCode
const text = `📖 推荐一本好书《一场SOUL的创业实验场》
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事55个真实案例讲透创业的底层逻辑。
👉 点击阅读: ${link}
#创业 #商业思维 #Soul派对`
wx.setClipboardData({
data: text,
success: () => wx.showToast({ title: '朋友圈文案已复制', icon: 'success' })
})
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
applyWithdraw() {
const user = app.globalData.userInfo
if (!user) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const total = Number(user.earnings != null ? user.earnings : 0)
if (total < 10) {
wx.showToast({ title: '满10元可提现', icon: 'none' })
return
}
wx.showToast({
title: '请在小程序内联系客服或使用提现功能',
icon: 'none',
duration: 2500
})
}
})
})

View File

@@ -4,5 +4,59 @@
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">推广中心</text>
</view>
<view class="placeholder-body">待开发</view>
<block wx:if="{{!isLoggedIn}}">
<view class="empty-wrap">
<text class="empty-desc">请先登录</text>
<view class="btn-primary" bindtap="goBack">返回我的</view>
</view>
</block>
<block wx:else>
<view class="earnings-card">
<view class="earnings-head">
<view class="earnings-title-row">
<text class="earnings-icon">💰</text>
<view>
<text class="earnings-label">累计收益</text>
<text class="earnings-rate">{{distributorShare}}% 返利</text>
</view>
</view>
<view class="earnings-right">
<text class="earnings-total">¥{{totalEarnings}}</text>
<text class="earnings-pending">待结算: ¥{{pendingEarnings}}</text>
</view>
</view>
<view class="btn-withdraw {{canWithdraw ? '' : 'disabled'}}" bindtap="applyWithdraw">
{{canWithdraw ? '申请提现' : '满10元可提现'}}
</view>
</view>
<view class="code-card">
<view class="code-row">
<text class="code-label">我的邀请码</text>
<text class="code-value">{{referralCode}}</text>
</view>
<text class="code-desc">好友通过你的链接购买立省5%,你获得{{distributorShare}}%收益</text>
</view>
<view class="action-list">
<view class="action-item" bindtap="copyLink">
<view class="action-icon">🔗</view>
<view class="action-text">
<text class="action-title">复制邀请链接</text>
<text class="action-desc">分享给好友购买</text>
</view>
<text class="action-arrow"></text>
</view>
<view class="action-item" bindtap="shareToMoments">
<view class="action-icon wechat">💬</view>
<view class="action-text">
<text class="action-title">分享到朋友圈</text>
<text class="action-desc">复制文案发朋友圈</text>
</view>
<text class="action-arrow"></text>
</view>
</view>
</block>
</view>

View File

@@ -1,8 +1,38 @@
page { background: #000; color: #fff; }
.page { min-height: 100vh; }
.page { min-height: 100vh; padding-bottom: 80rpx; box-sizing: border-box; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
.container { padding: 32rpx; }
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
.btn-primary { display: inline-block; padding: 24rpx 64rpx; border-radius: 48rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; }
.earnings-card { margin: 32rpx; padding: 32rpx; border-radius: 32rpx; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); border: 2rpx solid rgba(0,206,209,0.2); box-sizing: border-box; }
.earnings-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; gap: 24rpx; min-width: 0; }
.earnings-title-row { display: flex; align-items: center; gap: 16rpx; min-width: 0; }
.earnings-icon { font-size: 40rpx; flex-shrink: 0; }
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; }
.earnings-rate { font-size: 24rpx; color: #00CED1; display: block; margin-top: 4rpx; }
.earnings-right { text-align: right; flex-shrink: 0; }
.earnings-total { font-size: 56rpx; font-weight: 700; color: #fff; display: block; }
.earnings-pending { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.btn-withdraw { width: 100%; padding: 24rpx; border-radius: 24rpx; background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; text-align: center; margin-top: 16rpx; box-sizing: border-box; }
.btn-withdraw.disabled { opacity: 0.5; background: #2c2c2e; color: rgba(255,255,255,0.5); }
.code-card { margin: 32rpx; padding: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); }
.code-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
.code-label { font-size: 30rpx; color: #fff; font-weight: 500; }
.code-value { font-size: 28rpx; color: #00CED1; font-family: monospace; background: rgba(0,206,209,0.1); padding: 12rpx 24rpx; border-radius: 16rpx; }
.code-desc { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.action-list { margin: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); overflow: hidden; }
.action-item { display: flex; align-items: center; padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.action-item:last-child { border-bottom: none; }
.action-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.15); display: flex; align-items: center; justify-content: center; font-size: 40rpx; margin-right: 24rpx; flex-shrink: 0; }
.action-icon.wechat { background: rgba(7,193,96,0.15); }
.action-text { flex: 1; min-width: 0; }
.action-title { font-size: 30rpx; color: #fff; font-weight: 500; display: block; }
.action-desc { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-top: 8rpx; }
.action-arrow { font-size: 32rpx; color: rgba(255,255,255,0.3); }

View File

@@ -5,7 +5,7 @@
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev -p 3006",
"dev": "next dev -p 30006",
"lint": "eslint .",
"start": "node scripts/start-standalone.js"
},

View File

@@ -19,7 +19,7 @@ Soul 创业派对 - 一键部署脚本
BAOTA_PANEL_URL # 宝塔面板地址,默认 https://42.194.232.22:9988
BAOTA_API_KEY # 宝塔 API 密钥,默认 hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd
DEPLOY_PM2_APP # PM2 项目名称,默认 soul
DEPLOY_PORT # Next.js 监听端口,默认 3006与 package.json / ecosystem 一致)
DEPLOY_PORT # Next.js 监听端口,默认 30006与 package.json / ecosystem 一致)
DEPLOY_NODE_VERSION # Node 版本,默认 v22.14.0(用于显示)
DEPLOY_NODE_PATH # Node 可执行文件路径,默认 /www/server/nodejs/v22.14.0/bin
# 用于避免多 Node 环境冲突,确保使用指定的 Node 版本
@@ -71,7 +71,7 @@ def get_cfg():
"api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"),
"pm2_name": os.environ.get("DEPLOY_PM2_APP", "soul"),
"site_url": os.environ.get("DEPLOY_SITE_URL", "https://soul.quwanzhi.com"),
"port": int(os.environ.get("DEPLOY_PORT", "3006")), # Next.js 监听端口,与 package.json / ecosystem 一致
"port": int(os.environ.get("DEPLOY_PORT", "30006")), # Next.js 监听端口,与 package.json / ecosystem 一致
# Node 环境配置
"node_version": os.environ.get("DEPLOY_NODE_VERSION", "v22.14.0"), # 指定 Node 版本
"node_path": os.environ.get("DEPLOY_NODE_PATH", "/www/server/nodejs/v22.14.0/bin"), # Node 可执行文件路径
@@ -188,11 +188,11 @@ def restart_node_project(panel_url, api_key, pm2_name):
return False
def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=3006, node_path=None):
def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=30006, node_path=None):
"""通过宝塔 API 添加或更新 Node 项目配置
Next.js standalone 的 server.js 通过 process.env.PORT 读端口(默认 3000
这里在 run_cmd 中显式设置 PORT=port与项目 package.json / ecosystem 的 3006 一致。
这里在 run_cmd 中显式设置 PORT=port与项目 package.json / ecosystem 的 30006 一致。
"""
paths_to_try = [
"/project/nodejs/add_project",
@@ -724,7 +724,7 @@ def deploy_via_baota_api(cfg):
pm2_name = cfg["pm2_name"]
project_path = cfg["project_path"]
node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin")
port = cfg.get("port", 3006) # 与 package.json dev/start -p 3006、ecosystem PORT 一致
port = cfg.get("port", 30006) # 与 package.json dev/start -p 30006、ecosystem PORT 一致
# 1. 检查项目是否存在
print(" 检查项目状态...")
@@ -796,7 +796,7 @@ def main():
print(" 项目路径: %s" % cfg["project_path"])
print(" PM2 名称: %s" % cfg["pm2_name"])
print(" 站点地址: %s" % cfg["site_url"])
print(" 端口: %s" % cfg.get("port", 3006))
print(" 端口: %s" % cfg.get("port", 30006))
print(" Node 版本: %s" % cfg.get("node_version", "v22.14.0"))
print(" Node 路径: %s" % cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin"))
print("=" * 60)

View File

@@ -73,9 +73,9 @@ const port = process.env.PORT;
if (!port) {
console.error('❌ 错误:未设置 PORT 环境变量');
console.error(' 请设置端口后启动,例如:');
console.error(' PORT=3006 pnpm start');
console.error(' PORT=30006 pnpm start');
console.error(' 或:');
console.error(' export PORT=3006 && pnpm start');
console.error(' export PORT=30006 && pnpm start');
process.exit(1);
}

View File

@@ -8,10 +8,12 @@
-**Phase 1搭架子**已完成适配层、miniprogram.config.js、首页/目录/阅读占位、构建与合并脚本
-**Phase 2核心页**(已完成):阅读页接 `/api/book/chapter/[id]`、ChapterContent 组件、完整目录、上下篇切换
- **Phase 3我的与子页**待做):我的、推广、设置、购买记录、地址列表/编辑、Zustand + persist
- **Phase 4找伙伴与其余**待做match、关于、搜索、底部 tabBar、安全区
- **Phase 3我的与子页**已完成):我的、推广、设置、购买记录、关于、Zustand + persist
- **Phase 4找伙伴与其余**已完成match、搜索、底部 tabBar、安全区适配
-**Phase 5收尾**(待做):全量自检、样式对齐、踩坑修复、发布流程
**🎉 C 端页面已 100% 迁移完成2026-02-02**
---
## 一、策略选择

View File

@@ -0,0 +1,125 @@
# Phase 4 完成说明(找伙伴与其余)
## 完成时间
2026-02-02
## 已完成内容
### 1. 找伙伴页
**newpp/src/pages/MatchPage.jsx + src/match.jsx**
- 匹配类型选择:创业合伙、资源对接、导师顾问、团队招募
- 匹配次数管理每日免费1次 + 购买章节获得更多次数
- 匹配结果展示:匹配度、创业理念、共同兴趣、微信号
- 加入匹配池功能:提交手机号加入
- 次数统计:剩余次数、已用次数、解锁提示
### 2. 搜索页
**newpp/src/pages/SearchPage.jsx + src/search.jsx**
- 实时搜索章节标题
- 搜索结果展示:所属篇章、标题、免费标签
- 点击结果跳转到阅读页
- 空态与无结果提示
### 3. 底部 TabBar 组件
**newpp/src/components/BottomNav.jsx**
- 4 个 Tab首页、目录、找伙伴、我的
- 当前激活态标识
- 找伙伴 Tab 动态显示matchEnabled
- 安全区适配paddingBottom = safe-area-inset-bottom
- 跨端路由:小程序 wx.switchTabWeb window.location.href
### 4. 各页面集成 BottomNav
已在以下页面添加 `<BottomNav />` 组件:
- HomePage (current="/")
- ChaptersPage (current="/chapters")
- MatchPage (current="/match")
- MyPage (current="/my")
### 5. 构建配置更新
**webpack.mp.config.js**
- 新增入口match、search
**miniprogram.config.js**
- router.other 新增路由:/match、/search
---
## 已完成页面清单Phase 1-4
| Next 路由 | 小程序页面 | 入口文件 | 状态 |
|-----------|-----------|----------|------|
| app/page.tsx | pages/index/index | src/index.jsx | ✅ Phase 2 |
| app/chapters/page.tsx | pages/chapters/chapters | src/chapters.jsx | ✅ Phase 2 |
| app/read/[id]/page.tsx | pages/read/read | src/read.jsx | ✅ Phase 2 |
| app/my/page.tsx | pages/my/my | src/my.jsx | ✅ Phase 3 |
| app/my/referral/page.tsx | pages/referral/referral | src/referral.jsx | ✅ Phase 3 |
| app/my/settings/page.tsx | pages/settings/settings | src/settings.jsx | ✅ Phase 3 |
| app/my/purchases/page.tsx | pages/purchases/purchases | src/purchases.jsx | ✅ Phase 3 |
| app/about/page.tsx | pages/about/about | src/about.jsx | ✅ Phase 3 |
| app/match/page.tsx | pages/match/match | src/match.jsx | ✅ Phase 4 |
| app/search/page.tsx | pages/search/search | src/search.jsx | ✅ Phase 4 |
---
## 安全区适配说明
### 底部安全区
在 BottomNav 中使用 `paddingBottom: 'env(safe-area-inset-bottom)'`,确保在有底部刘海的设备上正常显示。
### 顶部安全区
小程序自动处理,无需额外适配。如需自定义导航栏,可在 app.js globalData 中读取 `navBarHeight``statusBarHeight`
### 右侧安全区(胶囊按钮)
若使用自定义导航栏,需在右侧预留 `capsulePaddingRight` 空间,避免遮挡胶囊按钮。
---
## 本地测试步骤
1. **构建**
```bash
cd newpp
npm run build:mp
```
确认生成 `newpp/dist/mp/`,包含 10 个页面index、chapters、read、my、referral、settings、purchases、about、match、search
2. **合并到 miniprogram**
```bash
cd ..
node scripts/merge-kbone-to-miniprogram.js
```
3. **手动合并 app.js**
确保 `miniprogram/app.js` 包含 globalDatabaseUrl、matchEnabled、navBarHeight 等、request、loadFeatureConfig 等逻辑。
4. **微信开发者工具测试**
- 底部 TabBar首页 ↔ 目录 ↔ 找伙伴 ↔ 我的
- 找伙伴:选择类型 → 开始匹配 → 查看结果 → 复制微信
- 搜索:首页 → 搜索 icon → 输入关键词 → 查看结果 → 点击跳转阅读
- 我的:查看统计 → 推广中心 → 设置 → 关于
---
## 待完成Phase 5
- **全量自检**对照「Web转小程序并上传-提示词」逐项检查
- **样式对齐**:颜色、间距、圆角、阴影与 Web 保持一致
- **踩坑修复**WXML 禁忌、启动不阻塞、safe-area 边界
- **发布流程**:预览码 → 体验版 → 提审 → 发布
---
## 下一步
Phase 5收尾全量自检、样式对齐、踩坑修复、发布流程

View File

@@ -69,7 +69,7 @@ npm install --production
# 使用 next 命令启动
npm start
# 或
next start -p 3006
next start -p 30006
```
### Standalone 模式
@@ -79,7 +79,7 @@ next start -p 3006
node server.js
# 或指定端口
PORT=3006 node server.js
PORT=30006 node server.js
# 使用 PM2
pm2 start server.js --name soul

View File

@@ -9,9 +9,9 @@
### 1. 应用端口与 Nginx 不一致(已修复)
- **现象**:部署脚本用 `pm2 start server.js --name soul` 启动未指定端口。Next.js standalone 默认监听 **3000**
- **宝塔约定**:根据 `开发文档/服务器管理/references/端口配置表.md`soul 使用端口 **3006**Nginx 反代到 `127.0.0.1:3006`
- **结果**:应用实际在 3000 监听Nginx 请求 3006 → 无进程 → **502 Bad Gateway**
- **修复**:部署脚本 `scripts/devlop.py` 通过宝塔 API 重启 Node 项目,服务器上 PM2 启动时需设置 `PORT=3006`(可与 `ecosystem.config.cjs` 或环境变量 `DEPLOY_APP_PORT` 一致),保证与 Nginx 一致。
- **宝塔约定**:根据 `开发文档/服务器管理/references/端口配置表.md`soul 使用端口 **30006**Nginx 反代到 `127.0.0.1:30006`
- **结果**:应用实际在 3000 监听Nginx 请求 30006 → 无进程 → **502 Bad Gateway**
- **修复**:部署脚本 `scripts/devlop.py` 通过宝塔 API 重启 Node 项目,服务器上 PM2 启动时需设置 `PORT=30006`(可与 `ecosystem.config.cjs` 或环境变量 `DEPLOY_APP_PORT` 一致),保证与 Nginx 一致。
---
@@ -20,36 +20,36 @@
### 1. Nginx 反向代理
- **域名**soul.quwanzhi.com
- **要求**`proxy_pass http://127.0.0.1:3006;`(与端口配置表一致)
- **检查**:宝塔 → 网站 → soul.quwanzhi.com → 设置 → 反向代理 / 配置文件,确认 `proxy_pass` 指向 `127.0.0.1:3006`
- **要求**`proxy_pass http://127.0.0.1:30006;`(与端口配置表一致)
- **检查**:宝塔 → 网站 → soul.quwanzhi.com → 设置 → 反向代理 / 配置文件,确认 `proxy_pass` 指向 `127.0.0.1:30006`
- **SSL**:若走 HTTPS确认已配置 443 与证书(端口配置表注明使用通配符证书)。
### 2. PM2 与部署脚本一致
- **项目名**soul`scripts/devlop.py``DEPLOY_PM2_APP` 一致)
- **启动方式****必须用 `node server.js`**,工作目录 `/www/wwwroot/soul`,环境变量 `PORT=3006`
- **启动方式****必须用 `node server.js`**,工作目录 `/www/wwwroot/soul`,环境变量 `PORT=30006`
- **不要用**`npm start` / `next start`。standalone 部署后没有完整 node_modules也没有 `next` 命令,会报 `next: command not found`
- **宝塔 PM2 管理器**:启动文件填 `server.js`,启动命令填 `node server.js`或选「Node 项目」后只填 `server.js`),环境变量添加 `PORT=3006`。也可用 `pm2 start ecosystem.config.cjs`(项目根目录已提供该文件)。
- **宝塔 PM2 管理器**:启动文件填 `server.js`,启动命令填 `node server.js`或选「Node 项目」后只填 `server.js`),环境变量添加 `PORT=30006`。也可用 `pm2 start ecosystem.config.cjs`(项目根目录已提供该文件)。
- **注意**若同时在宝塔「PM2 管理器」里添加了同名项目,可能产生 root 与 www 用户冲突,建议只保留一种方式(要么只用脚本部署 + 命令行 PM2要么只用宝塔 PM2 界面)。
### 3. 项目目录与端口
- **项目路径**`/www/wwwroot/soul`(与 `DEPLOY_PROJECT_PATH` 一致)
- **应用端口**3006与端口配置表、Nginx、部署脚本中的 `PORT` 一致)
- **应用端口**30006与端口配置表、Nginx、部署脚本中的 `PORT` 一致)
---
## 三、快速检查命令SSH 到服务器后执行)
```bash
# 1. 应用是否在 3006 监听
ss -tlnp | grep 3006
# 1. 应用是否在 30006 监听
ss -tlnp | grep 30006
# 2. PM2 列表(是否有 soul状态 online
pm2 list
# 3. Nginx 配置是否包含 soul 且 proxy_pass 为 3006
grep -r "soul\|3006" /www/server/panel/vhost/nginx/
# 3. Nginx 配置是否包含 soul 且 proxy_pass 为 30006
grep -r "soul\|30006" /www/server/panel/vhost/nginx/
# 4. Nginx 语法
nginx -t
@@ -62,11 +62,11 @@ nginx -t
部署时若需改端口,可在本机执行脚本前设置:
```bash
set DEPLOY_APP_PORT=3006
set DEPLOY_APP_PORT=30006
python scripts/devlop.py
```
或修改 `scripts/devlop.py``get_cfg()``app_port` 默认值(当前为 3006
或修改 `scripts/devlop.py``get_cfg()``app_port` 默认值(当前为 30006
---

View File

@@ -9,7 +9,7 @@
**服务器**:与 开发文档/服务器管理 一致
- 小型宝塔:`42.194.232.22`
- 项目路径:`/www/wwwroot/soul`
- 端口3006域名https://soul.quwanzhi.com
- 端口30006域名https://soul.quwanzhi.com
**凭证**:与 服务器管理/SKILL.md 一致root / Zhiqun1984已写在项目部署脚本里。

View File

@@ -9,9 +9,9 @@
### 1. 移除硬编码端口
-**Dockerfile**: 移除 `ENV PORT 3000`,改为从环境变量读取
-**docker-compose.yml**: 使用 `APP_PORT` 环境变量,默认 3006
-**docker-compose.yml**: 使用 `APP_PORT` 环境变量,默认 30006
-**start-standalone.js**: 强制要求设置 `PORT` 环境变量
-**deploy_soul.py**: 已使用 3006 端口(与项目一致)
-**deploy_soul.py**: 已使用 30006 端口(与项目一致)
### 2. 端口配置方式
@@ -19,17 +19,17 @@
```bash
# Windows PowerShell
$env:PORT=3006
$env:PORT=30006
pnpm start
# 或一行命令
$env:PORT=3006; pnpm start
$env:PORT=30006; pnpm start
# Windows CMD
set PORT=3006 && pnpm start
set PORT=30006 && pnpm start
# Linux/Mac
PORT=3006 pnpm start
PORT=30006 pnpm start
```
#### 方式二Docker Compose 部署
@@ -37,7 +37,7 @@ PORT=3006 pnpm start
1. 创建 `.env` 文件(项目根目录):
```env
APP_PORT=3006
APP_PORT=30006
```
2. 启动容器:
@@ -46,7 +46,7 @@ APP_PORT=3006
docker-compose up -d
```
容器内外端口都会使用 3006。
容器内外端口都会使用 30006。
#### 方式三Docker 直接运行
@@ -61,7 +61,7 @@ docker run -e PORT=3007 -p 3007:3007 soul-book
```python
"config": {
"port": int(os.environ.get("DEPLOY_PORT", "3006")), # 修改此处
"port": int(os.environ.get("DEPLOY_PORT", "30006")), # 修改此处
# ...
}
```
@@ -77,7 +77,7 @@ python scripts/deploy_soul.py --action update
| 项目名称 | 端口 | 说明 |
|---------|------|------|
| soul-book | 3006 | 本项目默认端口 |
| soul-book | 30006 | 本项目默认端口 |
| other-project-1 | 3007 | 其他项目 |
| api-service | 3008 | API 服务 |
| admin-panel | 3009 | 管理后台 |
@@ -101,7 +101,7 @@ python scripts/deploy_soul.py --action update
```nginx
location / {
proxy_pass http://localhost:3006; # 修改为实际端口
proxy_pass http://localhost:30006; # 修改为实际端口
# ...
}
```
@@ -112,11 +112,11 @@ location / {
```bash
# CentOS/RHEL
firewall-cmd --zone=public --add-port=3006/tcp --permanent
firewall-cmd --zone=public --add-port=30006/tcp --permanent
firewall-cmd --reload
# Ubuntu/Debian
ufw allow 3006/tcp
ufw allow 30006/tcp
```
### 4. PM2 配置(如使用)
@@ -130,7 +130,7 @@ module.exports = {
script: 'node_modules/next/dist/bin/next',
args: 'start',
env: {
PORT: 3006
PORT: 30006
}
}]
}
@@ -142,10 +142,10 @@ module.exports = {
```bash
# 检查端口监听
netstat -tlnp | grep 3006
netstat -tlnp | grep 30006
# 测试访问
curl http://localhost:3006
curl http://localhost:30006
# 查看容器端口映射
docker ps | grep soul

View File

@@ -49,7 +49,7 @@ def get_cfg():
"password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"),
"ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""),
"project_path": os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul"),
"app_port": os.environ.get("DEPLOY_APP_PORT", "3006"),
"app_port": os.environ.get("DEPLOY_APP_PORT", "30006"),
"pm2_name": os.environ.get("DEPLOY_PM2_APP", BAOTA_CFG["pm2_name"]),
}