This commit is contained in:
Alex-larget
2026-03-24 15:44:08 +08:00
parent 346e8ab057
commit 28ad08da84
62 changed files with 814 additions and 840 deletions

View File

@@ -0,0 +1,11 @@
# 产品经理 经验记录 - 2026-03-24
## 开发进度同步会议
### 文档同步原则
- 实现变更后需同步更新:《需求汇总》《运营与变更》及对应角色项目索引。
- 项目索引「最后更新」应与实际变更日期一致,避免滞后。
### 当前状态
- 2026-03-20 需求(提现、我的收益、推广设置等)已与实现对齐。
- 主需求、落地推进表已基本同步;项目索引已补齐至 2026-03-24。

View File

@@ -5,3 +5,4 @@
| 2026-03-05 | 分支冲突后需求文档与实现一致性核对 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-05 | 文章详情@某人高亮与一键加好友验收标准与待确认 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-10 | 管理端迁移 Mycontent-temp主导航收敛与隐藏页面入口承载策略 | [2026-03-10.md](./2026-03-10.md) |
| 2026-03-24 | 开发进度同步会议:文档同步原则、项目索引补齐 | [2026-03-24.md](./2026-03-24.md) |

View File

@@ -0,0 +1,17 @@
# 后端工程师 经验记录 - 2026-03-24
## 开发进度同步会议
### 提现相关2026-03-20 已落地,与文档一致)
- 审批逻辑doApproveWithdrawal 校验「累计-已提现>=待审核」,-0.01 浮点容差。
- referral_configwithdrawFee、enableAutoWithdraw、minWithdrawAmount 使用正确。
- admin_withdrawalsfail_reason、error_message 落库。
### router 缺失 handler 补齐(编译通过)
- `BookRanking``book.go`,复用 `computeArticleRankingSections``?limit=` 默认 50、最大 200字段与 `sectionListItem` 对齐(无 `titles` 字段)。
- `DBPersonPinnedToken` / `CKBPinnedPerson``db_person.go`,置顶人物 `Order("updated_at DESC").First`,与 `DBPersonPinnedList` 首条一致;小程序无置顶返回 `data: null`
- `AdminDashboardLeads``admin_dashboard.go``ckb_lead_records` / `ckb_submit_records` 总量、今日量、留资去重用户数。
### 待办
- router 补齐users/rfm、users/journey-stats、shensheshou 共 5 个。
- 确认 /api/admin/settings 是否支持 ossConfig。

View File

@@ -9,3 +9,4 @@
| 2026-03-14 | 内容排名算法修正:排名分公式(阅读/新度/付款前 N 名),支持 hot_score 手动覆盖 | [2026-03-14.md](./2026-03-14.md) |
| 2026-03-16 | ParseAutoLinkContent data-label存客宝 create planType/sceneId/status | [2026-03-16.md](./2026-03-16.md) |
| 2026-03-17 | 代付 PayNotify beneficiaryUserID 权益归发起人gift-pay detail 返回 initiatorUserId | [2026-03-17.md](./2026-03-17.md) |
| 2026-03-24 | router 缺失四 handlerBookRanking、DBPersonPinnedToken、CKBPinnedPerson、AdminDashboardLeads | [2026-03-24.md](./2026-03-24.md) |

View File

@@ -0,0 +1,12 @@
# 团队 经验记录 - 2026-03-24
## 开发进度同步会议 - 文档同步原则(跨角色共识)
### 团队级决议
- **实现变更后**需同步更新:`开发文档/1、需求/需求汇总.md``开发文档/10、项目管理/运营与变更.md` 及对应角色 `agent/开发助理/项目索引/{角色}.md`
- **项目索引**:每次开发完成或会议收尾后,在开发进度表追加一行(含日期),并将「最后更新」改为当前日期。
- 避免文档与实现脱节:索引滞后会导致下次同步会议重复盘点。
### 落地建议
- 各角色在完成功能开发或吸收经验时,主动更新项目索引。
- 橙子收尾时统一检查并补齐索引。

View File

@@ -10,3 +10,4 @@
| 2026-03-14 | 内容排名算法跨端复用:管理端内容排行与小程序精选推荐共用 computeArticleRankingSections | [2026-03-14.md](./2026-03-14.md) |
| 2026-03-16 | TipTap Mention 需 data-label否则显示 token | [2026-03-16.md](./2026-03-16.md) |
| 2026-03-17 | 代付美团式流程与权益归属约定:读页→代付页→分享;权益/分佣归发起人 | [2026-03-17.md](./2026-03-17.md) |
| 2026-03-24 | 文档同步原则:实现变更后同步需求/运营与变更/项目索引 | [2026-03-24.md](./2026-03-24.md) |

View File

@@ -0,0 +1,10 @@
# 小程序开发工程师 经验记录 - 2026-03-24
## 开发进度同步会议
### 近期已落地(与文档一致)
- 2026-03-19原生按钮覆盖定位cover-view经验。
- 2026-03-20手机号一键登录、login-modal 公用组件;我的收益取 availableEarnings。
### 技术债
- 富文本渲染rich-text待实施需确认 DB 格式后再推进。

View File

@@ -26,9 +26,10 @@ Soul 创业派对产品定位:面向创业者的社区/工具型小程序。
| 2026-03-17 | 会议:稳定版源码质量优化;验收标准功能不变、三端联调通过 | 待续 |
| 2026-03-18 | 文档归档整理:以《以界面定需求》为基准,重整需求口径/验收点/分享 singlePage 约束,写入产品经验库 | 已完成 |
| 2026-03-18 | 会议:超级个体开通后自动创建@人与支付前资料引导(头像+昵称) | 已完成 |
| 2026-03-24 | 会议:开发进度同步;项目索引补齐 2026-03-19/20 及之后记录;文档同步原则确认 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-18
**最后更新**2026-03-24

View File

@@ -38,9 +38,11 @@ soul-apiGo + Gin + GORM + MySQL提供三组路由`/api/miniprogram/*`
| 2026-03-17 | 性能优化会议Redis 缓存接入parts/hot/recommended/stats/config/章节 content、容灾回退 DBOSS 上传接入;/health 返回 database/redis 状态 | 已完成 |
| 2026-03-18 | 文档归档整理:按界面→接口→规则口径重整后端功能需求与风险点,写入角色经验库 | 已完成 |
| 2026-03-18 | 会议:超级个体开通后自动创建@人Person 绑定 userId 幂等)与资料完善 flags 方案 | 已完成 |
| 2026-03-20 | 提现审批逻辑修复、fail_reason/error_message 落库、referral_config withdrawFee/enableAutoWithdraw | 已完成 |
| 2026-03-24 | 会议开发进度同步项目索引补齐router/ossConfig 待确认 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-18
**最后更新**2026-03-24

View File

@@ -41,9 +41,12 @@
| 2026-03-17 | 会议收尾:源码优化 5 项全部完成;开发环境测试通过 | 已完成 |
| 2026-03-18 | 吸收经验:分享链路需兼容好友/朋友圈 singlePage单页模式能力降级并引导“前往小程序”进入完整版 | 已完成 |
| 2026-03-18 | 会议:支付超级个体前/开通后资料默认校验,跳转 avatar-nickname 引导页(仅头像+昵称) | 已完成 |
| 2026-03-19 | 原生按钮覆盖定位cover-view经验入库 | 已完成 |
| 2026-03-20 | 手机号一键登录、login-modal 公用组件read/my/gift-pay 引入);我的收益取 availableEarnings | 已完成 |
| 2026-03-24 | 会议:开发进度同步;项目索引补齐;富文本 rich-text 技术债待续 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-18
**最后更新**2026-03-24

View File

@@ -32,7 +32,8 @@
| 2026-03-17 | 性能优化会议test_upload.py 6 用例;/health 可验证 database/redis部署后回归缓存接口 | 已完成 |
| 2026-03-18 | 文档归档整理:按界面驱动口径统一验收;补充分享 singlePage 降级与引导为必测项 | 已完成 |
| 2026-03-18 | 会议新增用例资料默认阻断支付、Person 自动创建幂等、昵称变更同步回归) | 已完成 |
| 2026-03-24 | 会议:开发进度同步;项目索引补齐;提现/登录/收益/推广设置用例补充singlePage、getPhoneNumber 边界 | 已完成 |
---
**最后更新**2026-03-18
**最后更新**2026-03-24

View File

@@ -43,11 +43,13 @@
| 2026-03-17 | 性能优化会议OSS 配置后上传自动优先 OSS失败回退本地无需前端改动 | 已完成 |
| 2026-03-18 | 文档归档整理:按《以界面定需求》重整管理端功能需求与验收口径,写入角色经验库 | 已完成 |
| 2026-03-18 | 会议:超级个体开通后自动创建@人;管理端可选展示 userId/来源以便排查重名 | 已完成 |
| 2026-03-20 | 提现审核备注列fail_reason/error_message、自动审批开关推广设置提现手续费 | 已完成 |
| 2026-03-24 | 会议开发进度同步项目索引补齐DistributionPage Order.description 待修 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-18
**最后更新**2026-03-24
> soul-admin 构建仍有 DistributionPage Order.description 类型错误(与本次迁移无关),待修复。

View File

@@ -0,0 +1,10 @@
# 管理端开发工程师 经验记录 - 2026-03-24
## 开发进度同步会议
### 提现相关2026-03-20 已落地,与文档一致)
- 提现审核备注列fail_reason/error_message、自动审批开关。
- 推广设置:提现手续费、自动提现开关。
### 待办
- DistributionPage Order.description 类型错误待修(与本次迁移无关,有空即修)。

View File

@@ -0,0 +1,8 @@
# 软件测试 经验记录 - 2026-03-24
## 开发进度同步会议
### 用例补充待办
- 提现审批、手机号登录、我的收益、推广设置、提现审核等新增功能需补充/更新用例。
- 边界必测singlePage 分享进入、getPhoneNumber 隐私协议同意流程。
- 下次回归前完成用例更新。

View File

@@ -0,0 +1,114 @@
# 会议纪要 - 2026-03-24 | 开发进度同步会议
> 本文件由**助理橙子**在会议结束后自动生成。
---
## 基本信息
- **时间**2026-03-24
- **议题**:全员查看自己的代码与开发文档,同步开发进度
- **触发方式**:开个会议,所有的人查看自己的代码和@开发文档同步开发进度
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员
---
## 各角色发言
### 【产品经理】
- 2026-03-20 需求(提现审批、我的收益、推广设置等)已与实现对齐;小程序手机号登录变更已写入运营与变更。
- 项目索引、主需求、落地推进表「最后更新」停在 2026-03-18缺 2026-03-19、2026-03-20 及之后的记录。
- 待办:补充项目索引近期决议;链接人与事置顶按《链接人与事-置顶与超级个体对应设计》为准。
### 【后端开发】
- 提现审批逻辑、referral_configwithdrawFee、enableAutoWithdraw、admin_withdrawals 落库与文档一致。
- 后端项目索引最后更新 2026-03-18未记录 2026-03-20 提现相关变更。
- 待办:项目索引补充 2026-03-20router 补齐users/rfm、journey-stats、shensheshou、ossConfig 确认。
### 【管理端开发工程师】
- 提现审核备注列、自动审批开关、推广设置提现手续费与文档一致。
- 项目索引停在 2026-03-18未记录 2026-03-20 变更。
- 待办项目索引补充DistributionPage Order.description 类型错误待修。
### 【小程序开发工程师】
- 手机号一键登录、login-modal 公用组件、我的收益 availableEarnings 与文档一致;经验已入库 2026-03-19、2026-03-20。
- 项目索引停在 2026-03-18未体现 2026-03-19、2026-03-20 改动。
- 待办项目索引补充富文本渲染rich-text为技术债。
### 【测试人员】
- 功能测试流程与报告模板已定稿。
- 提现、登录、收益、推广设置等需补充/更新用例singlePage、getPhoneNumber 隐私边界用例待补。
- 项目索引停在 2026-03-18。
---
## 讨论过程
- 乘风汇总:各端项目索引普遍停在 2026-03-18实现已到 2026-03-20需补齐。
- 产品确认:需求清单、运营与变更已基本同步,主要是项目索引滞后。
- 橙子建议按角色补充索引后统一标注「最后更新2026-03-24」。
---
## 会议决议
1. **项目索引补齐**:产品、后端、管理端、小程序、测试五份索引补充 2026-03-19、2026-03-20 及之后变更,最后更新统一为 2026-03-24。
2. **文档同步原则**:实现变更后同步更新《需求汇总》《运营与变更》及对应角色项目索引。
3. **原待办保留**router/ossConfig 迁移、DistributionPage 类型错误、富文本渲染、测试用例补充按原计划推进。
---
## 待办事项
| 责任角色 | 任务 | 优先级 | 截止建议 |
|---------|------|--------|---------|
| 后端开发 | router 补齐 users/rfm、journey-stats、shensheshou确认 ossConfig | 中 | 按迁移清单 |
| 管理端开发工程师 | DistributionPage Order.description 类型错误修复 | 低 | 有空即修 |
| 小程序开发工程师 | 富文本渲染rich-text技术债 | 低 | 待定 |
| 测试人员 | 提现/登录/收益/推广设置用例补充singlePage、getPhoneNumber 隐私边界 | 中 | 下次回归前 |
---
## 问题与作答区
| # | 问题 | 责任角色 | 作答 |
|---|------|---------|------|
| 1 | `/api/admin/settings` 是否支持 ossConfig | 后端开发 | (待补充) |
| 2 | 富文本 content 数据库格式确认后rich-text 实施方案? | 产品/小程序 | (待补充) |
---
## 各角色经验与业务理解更新
### 产品经理
- 项目索引与开发文档需定期同步,实现变更后应同步更新索引日期。
### 后端开发
- 2026-03-20 提现相关变更审批逻辑、fail_reason/error_message、referral_config已与文档一致项目索引需及时跟进。
### 管理端开发工程师
- 提现审核备注列、自动审批开关与推广设置已与文档一致;项目索引需及时跟进。
### 小程序开发工程师
- 手机号登录、login-modal、我的收益 availableEarnings 已与文档一致;项目索引需及时跟进。
### 测试人员
- 每次功能变更提现、登录、推广设置等需同步更新回归用例singlePage、getPhoneNumber 为必测边界。
### 团队共享
- 文档同步原则:实现变更后同步更新需求汇总、运营与变更及对应角色项目索引,最后更新日期统一标注。
---
*会议纪要由助理橙子生成 | 各角色经验已同步至 `agent/{角色}/evolution/2026-03-24.md`*

View File

@@ -80,3 +80,4 @@ YYYY-MM-DD_会议主题.md
| 2026-03-17 | 会议收尾:源码优化完成与测试流程定稿 | 产品、后端、管理端、小程序、测试、助理橙子 | [2026-03-17_会议收尾-源码优化完成与测试流程定稿.md](2026-03-17_会议收尾-源码优化完成与测试流程定稿.md) |
| 2026-03-17 | 性能优化与 Redis 缓存方案落地 | 后端、管理端、小程序、测试、助理橙子 | [2026-03-17_性能优化与Redis缓存方案落地.md](2026-03-17_性能优化与Redis缓存方案落地.md) |
| 2026-03-18 | 超级个体开通后自动创建@人与资料引导 | 产品、后端、管理端、小程序、测试 | [2026-03-18_超级个体开通后自动创建@人与资料引导.md](2026-03-18_超级个体开通后自动创建@人与资料引导.md) |
| 2026-03-24 | 开发进度同步会议(查看代码与开发文档对齐) | 产品、后端、管理端、小程序、测试 | [2026-03-24_开发进度同步会议.md](2026-03-24_开发进度同步会议.md) |

View File

@@ -9,3 +9,4 @@
## 2026-03-24
- Skill 优化assistant-doc-sync 新增 sync-log 记忆机制、assets 模板、Skill 撰写原则
- 会议收尾:开发进度同步会议;项目索引五角色补齐;纪要 `.cursor/meeting/2026-03-24_开发进度同步会议.md`

View File

@@ -11,6 +11,8 @@ const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version正式版否则用本字段
const APP_DISPLAY_VERSION = '1.7.1'
// 章节总数API 获取失败时的统一兜底,避免 90/62 混用
const FALLBACK_TOTAL_SECTIONS = 62
App({
globalData: {
@@ -40,7 +42,7 @@ App({
// 书籍数据bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters
bookData: null,
totalSections: 90,
totalSections: FALLBACK_TOTAL_SECTIONS, // 来自 book/parts 或 book/stats失败时用常量
// 购买记录
purchasedSections: [],
@@ -910,8 +912,12 @@ App({
if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
this.logout()
}
showError(msg)
reject(new Error(msg))
const err = new Error(msg)
err.response = data
const skipToast = data.needBindWechat === true || data.needBind === true ||
(data.errorCode && String(data.errorCode).indexOf('ERR_') === 0)
if (!silent && !skipToast) showError(msg)
reject(err)
return
}
resolve(data)
@@ -947,8 +953,12 @@ App({
}
// 4xx/5xx优先用返回体的 message/error
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
showError(msg)
reject(new Error(msg))
const err = new Error(msg)
if (data && typeof data === 'object') err.response = data
const skipToast = data && (data.needBindWechat === true || data.needBind === true ||
(data.errorCode && String(data.errorCode).indexOf('ERR_') === 0))
if (!silent && !skipToast) showError(msg)
reject(err)
},
fail: (err) => {
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
@@ -1124,6 +1134,11 @@ App({
if (res.success && res.data) {
const user = res.data.user
const oid = res.data.openId || user.openId
if (oid) {
this.globalData.openId = oid
wx.setStorageSync('openId', oid)
}
this.globalData.userInfo = user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = user.purchasedSections || []
@@ -1154,6 +1169,7 @@ App({
} else {
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
setTimeout(() => this.connectWsHeartbeat(), 2000)
}
return res.data
@@ -1198,9 +1214,10 @@ App({
return (this.globalData.readSectionIds || []).length
},
// 获取章节总数
// 获取章节总数(优先 API 已加载值,失败时返回统一兜底常量)
getTotalSections() {
return this.globalData.totalSections
const v = this.globalData.totalSections
return (v != null && v > 0) ? v : FALLBACK_TOTAL_SECTIONS
},
// 切换TabBar

View File

@@ -19,7 +19,7 @@ Page({
},
bookInfo: {
title: '一场Soul的创业实验',
totalChapters: 62,
totalChapters: 0, // 来自 book/stats 或 app.getTotalSections()
parts: [
{ name: '真实的人', chapters: 10 },
{ name: '真实的行业', chapters: 15 },
@@ -34,7 +34,8 @@ Page({
onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight
statusBarHeight: app.globalData.statusBarHeight,
'bookInfo.totalChapters': app.getTotalSections()
})
this.loadAuthor()
this.loadBookStats()
@@ -59,7 +60,7 @@ Page({
title: d.title || '',
bio: d.bio || '',
stats: Array.isArray(d.stats) ? d.stats : [
{ label: '商业案例', value: '62' },
{ label: '商业案例', value: String(app.getTotalSections()) },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
],
@@ -81,7 +82,7 @@ Page({
try {
const res = await app.request({ url: '/api/miniprogram/book/stats', silent: true })
if (res?.success && res.data) {
const total = res.data?.totalChapters || 62
const total = res.data?.totalChapters ?? app.getTotalSections()
this.setData({ 'bookInfo.totalChapters': total })
const stats = this.data.author?.stats || []
const idx = stats.findIndex((s) => s && (s.label === '商业案例' || s.label === '章节'))

View File

@@ -3,6 +3,7 @@
* 改造后:发起人支付,好友领取。支持单页模式引导、登录检测。
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
Page({
data: {
@@ -180,28 +181,10 @@ Page({
}
const payParams = res.data.payParams
const orderSn = res.data.orderSn
// 与正常章节支付一致:只传 5 个必需参数,不传 appId 等多余字段
await new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: resolve,
fail: reject
})
})
await soulBridge.requestWxJsapiPayment(payParams)
wx.showToast({ title: '支付成功', icon: 'success' })
this.setData({ paying: false })
// 主动同步订单状态(与 read 页一致)
if (orderSn) {
try {
await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true })
} catch (e) {
console.warn('[GiftPay] 主动同步订单失败:', e)
}
}
await soulBridge.syncOrderStatusQuery(app, orderSn)
this.loadDetail()
} catch (e) {
this.setData({ paying: false })

View File

@@ -33,16 +33,12 @@ Page({
hasFullBook: false,
readCount: 0,
// 书籍数据
totalSections: 62,
// 书籍数据totalSections 来自 book/parts初始用 app.getTotalSections() 兜底)
totalSections: 0, // onLoad 后由 loadBookData 更新
bookData: [],
// 推荐章节
featuredSections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
],
// 推荐章节(来自 recommended/hot API初始为空避免占位错误
featuredSections: [],
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters
bannerSection: null,
@@ -277,21 +273,21 @@ Page({
if (res?.success) {
const total = res.totalSections ?? 0
const parts = res.parts || []
app.globalData.totalSections = total || 62
app.globalData.totalSections = (total != null && total > 0) ? total : app.getTotalSections()
this.setData({
totalSections: app.globalData.totalSections,
partCount: parts.length || 5
})
}
} catch (e) {
this.setData({ totalSections: app.globalData.totalSections || 62, partCount: 5 })
this.setData({ totalSections: app.getTotalSections(), partCount: 5 })
}
},
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
const readCount = Math.min(app.getReadCount(), this.data.totalSections || app.getTotalSections())
this.setData({
isLoggedIn,
hasFullBook,

View File

@@ -5,6 +5,7 @@
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
const { checkAndExecute } = require('../../utils/ruleEngine.js')
const { trackClick } = require('../../utils/trackClick')
@@ -426,6 +427,7 @@ Page({
// 从数据库获取真实用户匹配
let matchedUser = null
let matchProfileError = ''
try {
const res = await app.request({ url: '/api/miniprogram/match/users', silent: true,
method: 'POST',
@@ -434,20 +436,38 @@ Page({
userId: app.globalData.userInfo?.id || ''
}
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
}
} catch (e) {
console.log('[Match] 数据库匹配失败:', e)
const r = e.response || {}
if (r.errorCode === 'ERR_PROFILE_INCOMPLETE') {
matchProfileError = r.message || '请先完善手机号或微信号后再发起匹配'
}
}
// 延迟显示结果(模拟匹配过程)
const delay = Math.random() * 2000 + 2000
setTimeout(() => {
clearInterval(timer)
if (matchProfileError) {
this.setData({ isMatching: false })
wx.showModal({
title: '完善资料',
content: matchProfileError,
confirmText: '去完善',
showCancel: false,
success: (mr) => {
if (mr.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
// 如果没有匹配到用户,提示用户
if (!matchedUser) {
this.setData({ isMatching: false })
@@ -632,7 +652,27 @@ Page({
wx.showToast({ title: res.error || '加入失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
const r = e.response || {}
if (r.errorCode === 'ERR_REQUIRE_PURCHASE') {
wx.showModal({
title: '需要先购买',
content: r.message || '请先购买章节或解锁全书后再使用资源对接',
confirmText: '去购买',
cancelText: '取消',
success: (mr) => { if (mr.confirm) this.goToChapters() }
})
} else if (r.errorCode === 'ERR_PROFILE_INCOMPLETE') {
wx.showModal({
title: '完善资料',
content: r.message || '请先完善资料',
confirmText: '去完善',
success: (mr) => {
if (mr.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
} else {
wx.showToast({ title: e.message || '网络异常,请重试', icon: 'none' })
}
} finally {
this.setData({ isJoining: false })
}
@@ -670,9 +710,7 @@ Page({
return
}
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 调用支付接口购买匹配次数
const referralCode = soulBridge.getReferralCodeForPay(app)
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -685,17 +723,11 @@ Page({
referralCode: referralCode || undefined
}
})
if (res.success && res.data?.payParams) {
// 调用微信支付
await new Promise((resolve, reject) => {
wx.requestPayment({
...res.data.payParams,
success: resolve,
fail: reject
})
})
await soulBridge.requestWxJsapiPayment(res.data.payParams)
await soulBridge.syncOrderStatusQuery(app, res.data.orderSn)
// 支付成功,增加匹配次数
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)

View File

@@ -8,6 +8,7 @@
* 点头像:若后台 persons.user_id 已绑定则带 ckbLeadToken走存客宝 CKBLead与阅读页 @ 一致)
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
Page({
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true },
@@ -188,66 +189,14 @@ Page({
wx.showToast({ title: '暂未公开联系方式', icon: 'none' })
},
/** 与 read 页 _doMentionAddFriend 一致targetUserId = Person.token */
/** 与阅读页 @mention 同链路soulBridge.submitCkbLead */
async _doCkbLeadSubmit(targetUserId, targetNickname) {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '完善资料',
content: '请先填写手机号(必填),以便对方通过获客计划联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source: 'member_detail_avatar'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetUserId,
targetNickname,
source: 'member_detail_avatar',
phoneModalContent: '请先填写手机号(必填),以便对方通过获客计划联系您'
})
},
_ensureUnlockedForLink(field) {

View File

@@ -9,6 +9,12 @@ const { formatStatNum } = require('../../utils/util.js')
const { trackClick } = require('../../utils/trackClick')
const { cleanSingleLineField } = require('../../utils/contentParser.js')
/** 与 referral 一致:提现需已绑定微信号(便于到账核对) */
function hasWechatIdBound() {
const ui = app.globalData.userInfo
return !!(ui && (ui.wechat || ui.wechatId || wx.getStorageSync('user_wechat')))
}
/** 是否视为「单章解锁」类订单(排除全书/VIP 等聚合商品名) */
function isSectionUnlockOrder(o) {
const name = String(o.product_name || o.title || '').trim()
@@ -35,7 +41,7 @@ Page({
userInfo: null,
// 统计数据
totalSections: 62,
totalSections: 0, // 来自 app.getTotalSections() 或 dashboard-stats
readCount: 0,
referralCount: 0,
earnings: '-',
@@ -961,7 +967,19 @@ Page({
wx.showToast({ title: '暂无可提现金额', icon: 'none' })
return
}
await this.ensureContactInfo(() => this.doWithdraw(amount))
if (!hasWechatIdBound()) {
wx.showModal({
title: '请先绑定微信号',
content: '提现需先绑定微信号,便于到账核对。请到「设置」中绑定后再提现。',
confirmText: '去绑定',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
return
}
this.doWithdraw(amount)
},
async doWithdraw(amount) {
@@ -980,6 +998,16 @@ Page({
this.loadWalletBalance()
} catch (e) {
wx.hideLoading()
const r = e.response || {}
if (r.needBind || r.needBindWechat) {
wx.showModal({
title: r.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: r.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (mr) => { if (mr.confirm) wx.navigateTo({ url: '/pages/settings/settings' }) }
})
return
}
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
}
}

View File

@@ -17,6 +17,7 @@ const accessManager = require('../../utils/chapterAccessManager')
const readingTracker = require('../../utils/readingTracker')
const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
const soulBridge = require('../../utils/soulBridge.js')
const { trackClick } = require('../../utils/trackClick')
const app = getApp()
@@ -92,7 +93,7 @@ Page({
// 价格
sectionPrice: 1,
fullBookPrice: 9.9,
totalSections: 62,
totalSections: 0, // 来自 app.getTotalSections() 或 book/parts
// 弹窗
showShareModal: false,
@@ -121,6 +122,9 @@ Page({
// 审核模式:隐藏购买按钮
auditMode: false,
// 分润比例(来自 config.shareRate用于分享提示文案
shareRate: 90,
// 好友从代付分享进入:待自动领取的 requestSn
pendingGiftRequestSn: '',
},
@@ -189,7 +193,8 @@ Page({
sectionMid: mid || null,
loading: true,
accessState: 'unknown',
pendingGiftRequestSn: giftRequestSn || ''
pendingGiftRequestSn: giftRequestSn || '',
totalSections: app.getTotalSections()
})
if (ref) {
@@ -200,9 +205,12 @@ Page({
try {
const config = await accessManager.fetchLatestConfig()
const shareRate = (config && config.shareRate != null) ? config.shareRate : 90
this.setData({
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
fullBookPrice: config.prices?.fullbook ?? 9.9,
shareRate,
totalSections: app.getTotalSections()
})
// 统一:先拉章节数据,用 isFree/price===0 判断免费
@@ -675,71 +683,13 @@ Page({
})
},
// 边界:未登录→去登录;无手机/微信号→去资料编辑;重复同一人→本地 key 去重
// 存客宝留资:统一 soulBridge.submitCkbLead与会员详情点头像同链路
async _doMentionAddFriend(targetUserId, targetNickname) {
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
}
})
return
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '完善资料',
content: '请先填写手机号(必填),以便对方联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source: 'article_mention'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetUserId,
targetNickname,
source: 'article_mention'
})
},
// 分享弹窗
@@ -848,24 +798,8 @@ Page({
}
const payParams = payRes.data.payParams
const orderSn = payRes.data.orderSn
await new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: resolve,
fail: reject
})
})
// 3) 主动同步(与其他支付流程一致)
if (orderSn) {
try {
await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true })
} catch (e) {}
}
await soulBridge.requestWxJsapiPayment(payParams)
await soulBridge.syncOrderStatusQuery(app, orderSn)
wx.showToast({ title: '支付成功', icon: 'success' })
this.setData({ giftPaid: true, giftRequestSn: requestSn, giftPaying: false })
@@ -883,8 +817,7 @@ Page({
// 复制链接
copyLink() {
const userInfo = app.globalData.userInfo
const referralCode = userInfo?.referralCode || ''
const referralCode = app.getMyReferralCode() || ''
const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
wx.setClipboardData({
@@ -900,9 +833,10 @@ Page({
copyShareText() {
const { section } = this.data
const total = app.getTotalSections()
const shareText = `🔥 刚看完这篇《${section?.title || '卡若创业派对'}》,太上头了!
62个真实商业案例每个都是从0到1的实战经验。私域运营、资源整合、商业变现干货满满。
${total}个真实商业案例每个都是从0到1的实战经验。私域运营、资源整合、商业变现干货满满。
推荐给正在创业或想创业的朋友,搜"卡若创业派对"小程序就能看!
@@ -1198,7 +1132,7 @@ Page({
try {
// 0. 尝试余额支付(若余额足够)
const userId = app.globalData.userInfo?.id
const referralCode = wx.getStorageSync('referral_code') || ''
const referralCode = soulBridge.getReferralCodeForPay(app)
if (userId) {
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
@@ -1262,14 +1196,9 @@ Page({
let paymentData = null
try {
// 获取章节完整名称用于支付描述
const sectionTitle = this.data.section?.title || sectionId
const description = type === 'fullbook'
? '《一场Soul的创业实验》全书'
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
// 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账
const referralCode = wx.getStorageSync('referral_code') || ''
const description = soulBridge.buildSectionPayDescription(type, sectionId, sectionTitle)
const referralCode = soulBridge.getReferralCodeForPay(app)
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -1321,18 +1250,11 @@ Page({
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
try {
await this.callWechatPay(paymentData)
// 4. 【关键】主动向微信查询订单状态并同步到本地(不依赖回调,解决订单一直 created 的问题)
await soulBridge.requestWxJsapiPayment(paymentData)
const orderSn = paymentData._orderSn || paymentData.orderSn
if (orderSn) {
try {
await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true })
console.log('[Pay] 已主动同步订单状态:', orderSn)
} catch (e) {
console.warn('[Pay] 主动同步订单失败,继续刷新购买状态:', e)
}
}
await soulBridge.syncOrderStatusQuery(app, orderSn)
if (orderSn) console.log('[Pay] 已主动同步订单状态:', orderSn)
// 5. 【标准流程】刷新权限并解锁内容
console.log('[Pay] 微信支付成功!')
@@ -1467,21 +1389,6 @@ Page({
}
},
// 调用微信支付
callWechatPay(paymentData) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType || 'MD5',
paySign: paymentData.paySign,
success: resolve,
fail: reject
})
})
},
// 跳转到上一篇
goToPrev() {
if (this.data.prevSection) {
@@ -1728,11 +1635,13 @@ Page({
try {
const config = await accessManager.fetchLatestConfig()
const shareRate = (config && config.shareRate != null) ? config.shareRate : 90
this.setData({
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
fullBookPrice: config.prices?.fullbook ?? 9.9,
shareRate
})
// 重新拉取章节,用 isFree/price 判断免费
const chapterRes = await app.request({
url: this._getChapterUrl({}),

View File

@@ -106,7 +106,7 @@
</view>
</view>
<view class="share-tip-inline" wx:if="{{!auditMode}}">
<text class="share-tip-text">分享后好友购买,你可获得 90% 收益</text>
<text class="share-tip-text">分享后好友购买,你可获得 {{shareRate || 90}}% 收益</text>
</view>
</view>

View File

@@ -63,7 +63,7 @@ Page({
posterReferralLink: '',
posterNickname: '',
posterNicknameInitial: '',
posterCaseCount: 62
posterCaseCount: 0 // 来自 app.getTotalSections()initData 后更新
},
onLoad() {
@@ -174,6 +174,7 @@ Page({
minWithdrawAmount: minWithdrawAmount,
bindingDays: realData?.bindingDays ?? 30,
userDiscount: realData?.userDiscount ?? 5,
posterCaseCount: app.getTotalSections(),
// 统计
referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
@@ -545,9 +546,10 @@ Page({
// 分享到朋友圈 - 随机文案
shareToMoments() {
const total = app.getTotalSections()
// 10条随机文案基于书的内容
const shareTexts = [
`🔥 在派对房里听到的真实故事比虚构的小说精彩100倍\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`🔥 在派对房里听到的真实故事比虚构的小说精彩100倍\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n${total}个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《卡若创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
@@ -654,37 +656,31 @@ Page({
method: 'POST',
data: { userId, amount }
})
wx.hideLoading()
if (res.success) {
wx.showModal({
title: '提现申请已提交 ✅',
content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
showCancel: false,
confirmText: '知道了'
})
// 刷新数据(此时待审核金额会增加,可提现金额会减少)
this.initData()
} else {
if (res.needBind || res.needBindWechat) {
wx.showModal({
title: res.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: res.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
} else {
wx.showToast({ title: res.message || res.error || '提现失败', icon: 'none', duration: 3000 })
}
}
wx.showModal({
title: '提现申请已提交 ✅',
content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
showCancel: false,
confirmText: '知道了'
})
this.initData()
} catch (e) {
wx.hideLoading()
console.error('[Referral] 提现失败:', e)
wx.showToast({ title: '提现失败,请重试', icon: 'none' })
const r = e.response || {}
if (r.needBind || r.needBindWechat) {
wx.showModal({
title: r.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: r.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
return
}
wx.showToast({ title: e.message || '提现失败,请重试', icon: 'none', duration: 3000 })
}
},
@@ -860,8 +856,9 @@ Page({
onShareTimeline() {
const ref = this.data.referralCode || app.getMyReferralCode()
console.log('[Referral] 分享到朋友圈,推荐码:', ref)
const total = app.getTotalSections()
return {
title: `卡若创业派对 - 62个真实商业案例`,
title: `卡若创业派对 - ${total}个真实商业案例`,
query: ref ? `ref=${ref}` : ''
// 不设置 imageUrl使用小程序默认截图
}

View File

@@ -1,4 +1,5 @@
const accessManager = require('../../utils/chapterAccessManager')
const soulBridge = require('../../utils/soulBridge.js')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
@@ -90,7 +91,7 @@ Page({
const amount = this.data.price
try {
// 0. 尝试余额支付(若余额足够)
const referralCode = wx.getStorageSync('referral_code') || ''
const referralCode = soulBridge.getReferralCodeForPay(app)
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
const balance = balanceRes?.data?.balance || 0
@@ -117,7 +118,7 @@ Page({
console.warn('[VIP] 余额支付失败,改用微信支付:', e)
}
// 1. 微信支付
// 1. 微信支付(带推荐码,与章节/匹配支付一致,便于分销归因)
const payRes = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -126,18 +127,21 @@ Page({
productType: 'vip',
productId: 'vip_annual',
amount,
description: '卡若创业派对VIP年度会员365天'
description: '卡若创业派对VIP年度会员365天',
referralCode: soulBridge.getReferralCodeForPay(app) || undefined
}
})
if (payRes?.success && payRes.data?.payParams) {
wx.requestPayment({
...payRes.data.payParams,
success: async () => {
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
await this._onVipPaymentSuccess()
},
fail: () => wx.showToast({ title: '支付取消', icon: 'none' })
})
try {
await soulBridge.requestWxJsapiPayment(payRes.data.payParams)
await soulBridge.syncOrderStatusQuery(app, payRes.data.orderSn)
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
await this._onVipPaymentSuccess()
} catch (e) {
const msg = (e && e.errMsg) ? String(e.errMsg) : ''
if (msg.indexOf('cancel') !== -1) wx.showToast({ title: '支付取消', icon: 'none' })
else wx.showToast({ title: '支付失败', icon: 'none' })
}
} else {
wx.showToast({ title: payRes?.error || '支付参数获取失败', icon: 'none' })
}

View File

@@ -1,4 +1,5 @@
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
const { trackClick } = require('../../utils/trackClick')
Page({
@@ -114,23 +115,22 @@ Page({
})
const params = (payRes && payRes.data && payRes.data.payParams) ? payRes.data.payParams : (payRes && payRes.payParams ? payRes.payParams : null)
if (params) {
wx.requestPayment({
...params,
success: async () => {
// Confirm the recharge
await app.request({
url: '/api/miniprogram/balance/recharge/confirm',
method: 'POST',
data: { orderSn: res.data.orderSn }
})
wx.showToast({ title: '充值成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
},
fail: () => {
wx.showToast({ title: '支付取消', icon: 'none' })
}
})
try {
await soulBridge.requestWxJsapiPayment(params)
await soulBridge.syncOrderStatusQuery(app, payRes.data && payRes.data.orderSn)
await app.request({
url: '/api/miniprogram/balance/recharge/confirm',
method: 'POST',
data: { orderSn: res.data.orderSn }
})
wx.showToast({ title: '充值成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
} catch (e) {
const msg = (e && e.errMsg) ? String(e.errMsg) : ''
if (msg.indexOf('cancel') !== -1) wx.showToast({ title: '支付取消', icon: 'none' })
else wx.showToast({ title: '支付失败', icon: 'none' })
}
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
}

View File

@@ -25,14 +25,16 @@ class ChapterAccessManager {
const res = await app.getConfig()
if (res && res.success && res.prices) {
return {
prices: res.prices || { section: 1, fullbook: 9.9 }
prices: res.prices || { section: 1, fullbook: 9.9 },
shareRate: res.shareRate != null ? res.shareRate : 90
}
}
} catch (e) {
console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
}
return {
prices: { section: 1, fullbook: 9.9 }
prices: { section: 1, fullbook: 9.9 },
shareRate: 90
}
}

View File

@@ -4,8 +4,8 @@
*
* segment 类型:
* { type: 'text', text }
* { type: 'mention', userId, nickname } — @某人,点击加好友
* { type: 'linkTag', label, url } — #链接标签,点击跳转
* { type: 'mention', userId, nickname } — @某人,点击加好友(提交存客宝见 utils/soulBridge.submitCkbLead
* { type: 'linkTag', label, url, ... } — #链接标签,点击跳转(阅读页 onLinkTagTap外链→link-preview、小程序→navigateToMiniProgram
* { type: 'image', src, alt } — 图片
*/

View File

@@ -0,0 +1,155 @@
/**
* 分销 / 微信支付 / 代付链路 / 存客宝留资 — 小程序侧统一桥接
* 阅读页 @mention、会员详情点头像、章节与代付支付等共用。
*/
/**
* 支付订单携带的推荐码:优先落地页写入的 storage否则当前用户自己的码便于自购归因一致
*/
function getReferralCodeForPay(app) {
try {
const s = wx.getStorageSync('referral_code')
if (s != null && String(s).trim() !== '') return String(s).trim()
} catch (e) {}
if (app && typeof app.getMyReferralCode === 'function') {
const c = app.getMyReferralCode()
if (c) return String(c).trim()
}
return ''
}
/** 章节 / 全书支付描述(与 read 页原逻辑一致) */
function buildSectionPayDescription(productType, sectionId, sectionTitle) {
if (productType === 'fullbook') return '《一场Soul的创业实验》全书'
if (productType === 'section') {
const t = sectionTitle || sectionId || ''
const short = t.length > 20 ? t.slice(0, 20) + '...' : t
return `章节${sectionId}-${short}`
}
return ''
}
/**
* 调起微信 JSAPI 支付(字段与 soul-api GetJSAPIPayParams 一致,勿 spread 全对象以免带入多余字段)
*/
function requestWxJsapiPayment(payParams) {
return new Promise((resolve, reject) => {
if (!payParams || payParams.timeStamp == null) {
reject(new Error('支付参数异常'))
return
}
wx.requestPayment({
timeStamp: String(payParams.timeStamp),
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: resolve,
fail: reject
})
})
}
/** 支付成功后主动查单,缓解回调延迟导致订单长期 created */
function syncOrderStatusQuery(app, orderSn) {
if (!app || !orderSn) return Promise.resolve()
return app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true }).catch(() => null)
}
/**
* 提交存客宝 lead与阅读页 @、会员详情点头像同接口)
* @param {object} app getApp()
* @param {{ targetUserId: string, targetNickname?: string, source: string, phoneModalContent?: string }} opts
* @returns {Promise<boolean>} 是否提交成功
*/
async function submitCkbLead(app, opts) {
const targetUserId = (opts && opts.targetUserId) || ''
const targetNickname = ((opts && opts.targetNickname) || 'TA').trim() || 'TA'
const source = (opts && opts.source) || 'article_mention'
const phoneModalContent = (opts && opts.phoneModalContent) || '请先填写手机号(必填),以便对方联系您'
if (!targetUserId) return false
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
return await new Promise((resolve) => {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
resolve(false)
}
})
})
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes && profileRes.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
return await new Promise((resolve) => {
wx.showModal({
title: '完善资料',
content: phoneModalContent,
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
resolve(false)
}
})
})
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source
}
})
wx.hideLoading()
if (res && res.success) {
try {
wx.setStorageSync('lead_last_submit_ts', Date.now())
} catch (e) {}
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
return true
}
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
return false
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
return false
}
}
module.exports = {
getReferralCodeForPay,
buildSectionPayDescription,
requestWxJsapiPayment,
syncOrderStatusQuery,
submitCkbLead
}

View File

@@ -392,6 +392,29 @@ func AdminSuperIndividualStats(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": out, "total": len(out)})
}
// AdminDashboardLeads GET /api/admin/dashboard/leads 管理端看板-存客宝线索/提交记录概览
func AdminDashboardLeads(c *gin.Context) {
db := database.DB()
var contactTotal, submitTotal, uniqueContactUsers int64
db.Model(&model.CkbLeadRecord{}).Count(&contactTotal)
db.Model(&model.CkbSubmitRecord{}).Count(&submitTotal)
db.Raw(`SELECT COUNT(DISTINCT user_id) FROM ckb_lead_records WHERE user_id IS NOT NULL AND user_id != ''`).Scan(&uniqueContactUsers)
var todayContact, todaySubmit int64
db.Raw(`SELECT COUNT(*) FROM ckb_lead_records WHERE DATE(created_at) = CURDATE()`).Scan(&todayContact)
db.Raw(`SELECT COUNT(*) FROM ckb_submit_records WHERE DATE(created_at) = CURDATE()`).Scan(&todaySubmit)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"contactLeadsTotal": contactTotal,
"submitRecordsTotal": submitTotal,
"uniqueContactUsers": uniqueContactUsers,
"todayContactLeads": todayContact,
"todaySubmitRecords": todaySubmit,
"combinedTotal": contactTotal + submitTotal,
},
})
}
func buildNewUsersOut(newUsers []model.User) []gin.H {
out := make([]gin.H, 0, len(newUsers))
for _, u := range newUsers {

View File

@@ -1075,3 +1075,31 @@ func BookStats(c *gin.Context) {
func BookSync(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步由 DB 维护"})
}
// BookRanking GET /api/miniprogram/book/ranking 内容排行榜(与 BookRecommended 同一套 computeArticleRankingSections
func BookRanking(c *gin.Context) {
limit := 50
if l := c.Query("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 200 {
limit = n
}
}
sections, err := computeArticleRankingSections(database.DB())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "排行榜计算失败"})
return
}
if len(sections) > limit {
sections = sections[:limit]
}
out := make([]gin.H, 0, len(sections))
for _, s := range sections {
out = append(out, gin.H{
"id": s.ID, "mid": s.MID, "title": s.Title,
"partTitle": s.PartTitle, "chapterTitle": s.ChapterTitle,
"hotScore": s.HotScore, "isPinned": s.IsPinned,
"price": s.Price, "isFree": s.IsFree, "isNew": s.IsNew,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
}

View File

@@ -78,6 +78,22 @@ func ckbSign(params map[string]interface{}, apiKey string) string {
return hex.EncodeToString(h2[:])
}
// userHasContentPurchase 与小程序资源对接 requirePurchase 一致:已付章节或全书解锁
func userHasContentPurchase(db *gorm.DB, userID string) bool {
if strings.TrimSpace(userID) == "" {
return false
}
var u model.User
if db.Select("has_full_book").Where("id = ?", userID).First(&u).Error == nil {
if u.HasFullBook != nil && *u.HasFullBook {
return true
}
}
var n int64
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND product_type = ?", userID, "paid", "section").Count(&n)
return n > 0
}
// getCkbLeadApiKey 链接卡若密钥优先级system_config.site_settings.ckbLeadApiKey > .env CKB_LEAD_API_KEY > 代码内置 ckbAPIKey
func getCkbLeadApiKey() string {
var row model.SystemConfig
@@ -119,6 +135,16 @@ func CKBJoin(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的加入类型"})
return
}
if body.Type == "investor" && body.UserID != "" {
if !userHasContentPurchase(database.DB(), body.UserID) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "请先购买章节或解锁全书后再使用资源对接",
"errorCode": "ERR_REQUIRE_PURCHASE",
})
return
}
}
nickname := strings.TrimSpace(body.Name)
if nickname == "" && body.UserID != "" {
var u model.User

View File

@@ -106,7 +106,7 @@ func buildMiniprogramConfig() gin.H {
}
}
}
// 好友优惠(用于 read 页展示优惠价)
// 好友优惠与分润(用于 read 页展示优惠价、分享提示分润比例
var refRow model.SystemConfig
if err := db.Where("config_key = ?", "referral_config").First(&refRow).Error; err == nil {
var refVal map[string]interface{}
@@ -114,6 +114,10 @@ func buildMiniprogramConfig() gin.H {
if v, ok := refVal["userDiscount"].(float64); ok {
out["userDiscount"] = v
}
// 内容订单分润比例0-100供小程序 read/referral 页展示
if v, ok := refVal["distributorShare"].(float64); ok {
out["shareRate"] = int(v)
}
}
}
if _, has := out["userDiscount"]; !has {
@@ -218,6 +222,7 @@ func GetCoreConfig(c *gin.Context) {
"prices": full["prices"],
"features": full["features"],
"userDiscount": full["userDiscount"],
"shareRate": full["shareRate"],
"mpConfig": full["mpConfig"],
}
if out["prices"] == nil {
@@ -229,6 +234,9 @@ func GetCoreConfig(c *gin.Context) {
if out["userDiscount"] == nil {
out["userDiscount"] = float64(5)
}
if out["shareRate"] == nil {
out["shareRate"] = 90
}
if out["mpConfig"] == nil {
out["mpConfig"] = gin.H{}
}
@@ -305,6 +313,7 @@ func WarmConfigCache() {
"prices": out["prices"],
"features": out["features"],
"userDiscount": out["userDiscount"],
"shareRate": out["shareRate"],
"mpConfig": out["mpConfig"],
}
if core["prices"] == nil {
@@ -316,6 +325,9 @@ func WarmConfigCache() {
if core["userDiscount"] == nil {
core["userDiscount"] = float64(5)
}
if core["shareRate"] == nil {
core["shareRate"] = 90
}
if core["mpConfig"] == nil {
core["mpConfig"] = gin.H{}
}

View File

@@ -591,6 +591,58 @@ func DBPersonPinnedList(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "persons": out})
}
// DBPersonPinnedToken GET /api/db/persons/pinned-token 当前置顶人物 token管理端预览/配置用,与置顶列表首条一致)
func DBPersonPinnedToken(c *gin.Context) {
db := database.DB()
var p model.Person
err := db.Where("is_pinned = ?", true).Order("updated_at DESC").First(&p).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusOK, gin.H{"success": true, "token": ""})
return
}
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "token": strings.TrimSpace(p.Token)})
}
// CKBPinnedPerson GET /api/miniprogram/ckb/pinned-person 小程序首页:当前置顶人物(无置顶时 data 为 null
func CKBPinnedPerson(c *gin.Context) {
db := database.DB()
var p model.Person
err := db.Where("is_pinned = ?", true).Order("updated_at DESC").First(&p).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
return
}
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
return
}
nickname := strings.TrimSpace(p.Name)
avatar := strings.TrimSpace(p.Avatar)
if p.UserID != nil && *p.UserID != "" {
var u model.User
if db.Select("nickname", "avatar").Where("id = ?", *p.UserID).First(&u).Error == nil {
if v := getStringValue(u.Nickname); v != "" {
nickname = v
}
if v := getUrlValue(u.Avatar); v != "" {
avatar = v
}
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"nickname": nickname,
"avatar": avatar,
"token": strings.TrimSpace(p.Token),
},
})
}
// AdminCKBPlanCheck GET /api/admin/ckb/plan-check 管理端-检查存客宝计划在线状态
// 查询所有有 ckb_plan_id 的 Person对每个计划调用存客宝获取状态
func AdminCKBPlanCheck(c *gin.Context) {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"soul-api/internal/database"
@@ -165,12 +166,22 @@ func MatchUsers(c *gin.Context) {
return
}
db := database.DB()
// 全书用户无限制,否则校验今日剩余次数
var user model.User
skipQuota := false
if err := db.Where("id = ?", body.UserID).First(&user).Error; err == nil {
skipQuota = user.HasFullBook != nil && *user.HasFullBook
if err := db.Where("id = ?", body.UserID).First(&user).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "用户不存在"})
return
}
phoneOK := user.Phone != nil && strings.TrimSpace(*user.Phone) != ""
wechatOK := user.WechatID != nil && strings.TrimSpace(*user.WechatID) != ""
if !phoneOK && !wechatOK {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "请先完善手机号或微信号后再发起匹配",
"errorCode": "ERR_PROFILE_INCOMPLETE",
})
return
}
skipQuota := user.HasFullBook != nil && *user.HasFullBook
if !skipQuota {
freeLimit := getFreeMatchLimit(db)
quota := GetMatchQuota(db, body.UserID, freeLimit)

View File

@@ -123,6 +123,13 @@ func WechatPhoneLogin(c *gin.Context) {
"referralCount": intVal(user.ReferralCount),
"createdAt": user.CreatedAt,
}
// 与 /api/miniprogram/login 一致,避免手机号登录后 VIP 引导、权益展示滞后
if user.IsVip != nil {
responseUser["isVip"] = *user.IsVip
}
if user.VipExpireDate != nil {
responseUser["vipExpireDate"] = user.VipExpireDate.Format("2006-01-02")
}
token := fmt.Sprintf("tk_%s_%d", openID[len(openID)-8:], time.Now().Unix())
c.JSON(http.StatusOK, gin.H{

View File

@@ -6,6 +6,7 @@ import (
"math"
"net/http"
"os"
"strings"
"time"
"soul-api/internal/config"
@@ -88,14 +89,20 @@ func WithdrawPost(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "用户未绑定微信"})
return
}
// 与小程序 referral 一致:须填写资料中的微信号,便于运营到账核对(不再用 openid 顶替)
if user.WechatID == nil || strings.TrimSpace(*user.WechatID) == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"needBindWechat": true,
"message": "请先到设置页绑定微信号后再提现,便于到账核对",
})
return
}
withdrawID := generateWithdrawID()
status := "pending"
// 根据 user_id 已查到的用户信息,填充提现表所需字段;仅写入表中存在的列,避免 remark 等列不存在报错
wechatID := user.WechatID
if (wechatID == nil || *wechatID == "") && user.OpenID != nil && *user.OpenID != "" {
wechatID = user.OpenID
}
withdrawal := model.Withdrawal{
ID: withdrawID,
UserID: req.UserID,

View File

@@ -35,14 +35,12 @@ type Person struct {
AddFriendInterval int `gorm:"column:add_friend_interval;default:1" json:"addFriendInterval"`
StartTime string `gorm:"column:start_time;size:10;default:'09:00'" json:"startTime"`
EndTime string `gorm:"column:end_time;size:10;default:'18:00'" json:"endTime"`
DeviceGroups string `gorm:"column:device_groups;size:255;default:''" json:"deviceGroups"` // 逗号分隔的设备ID列表
IsPinned bool `gorm:"column:is_pinned;default:false" json:"isPinned"`
DeviceGroups string `gorm:"column:device_groups;size:255;default:''" json:"deviceGroups"` // 逗号分隔的设备ID列表
IsPinned bool `gorm:"column:is_pinned;default:false" json:"isPinned"` // 置顶到小程序首页
// PersonSource 来源:空=后台手工添加vip_sync=超级个体自动同步(共用统一计划)
PersonSource string `gorm:"column:person_source;size:32;default:''" json:"personSource"`
IsPinned bool `gorm:"column:is_pinned;default:false" json:"isPinned"` // 置顶到小程序首页
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}

View File

@@ -18,13 +18,15 @@ func Init(url string) error {
if err != nil {
return err
}
client = redis.NewClient(opt)
tmp := redis.NewClient(opt)
ctx := context.Background()
if err := client.Ping(ctx).Err(); err != nil {
client = nil // 连接失败时清空避免后续使用超时cache 将自动降级到内存备用
if err := tmp.Ping(ctx).Err(); err != nil {
_ = tmp.Close() // 避免未关闭客户端在后台持续 dial刷屏 pool 重试日志
client = nil
log.Printf("redis: 连接失败,已降级到内存缓存(%v", err)
return err
}
client = tmp
log.Printf("redis: connected to %s", opt.Addr)
return nil
}

View File

@@ -215,7 +215,6 @@ func Setup(cfg *config.Config) *gin.Engine {
db.GET("/link-tags", handler.DBLinkTagList)
db.POST("/link-tags", handler.DBLinkTagSave)
db.DELETE("/link-tags", handler.DBLinkTagDelete)
db.PUT("/persons/pin", handler.DBPersonPin)
db.GET("/persons/pinned", handler.DBPersonPinnedList)
db.GET("/ckb-leads", handler.DBCKBLeadList)
db.GET("/ckb-person-leads", handler.DBCKBPersonLeads)

View File

@@ -1,4 +0,0 @@
-- 为 chapters 表添加 preview_percent 列章节级预览比例NULL 表示使用全局 unpaid_preview_percent
-- 执行: mysql -u user -p db < soul-api/scripts/add-chapters-preview-percent.sql
ALTER TABLE chapters ADD COLUMN IF NOT EXISTS preview_percent INT NULL COMMENT '章节级预览比例(%)NULL 表示使用全局设置' AFTER hot_score;

View File

@@ -1,3 +0,0 @@
-- ckb_lead_records 增加 ckb_error 字段,存客宝请求失败时写入错误信息
-- 执行: node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add_ckb_lead_error.sql
ALTER TABLE ckb_lead_records ADD COLUMN ckb_error VARCHAR(500) DEFAULT '' COMMENT '存客宝请求失败时的错误信息';

View File

@@ -1,3 +0,0 @@
-- 角标单独字段chapters 表新增 part_label
-- 执行: node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add_part_label.sql
ALTER TABLE chapters ADD COLUMN part_label VARCHAR(20) DEFAULT '' COMMENT '篇角标,如 一、二、派';

View File

@@ -1,6 +0,0 @@
-- 链接人与事 - persons 表新增 avatar 字段
-- 有 user_id 时头像从 users 表取;无 user_id 时用本字段(管理端可编辑)
-- 执行mysql -u user -p db < soul-api/scripts/add_persons_avatar.sql
-- 若已存在则忽略错误
ALTER TABLE persons ADD COLUMN avatar VARCHAR(512) DEFAULT '' COMMENT '头像URL无user_id时使用';

View File

@@ -1,3 +0,0 @@
-- 文章级未解锁预览百分比chapters.preview_percent
-- 空或0则使用全局 unpaid_preview_percent
ALTER TABLE chapters ADD COLUMN preview_percent INT DEFAULT NULL COMMENT '未解锁显示前N%,空则用全局';

View File

@@ -1,31 +0,0 @@
# 需求:文章级未解锁预览百分比
**日期**2026-03-20
## 需求描述
小程序每篇文章未解锁显示的百分比可在管理端文章编辑、添加时单独设置。每篇文章独立配置,默认按全局设置展示。
## 三端实现
### 后端 (soul-api)
- **chapters 表**:新增 `preview_percent` 字段INT NULL空则用全局 `unpaid_preview_percent`
- **book.go**`findChapterAndRespond` 中,付费未解锁时优先使用 `ch.PreviewPercent`,空则 `getUnpaidPreviewPercent(db)`
- **db_book.go**PUT 创建/更新支持 `previewPercent`GET read 返回 `previewPercent`
### 管理端 (soul-admin)
- **ContentPage**:新建章节、编辑章节表单增加「未解锁预览比例 (%)」输入框
- 占位符「空则用全局」1100 有效,空或 0 表示使用全局
### 小程序
- 无变更,后端按文章级或全局百分比返回预览内容
## 验收
- [ ] 管理端新建文章时可设置预览百分比
- [ ] 管理端编辑文章时可修改/清空预览百分比
- [ ] 未设置时小程序显示按全局配置
- [ ] 已设置时小程序显示按文章级配置

View File

@@ -1,143 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>代付订单详情 - 我发起的</title>
<!-- Tailwind CSS v3 CDN with plugins -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<style data-purpose="custom-colors">
/* Custom theme colors based on the design tokens */
:root {
--brand-mint: #4adeca;
--bg-dark: #000000;
--card-bg: #1c1c1e;
--text-muted: #8e8e93;
}
</style>
<style data-purpose="layout-styles">
body {
background-color: var(--bg-dark);
color: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.mint-glow {
box-shadow: 0 0 20px rgba(74, 222, 202, 0.3);
}
.custom-scrollbar::-webkit-scrollbar {
width: 0px;
}
</style>
</head>
<body class="flex flex-col min-h-screen">
<!-- BEGIN: Header -->
<header class="sticky top-0 z-10 bg-black/80 backdrop-blur-md px-4 py-4 flex items-center">
<button class="text-white mr-4" data-purpose="back-button">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M15 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</button>
<h1 class="text-lg font-medium">代付订单详情</h1>
</header>
<!-- END: Header -->
<main class="flex-grow px-5 pb-32">
<!-- BEGIN: Article Card -->
<section class="mt-4 mb-8" data-purpose="article-preview">
<div class="bg-[#1c1c1e] rounded-2xl p-5 border border-white/5">
<div class="flex justify-between items-start mb-4">
<div class="flex-1 pr-4">
<span class="inline-block bg-[#1a2d2a] text-[#4adeca] text-[10px] font-bold px-2 py-0.5 rounded border border-[#4adeca]/30 mb-2">FREE GIFT</span>
<h2 class="text-xl font-bold leading-tight mb-2">3000万流水如何跑出来(退税模式解析)</h2>
<p class="text-sm text-[#8e8e93] line-clamp-2">深入剖析跨境电商退税合规路径,解析三千万级流水规模下的财务闭环与架构...</p>
</div>
<div class="w-16 h-16 bg-[#2c2c2e] rounded-xl flex items-center justify-center shrink-0 border border-white/10">
<svg class="h-8 w-8 text-[#4adeca]" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path>
</svg>
</div>
</div>
<div class="border-t border-white/5 pt-4 flex justify-between items-center">
<span class="text-[#4adeca] text-sm italic">“我已为你买单,点击免费阅读”</span>
<svg class="h-4 w-4 text-[#8e8e93]" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</div>
</div>
</section>
<!-- END: Article Card -->
<!-- BEGIN: Claim Progress -->
<section class="mb-10" data-purpose="progress-section">
<div class="flex justify-between items-end mb-3">
<h3 class="text-sm font-medium text-white">领取进度</h3>
<span class="text-xs text-[#8e8e93]">已领取 <span class="text-[#4adeca] font-bold">3</span> / 5</span>
</div>
<!-- Progress Bar -->
<div class="w-full bg-[#2c2c2e] h-2 rounded-full overflow-hidden">
<div class="bg-[#4adeca] h-full rounded-full" style="width: 60%"></div>
</div>
</section>
<!-- END: Claim Progress -->
<!-- BEGIN: Friends Claim List -->
<section data-purpose="friends-list">
<h3 class="text-sm font-medium text-[#8e8e93] mb-4">领取详情</h3>
<div class="space-y-6">
<!-- Friend Item 1 -->
<div class="flex items-center justify-between">
<div class="flex items-center">
<img alt="User Avatar" class="w-10 h-10 rounded-full bg-gray-700 mr-3" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCy88duzYMwiZg941lgxnxeIp3W-VjiK6NfDZh45dViX3osk1Y953WQuUaBGy5kG2gKYs02dgrWgdFRm-cmHlw4USfI-zQZ4UfOmPoPYqV39tfr8JxKl818FwpRaCEPCh3E9CUX-Dd9AX4h77UzK-u_R8ov42zz1Q1KN2TSU7QtPm9W3zbG6N6gwrXY_91-xbea1vzHS4SYXZ53r3qYvkzhGVu5D3HcxgAJO-uYuDy2VZ8B2ryahrRt12nK50br5i4w-KG74A_ivis"/>
<div>
<p class="text-[15px] font-medium text-white">王小明</p>
<p class="text-xs text-[#8e8e93]">通过分享链接领取</p>
</div>
</div>
<span class="text-xs text-[#8e8e93]">10-24 14:20</span>
</div>
<!-- Friend Item 2 -->
<div class="flex items-center justify-between">
<div class="flex items-center">
<img alt="User Avatar" class="w-10 h-10 rounded-full bg-gray-700 mr-3" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBQDll7zv_l-i7hevQbdFDD3URTtlQB788GW_mpBoB2TRPEI0lqZ3l713ZCGC87YKuwf-nIp0z-VnD-rYV4b5gkmECUK92mLf0nBfPuQqHI-Fo-1uVCnExztDlE30XxRe6lIWYpL9UsdSaF6iBVDIdUyzHTZungvsM-QPj-C2S4IrwecJTumScH5yChMRjPfqbdmqyF2CTeOhCiYh7af63QFS5WdrOJ6ZWZTF6suUxrqs4Pr-086FTDnSLnyBi-r-AdiUK3DgNt8sQ"/>
<div>
<p class="text-[15px] font-medium text-white">Li Hua</p>
<p class="text-xs text-[#8e8e93]">通过微信群领取</p>
</div>
</div>
<span class="text-xs text-[#8e8e93]">10-24 15:05</span>
</div>
<!-- Friend Item 3 -->
<div class="flex items-center justify-between">
<div class="flex items-center">
<img alt="User Avatar" class="w-10 h-10 rounded-full bg-gray-700 mr-3" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCZSKjVj0NEcMcndyXVBy9-pBnv9Ml5nD3XK22C61MBFv7Gsv24560Zh4Xw3iWD4IxpXnRwNbNQOCqRpR8-sbZeVQ0uYQcT_bRxhFdZVB2LVZDTJTvSGKTIeZ3plotcOS4SiCX4m3qIMhGzKEr3htlfDTv8NyaKexdbHWd4X4Kx-U2qoZ9m3mfGRwNk29T5xTyBZstubZIvG213v4hJbJoM5ymHxtlv-9K42tYhMQFC_CpG2stMespb9MyiKg0haq95EVVBDp3quT4"/>
<div>
<p class="text-[15px] font-medium text-white">张大伟</p>
<p class="text-xs text-[#8e8e93]">通过分享链接领取</p>
</div>
</div>
<span class="text-xs text-[#8e8e93]">10-24 16:45</span>
</div>
<!-- Waiting Slot 4 -->
<div class="flex items-center justify-between opacity-50">
<div class="flex items-center">
<div class="w-10 h-10 rounded-full bg-[#2c2c2e] border-2 border-dashed border-[#444] flex items-center justify-center mr-3">
<span class="text-[#444] text-xs">?</span>
</div>
<div>
<p class="text-[15px] font-medium text-white/60">待领取</p>
<p class="text-xs text-[#8e8e93]">剩余 2 个名额</p>
</div>
</div>
</div>
</div>
</section>
<!-- END: Friends Claim List -->
</main>
<!-- BEGIN: Bottom Action -->
<footer class="fixed bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black via-black/90 to-transparent">
<button class="w-full bg-[#4adeca] text-black font-bold py-4 rounded-full flex items-center justify-center gap-2 mint-glow active:scale-[0.98] transition-transform">
<svg class="h-5 w-5" fill="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"></path>
</svg>
再次分享给好友
</button>
</footer>
<!-- END: Bottom Action -->
</body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

View File

@@ -1,133 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>生成代付链接 - 支付确认</title>
<!-- Tailwind CSS v3 CDN -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'mint-green': '#40E0D0', // Primary Mint Green from Screen 116/142
'dark-gray': '#1C1C1E', // Dark card background
'inner-card': '#242426', // Slightly lighter card for nested content
}
}
}
}
</script>
<style data-purpose="custom-styling">
/* Custom glassmorphism-like shadow and transitions */
.modal-shadow {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.btn-glow:active {
transform: scale(0.98);
opacity: 0.9;
}
.input-field:focus {
border-color: #40E0D0 !important;
ring-color: #40E0D0 !important;
}
/* Hide number input spinners */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>
</head>
<body class="bg-black flex items-center justify-center min-h-screen font-sans antialiased text-white">
<!-- BEGIN: ModalOverlay -->
<div class="fixed inset-0 bg-black/80 flex items-center justify-center p-6 z-50" data-purpose="payment-proxy-modal">
<!-- BEGIN: ModalContent -->
<div class="bg-dark-gray w-full max-w-sm rounded-[32px] p-6 relative modal-shadow border border-white/5">
<!-- Close Button -->
<button aria-label="Close" class="absolute right-6 top-6 text-gray-500 hover:text-white transition-colors">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</button>
<!-- Header Title -->
<div class="mt-4 mb-8 text-center">
<h2 class="text-xl font-bold tracking-tight">生成代付链接</h2>
</div>
<!-- BEGIN: ArticlePreview -->
<div class="bg-inner-card rounded-2xl p-5 mb-6 border border-white/5" data-purpose="article-info">
<h3 class="text-lg font-bold leading-tight mb-3">3000万流水如何跑出来(退税模式解析)</h3>
<p class="text-gray-400 text-sm leading-relaxed line-clamp-2">
深入剖析跨境电商退税合规路径,解析三千万级流水规模下的财务闭环与架构...
</p>
</div>
<!-- END: ArticlePreview -->
<!-- BEGIN: InputSection -->
<div class="space-y-4 mb-8" data-purpose="payment-form">
<div>
<label class="block text-sm text-gray-400 mb-4 ml-1">选择代付名额数</label>
<div class="grid grid-cols-4 gap-3" id="spots-selection">
<button class="spot-btn border border-white/10 bg-inner-card rounded-xl py-3 text-white hover:border-mint-green/50 transition-all font-medium" data-value="6">6</button>
<button class="spot-btn border border-white/10 bg-inner-card rounded-xl py-3 text-white hover:border-mint-green/50 transition-all font-medium" data-value="30">30</button>
<button class="spot-btn border border-white/10 bg-inner-card rounded-xl py-3 text-white hover:border-mint-green/50 transition-all font-medium" data-value="100">100</button>
<button class="spot-btn border border-white/10 bg-inner-card rounded-xl py-3 text-white hover:border-mint-green/50 transition-all font-medium" data-value="1000">1000</button>
</div>
</div>
<!-- BEGIN: PriceDisplay -->
<div class="flex flex-col items-center justify-center py-4" data-purpose="price-calculation">
<span class="text-xs text-gray-500 mb-1">待支付总价格</span>
<div class="flex items-baseline gap-2">
<span class="text-mint-green text-sm">¥99.00 × <span id="display-spots">1</span> =</span>
<span class="text-mint-green text-3xl font-bold" id="total-price">¥99.00</span>
</div>
</div>
<!-- END: PriceDisplay -->
</div>
<!-- END: InputSection -->
<!-- BEGIN: ActionButtons -->
<div class="flex flex-col gap-4">
<button class="w-full bg-mint-green text-black font-bold py-4 rounded-full text-lg shadow-[0_0_20px_rgba(64,224,208,0.3)] btn-glow transition-all" id="confirm-btn">
确认并支付
</button>
<button class="w-full py-2 text-gray-500 text-sm font-medium hover:text-gray-300 transition-colors">
取消
</button>
</div>
<!-- END: ActionButtons -->
</div>
<!-- END: ModalContent -->
</div>
<!-- END: ModalOverlay -->
<!-- BEGIN: LogicScript -->
<script data-purpose="simple-price-calculator">
const buttons = document.querySelectorAll('.spot-btn');
const displaySpots = document.getElementById('display-spots');
const totalPrice = document.getElementById('total-price');
const UNIT_PRICE = 99.00;
buttons.forEach(btn => {
btn.addEventListener('click', () => {
// Reset all buttons
buttons.forEach(b => {
b.classList.remove('border-mint-green', 'text-mint-green', 'bg-mint-green/10');
b.classList.add('border-white/10', 'text-white', 'bg-inner-card');
});
// Highlight selected button
btn.classList.add('border-mint-green', 'text-mint-green', 'bg-mint-green/10');
btn.classList.remove('border-white/10', 'text-white', 'bg-inner-card');
// Update price
const val = parseInt(btn.getAttribute('data-value'));
displaySpots.textContent = val;
const total = (val * UNIT_PRICE).toFixed(2);
totalPrice.textContent = `¥${total}`;
});
});
// Initialize with first button
buttons[0].click();
</script>
<!-- END: LogicScript -->
</body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -1,181 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>订单列表 - 代付共享</title>
<!-- Tailwind CSS v3 with plugins -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
DEFAULT: '#4ade80',
dark: '#1f2937',
surface: '#121212',
card: '#1e1e1e',
},
mint: '#44d7b6',
}
}
}
}
</script>
<style data-purpose="custom-styling">
body {
background-color: #000000;
color: #ffffff;
-webkit-tap-highlight-color: transparent;
}
.tab-active {
color: #44d7b6;
border-bottom: 2px solid #44d7b6;
}
/* Smooth transition for tab switching */
.tab-content {
transition: opacity 0.2s ease-in-out;
}
.hidden-tab {
display: none;
}
/* Subtle glow for brand elements */
.brand-glow {
box-shadow: 0 0 15px rgba(68, 215, 182, 0.3);
}
</style>
</head>
<body class="min-h-screen font-sans">
<!-- BEGIN: MainHeader -->
<header class="sticky top-0 z-50 bg-black/80 backdrop-blur-md border-b border-white/10">
<div class="px-4 py-4 flex items-center justify-between">
<button class="p-1">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M15 19l-7-7 7-7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</button>
<h1 class="text-lg font-bold">订单列表</h1>
<div class="w-8"></div> <!-- Spacer for centering -->
</div>
</header>
<!-- END: MainHeader -->
<main class="p-4 space-y-4">
<!-- BEGIN: I Initiated List -->
<section class="tab-content space-y-3" id="list-initiated">
<!-- Order Card 1 -->
<article class="bg-brand-card rounded-2xl p-5 border border-white/5" data-purpose="order-card">
<div class="flex justify-between items-start mb-4">
<h2 class="text-base font-bold flex-1 pr-4 leading-relaxed">3000万流水如何跑出来(退税模式解析)</h2>
<span class="text-mint font-bold text-lg shrink-0">¥99.00</span>
</div>
<div class="flex items-center justify-between pt-4 border-t border-white/5">
<div class="flex items-center space-x-2 text-sm text-gray-400">
<span>总共 5 份</span>
<span class="text-gray-600">·</span>
<span class="text-mint">还剩 2 份</span>
</div>
<button class="px-5 py-2 bg-mint/10 text-mint text-xs font-bold rounded-full border border-mint/20 hover:bg-mint/20 transition-colors">
再次分享
</button>
</div>
</article>
<!-- Order Card 2 -->
<article class="bg-brand-card rounded-2xl p-5 border border-white/5" data-purpose="order-card">
<div class="flex justify-between items-start mb-4">
<h2 class="text-base font-bold flex-1 pr-4 leading-relaxed">跨境电商财务合规与架构设计实战手册</h2>
<span class="text-mint font-bold text-lg shrink-0">¥128.00</span>
</div>
<div class="flex items-center justify-between pt-4 border-t border-white/5">
<div class="flex items-center space-x-2 text-sm text-gray-400">
<span>总共 3 份</span>
<span class="text-gray-600">·</span>
<span>已领完</span>
</div>
<button class="px-5 py-2 bg-white/5 text-gray-400 text-xs font-bold rounded-full border border-white/5 hover:bg-white/10 transition-colors">
查看详情
</button>
</div>
</article>
</section>
<!-- END: I Initiated List -->
<!-- BEGIN: I Paid For List (Hidden by default, keeping for structure if needed later) -->
<section class="tab-content space-y-4 hidden-tab" id="list-paid">
<!-- Paid Card 1 -->
<article class="bg-brand-card rounded-2xl p-4 border border-white/5" data-purpose="order-card">
<div class="flex justify-between items-start mb-3">
<h3 class="text-base font-bold flex-1 pr-4 leading-tight">2024全球供应链趋势分析报告</h3>
<span class="text-mint font-bold text-lg">¥59.00</span>
</div>
<!-- Friend Info Section -->
<div class="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="relative">
<img alt="Avatar" class="w-10 h-10 rounded-full border-2 border-mint/50 p-0.5" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBh6DTueRcPNv93o66NBMhoFPhc6CMe14WYnm2-IWYG3DWUHVV4-LxnEojzT-XSjMVYGPASbct9a5KEPXJSc-fM9UG_bS6p2xJ2mS_cvffCFi3P0V7ZmFEosMgT3R6utOJvOUrCKbjScpAiRQaU8m_Jw_XdlRG-xYqNojme_v0yBhSYIo_sioCEeYgP72dL8f6reDQ4wA68WmCQPNlanpBeTdvGdSVQgEHvBbqCvInlGuCazj79aCQUdRMUNqth5O1sODz7uIbnDxk"/>
</div>
<div>
<p class="text-xs text-gray-500">为好友代付</p>
<p class="text-sm font-medium">张小龙</p>
</div>
</div>
<button class="text-mint text-xs flex items-center">
查看文章
<svg class="h-3 w-3 ml-1" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</button>
</div>
</article>
<!-- Paid Card 2 -->
<article class="bg-brand-card rounded-2xl p-4 border border-white/5" data-purpose="order-card">
<div class="flex justify-between items-start mb-3">
<h3 class="text-base font-bold flex-1 pr-4 leading-tight">高保真原型设计进阶课</h3>
<span class="text-mint font-bold text-lg">¥199.00</span>
</div>
<div class="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="relative">
<img alt="Avatar" class="w-10 h-10 rounded-full border-2 border-mint/50 p-0.5" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDSj1xdhZYfbS-_FRPiwvYqvStJ5tQhU6pIBEyGAKx88n2MWrX6UjfzIFK84BTL6UGOxq-FRcjn41fhLXD1DquNdRif97qp_iCAtCI8n0WZ1k1kYojPjUBe-Axhv6eRldKpOnUx7rIQxNLWW148gYboyOk4QO3C2duT_QhulEihi00n08hTYj-pPgOkj63v78IlNYe-906lkOcBNuk_mmTFLG9VAFaXQuhn8yq8MWFi_UpYKIpNAVV02DOIZWYy5_z7wHhbajXmWX8"/>
</div>
<div>
<p class="text-xs text-gray-500">为好友代付</p>
<p class="text-sm font-medium">李建国</p>
</div>
</div>
<button class="text-mint text-xs flex items-center">
查看文章
<svg class="h-3 w-3 ml-1" fill="none" stroke="currentColor" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>
</button>
</div>
</article>
</section>
<!-- END: I Paid For List -->
</main>
<!-- BEGIN: Bottom Interaction -->
<div class="fixed bottom-8 left-0 right-0 px-6 flex justify-center pointer-events-none">
<button class="pointer-events-auto flex items-center justify-center bg-mint text-black font-bold py-4 px-10 rounded-full w-full max-w-sm brand-glow transition-transform active:scale-95">
<svg class="h-5 w-5 mr-2" fill="currentColor" viewbox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clip-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" fill-rule="evenodd"></path>
</svg>
发起新的代付
</button>
</div>
<!-- END: Bottom Interaction -->
<script data-purpose="tab-switcher">
function switchTab(type) {
const initiatedList = document.getElementById('list-initiated');
const paidList = document.getElementById('list-paid');
// Since tabs are removed, this functionality is effectively disabled but kept for structure
if (type === 'initiated') {
initiatedList.classList.remove('hidden-tab');
paidList.classList.add('hidden-tab');
} else {
initiatedList.classList.add('hidden-tab');
paidList.classList.remove('hidden-tab');
}
}
</script>
</body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -6,3 +6,5 @@
|------|------|
| [项目落地推进表](项目落地推进表.md) | 当前阶段、里程碑、风险与阻塞、永平落地 |
| [运营与变更](运营与变更.md) | 运营数据、soul-admin 变更、近期讨论、技术决策 |
| [产品意图与功能闭环分析](产品意图与功能闭环分析.md) | 从代码推导产品意图,识别未闭环功能,后端/API 补齐清单 |
| [需求未补齐清单](需求未补齐清单.md) | 以代码为准的待办项、文档类缺口 |

View File

@@ -560,3 +560,20 @@ Mycontent-temp/miniprogram 为样式预览分支miniprogram 为线上主线
- `开发文档/1、需求/三端需求整理.md`:按小程序/管理端/后端分类的需求整理
- `开发文档/1、需求/需求汇总.md`:需求清单已追加 2026-03-20 提现相关条目
---
# 第二十二部分以代码为准反向补齐文档2026-03-24
## 原则
需求文档滞后于实现时,**以实际代码行为为准,反向补齐文档**。不从需求文档推断实现,而从代码推断已实现需求。
## 本次补齐
| 文档 | 补齐内容 |
|------|----------|
| 《以界面定需求》 | 补充 avatar-nickname、gift-pay 系列、wallet、link-preview§4.5 资料完善引导、§4.6 购买≥3 章解锁全书 |
| 《需求汇总》 | 资料完善引导、≥3 章解锁全书、config.shareRate 已实现,补入需求清单 |
| 《需求未补齐清单》 | 重构代码已实现项移出待办仅保留富文本、ERR_PROFILE_INCOMPLETE 为真正待办 |
| 《需求基准》 | 需求汇总增加「以代码为准」原则 |

View File

@@ -1,7 +1,7 @@
# 项目落地推进表(里程碑)
> 作为《需求汇总》《以界面定需求》《运营与变更》的“执行层”补充:记录**当前阶段目标**、**里程碑**、**阻塞项**与**下一步**。
> 更新日期2026-03-18
> **以代码为准**:需求滞后时从代码反向补齐文档。更新日期2026-03-24
---

View File

@@ -6,9 +6,9 @@
## 需求基准
- 需求以《以界面定需求》为准,以界面定需求
- **以代码为准**:需求文档滞后于实现时,以实际代码行为为准,**反向补齐文档**。
- 需求以《以界面定需求》为界面级基准;新增/变更功能时先对齐界面再落需求清单。
- 需求文件命名:`YYYY-MM-DD-需求.md`**日期最新的为主需求文件**
- 同步需求时:新建或更新当日需求文件,并更新本目录 `索引.md`
---
@@ -47,6 +47,9 @@
| 2026-03-20 | 推广设置:提现手续费、自动提现开关 | 已完成 | ReferralSettingsPage |
| 2026-03-20 | 提现审核列表:自动审批开关、备注列 | 已完成 | DistributionPage |
| 2026-03-20 | 提现失败记录fail_reason/error_message 落库 | 已完成 | admin_withdrawals |
| 2026-03-18 | 资料完善引导checkVipContactRequiredAndGuide、avatar-nickname、profile-edit、VIP 支付成功引导、新用户强制引导 | 已完成 | app.js、miniprogram/docs/资料完善引导流程图.md |
| 2026-03-18 | 购买≥3 章显示「解锁全书」按钮 | 已完成 | read.wxml wx:if="{{purchasedCount >= 3}}" |
| 2026-03-24 | 分润比例前端从 config 读取shareRate | 已完成 | config/core、read/referral 页 |
---

View File

@@ -43,6 +43,12 @@
| **pages/search/search** | 搜索 | `GET /api/miniprogram/book/*` 或搜索接口 |
| **pages/agreement/agreement** | 用户协议 | 静态或配置 |
| **pages/privacy/privacy** | 隐私政策 | 静态或配置 |
| **pages/avatar-nickname/avatar-nickname** | 头像+昵称引导页(新用户/非 VIP 完善用) | 无接口,跳转自 app.checkAvatarNicknameAndGuide |
| **pages/gift-pay/detail** | 代付详情:发起人分享/好友帮他付款 | `GET /api/miniprogram/gift-pay/detail`、支付与领取接口 |
| **pages/gift-pay/list** | 我的代付列表 | `GET /api/miniprogram/gift-pay/my-requests` 等 |
| **pages/gift-pay/redemption-detail** | 代付领取详情(发起人查看领取明细) | gift-pay 相关接口 |
| **pages/wallet/wallet** | 余额/钱包 | 余额相关 `/api/miniprogram/*` |
| **pages/link-preview/link-preview** | 链接预览(分享/H5 跳转用) | 静态或配置 |
---
@@ -106,9 +112,27 @@
| 规则 | 说明 |
|------|------|
| 推广中心 | 管理端「推广中心」对应 distribution小程序「推广中心」对应 referral 页(海报、数据、提现)。 |
| 会员分润 | 会员订单推广者 20%、非会员 10%(可配置);内容订单推广者 90%。 |
| 会员分润 | 会员订单推广者 20%、非会员 10%(可配置);内容订单推广者 90%可配置config.shareRate。 |
| 提现 | 小程序申请提现走 `/api/miniprogram/withdraw`;管理端审核/打款走 `/api/admin/withdrawals`。 |
### 4.5 资料完善引导(代码已实现)
| 规则 | 说明 |
|------|------|
| 入口统一 | `app.checkVipContactRequiredAndGuide`onLaunch 1.5s / onShow 0.5s 节流 5min / 登录成功 1.2s / VIP 支付成功) |
| 非 VIP | `checkAvatarNicknameAndGuide`:头像/昵称未完善且今日未提示 → 弹窗「请设置头像和昵称」→ navigateTo avatar-nickname |
| VIP | 头像/昵称未改 → 弹窗「完善资料」→ redirectTo profile-edit无手机号 → 弹窗引导;无微信号 → 弹窗引导 |
| 新用户 | 登录返回 isNewUser 且头像昵称未改 → redirectTo avatar-nickname无弹窗 |
| VIP 支付成功 | 弹窗「请填写好资料」→ redirectTo profile-edit?from=vip |
| 页面分工 | avatar-nickname仅头像+昵称profile-edit完整资料手机、微信号、MBTI 等) |
### 4.6 购买≥3 章解锁全书
| 规则 | 说明 |
|------|------|
| 展示条件 | 阅读页付费墙:`purchasedCount >= 3` 时显示「解锁全部 X 章」按钮 |
| 无独立弹窗 | 当前实现为按钮直接展示,购买第 3 章后自动出现;无「购买成功弹窗引导解锁全书」 |
---
## 五、与需求文档的关系
@@ -128,3 +152,4 @@
| 2026-03-11 | 初版小程序与管理端界面清单、业务逻辑对齐VIP 资料以用户资料为准、三端路由、免费章与 VIP、分销提现与需求汇总、README、运营与变更同步。 |
| 2026-03-17 | 管理端清单补充:用户规则、用户余额、订单支付方式;详见《管理端迁移分析-基于小程序功能.md》。 |
| 2026-03-20 | 小程序:登录改为手机号一键登录;新增公用组件 login-modalread/my/gift-pay 引入getPhoneNumber 需耦合 agreePrivacyAuthorization。 |
| 2026-03-24 | **以代码为准反向补齐**:补充 avatar-nickname、gift-pay 系列、wallet、link-preview§4.5 资料完善引导、§4.6 购买≥3章解锁全书config.shareRate 分润展示。 |

View File

@@ -1,6 +1,6 @@
# Soul 创业派对 - 开发文档索引
> 按目录与文件标注用途便于快速定位。最后更新2026-03-20
> 按目录与文件标注用途,便于快速定位。**以代码为准**:需求滞后时反向补齐文档。最后更新2026-03-24
---