diff --git a/.cursor/agent/产品经理/evolution/2026-03-24.md b/.cursor/agent/产品经理/evolution/2026-03-24.md new file mode 100644 index 00000000..81bc94fe --- /dev/null +++ b/.cursor/agent/产品经理/evolution/2026-03-24.md @@ -0,0 +1,11 @@ +# 产品经理 经验记录 - 2026-03-24 + +## 开发进度同步会议 + +### 文档同步原则 +- 实现变更后需同步更新:《需求汇总》《运营与变更》及对应角色项目索引。 +- 项目索引「最后更新」应与实际变更日期一致,避免滞后。 + +### 当前状态 +- 2026-03-20 需求(提现、我的收益、推广设置等)已与实现对齐。 +- 主需求、落地推进表已基本同步;项目索引已补齐至 2026-03-24。 diff --git a/.cursor/agent/产品经理/evolution/索引.md b/.cursor/agent/产品经理/evolution/索引.md index 5e876b42..fb0c36ef 100644 --- a/.cursor/agent/产品经理/evolution/索引.md +++ b/.cursor/agent/产品经理/evolution/索引.md @@ -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) | diff --git a/.cursor/agent/后端工程师/evolution/2026-03-24.md b/.cursor/agent/后端工程师/evolution/2026-03-24.md new file mode 100644 index 00000000..f5cc56ad --- /dev/null +++ b/.cursor/agent/后端工程师/evolution/2026-03-24.md @@ -0,0 +1,17 @@ +# 后端工程师 经验记录 - 2026-03-24 + +## 开发进度同步会议 + +### 提现相关(2026-03-20 已落地,与文档一致) +- 审批逻辑:doApproveWithdrawal 校验「累计-已提现>=待审核」,-0.01 浮点容差。 +- referral_config:withdrawFee、enableAutoWithdraw、minWithdrawAmount 使用正确。 +- admin_withdrawals:fail_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。 diff --git a/.cursor/agent/后端工程师/evolution/索引.md b/.cursor/agent/后端工程师/evolution/索引.md index 598379a7..99584d5c 100644 --- a/.cursor/agent/后端工程师/evolution/索引.md +++ b/.cursor/agent/后端工程师/evolution/索引.md @@ -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 缺失四 handler:BookRanking、DBPersonPinnedToken、CKBPinnedPerson、AdminDashboardLeads | [2026-03-24.md](./2026-03-24.md) | diff --git a/.cursor/agent/团队/evolution/2026-03-24.md b/.cursor/agent/团队/evolution/2026-03-24.md new file mode 100644 index 00000000..8677aaf8 --- /dev/null +++ b/.cursor/agent/团队/evolution/2026-03-24.md @@ -0,0 +1,12 @@ +# 团队 经验记录 - 2026-03-24 + +## 开发进度同步会议 - 文档同步原则(跨角色共识) + +### 团队级决议 +- **实现变更后**需同步更新:`开发文档/1、需求/需求汇总.md`、`开发文档/10、项目管理/运营与变更.md` 及对应角色 `agent/开发助理/项目索引/{角色}.md`。 +- **项目索引**:每次开发完成或会议收尾后,在开发进度表追加一行(含日期),并将「最后更新」改为当前日期。 +- 避免文档与实现脱节:索引滞后会导致下次同步会议重复盘点。 + +### 落地建议 +- 各角色在完成功能开发或吸收经验时,主动更新项目索引。 +- 橙子收尾时统一检查并补齐索引。 diff --git a/.cursor/agent/团队/evolution/索引.md b/.cursor/agent/团队/evolution/索引.md index 9d0b01f5..115c0ae5 100644 --- a/.cursor/agent/团队/evolution/索引.md +++ b/.cursor/agent/团队/evolution/索引.md @@ -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) | diff --git a/.cursor/agent/小程序开发工程师/evolution/2026-03-24.md b/.cursor/agent/小程序开发工程师/evolution/2026-03-24.md new file mode 100644 index 00000000..419fc7a1 --- /dev/null +++ b/.cursor/agent/小程序开发工程师/evolution/2026-03-24.md @@ -0,0 +1,10 @@ +# 小程序开发工程师 经验记录 - 2026-03-24 + +## 开发进度同步会议 + +### 近期已落地(与文档一致) +- 2026-03-19:原生按钮覆盖定位(cover-view)经验。 +- 2026-03-20:手机号一键登录、login-modal 公用组件;我的收益取 availableEarnings。 + +### 技术债 +- 富文本渲染(rich-text)待实施,需确认 DB 格式后再推进。 diff --git a/.cursor/agent/开发助理/项目索引/产品.md b/.cursor/agent/开发助理/项目索引/产品.md index aef54c31..61bee57b 100644 --- a/.cursor/agent/开发助理/项目索引/产品.md +++ b/.cursor/agent/开发助理/项目索引/产品.md @@ -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 diff --git a/.cursor/agent/开发助理/项目索引/后端.md b/.cursor/agent/开发助理/项目索引/后端.md index 15a3ef98..59948dd5 100644 --- a/.cursor/agent/开发助理/项目索引/后端.md +++ b/.cursor/agent/开发助理/项目索引/后端.md @@ -38,9 +38,11 @@ soul-api(Go + Gin + GORM + MySQL)提供三组路由:`/api/miniprogram/*` | 2026-03-17 | 性能优化会议:Redis 缓存接入(parts/hot/recommended/stats/config/章节 content)、容灾回退 DB;OSS 上传接入;/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 diff --git a/.cursor/agent/开发助理/项目索引/小程序.md b/.cursor/agent/开发助理/项目索引/小程序.md index 3639070e..0960c021 100644 --- a/.cursor/agent/开发助理/项目索引/小程序.md +++ b/.cursor/agent/开发助理/项目索引/小程序.md @@ -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 diff --git a/.cursor/agent/开发助理/项目索引/测试.md b/.cursor/agent/开发助理/项目索引/测试.md index ea8820bb..1abd9de4 100644 --- a/.cursor/agent/开发助理/项目索引/测试.md +++ b/.cursor/agent/开发助理/项目索引/测试.md @@ -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 diff --git a/.cursor/agent/开发助理/项目索引/管理端.md b/.cursor/agent/开发助理/项目索引/管理端.md index 7c0d69ae..8068170d 100644 --- a/.cursor/agent/开发助理/项目索引/管理端.md +++ b/.cursor/agent/开发助理/项目索引/管理端.md @@ -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 类型错误(与本次迁移无关),待修复。 diff --git a/.cursor/agent/管理端开发工程师/evolution/2026-03-24.md b/.cursor/agent/管理端开发工程师/evolution/2026-03-24.md new file mode 100644 index 00000000..4414d4a4 --- /dev/null +++ b/.cursor/agent/管理端开发工程师/evolution/2026-03-24.md @@ -0,0 +1,10 @@ +# 管理端开发工程师 经验记录 - 2026-03-24 + +## 开发进度同步会议 + +### 提现相关(2026-03-20 已落地,与文档一致) +- 提现审核:备注列(fail_reason/error_message)、自动审批开关。 +- 推广设置:提现手续费、自动提现开关。 + +### 待办 +- DistributionPage Order.description 类型错误待修(与本次迁移无关,有空即修)。 diff --git a/.cursor/agent/软件测试/evolution/2026-03-24.md b/.cursor/agent/软件测试/evolution/2026-03-24.md new file mode 100644 index 00000000..fbbb5873 --- /dev/null +++ b/.cursor/agent/软件测试/evolution/2026-03-24.md @@ -0,0 +1,8 @@ +# 软件测试 经验记录 - 2026-03-24 + +## 开发进度同步会议 + +### 用例补充待办 +- 提现审批、手机号登录、我的收益、推广设置、提现审核等新增功能需补充/更新用例。 +- 边界必测:singlePage 分享进入、getPhoneNumber 隐私协议同意流程。 +- 下次回归前完成用例更新。 diff --git a/.cursor/meeting/2026-03-24_开发进度同步会议.md b/.cursor/meeting/2026-03-24_开发进度同步会议.md new file mode 100644 index 00000000..42b647c7 --- /dev/null +++ b/.cursor/meeting/2026-03-24_开发进度同步会议.md @@ -0,0 +1,114 @@ +# 会议纪要 - 2026-03-24 | 开发进度同步会议 + +> 本文件由**助理橙子**在会议结束后自动生成。 + +--- + +## 基本信息 + +- **时间**:2026-03-24 +- **议题**:全员查看自己的代码与开发文档,同步开发进度 +- **触发方式**:开个会议,所有的人查看自己的代码和@开发文档同步开发进度 +- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员 + +--- + +## 各角色发言 + +### 【产品经理】 + +- 2026-03-20 需求(提现审批、我的收益、推广设置等)已与实现对齐;小程序手机号登录变更已写入运营与变更。 +- 项目索引、主需求、落地推进表「最后更新」停在 2026-03-18,缺 2026-03-19、2026-03-20 及之后的记录。 +- 待办:补充项目索引近期决议;链接人与事置顶按《链接人与事-置顶与超级个体对应设计》为准。 + +### 【后端开发】 + +- 提现审批逻辑、referral_config(withdrawFee、enableAutoWithdraw)、admin_withdrawals 落库与文档一致。 +- 后端项目索引最后更新 2026-03-18,未记录 2026-03-20 提现相关变更。 +- 待办:项目索引补充 2026-03-20;router 补齐(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`* diff --git a/.cursor/meeting/README.md b/.cursor/meeting/README.md index e62e85d5..70427633 100644 --- a/.cursor/meeting/README.md +++ b/.cursor/meeting/README.md @@ -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) | diff --git a/.cursor/skills/assistant-doc-sync/sync-log.md b/.cursor/skills/assistant-doc-sync/sync-log.md index 39e8a20e..fff5e8d7 100644 --- a/.cursor/skills/assistant-doc-sync/sync-log.md +++ b/.cursor/skills/assistant-doc-sync/sync-log.md @@ -9,3 +9,4 @@ ## 2026-03-24 - Skill 优化:assistant-doc-sync 新增 sync-log 记忆机制、assets 模板、Skill 撰写原则 +- 会议收尾:开发进度同步会议;项目索引五角色补齐;纪要 `.cursor/meeting/2026-03-24_开发进度同步会议.md` diff --git a/miniprogram/app.js b/miniprogram/app.js index 7e70cb16..3098061c 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -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 diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js index d37f9b96..d45b356c 100644 --- a/miniprogram/pages/about/about.js +++ b/miniprogram/pages/about/about.js @@ -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 === '章节')) diff --git a/miniprogram/pages/gift-pay/detail.js b/miniprogram/pages/gift-pay/detail.js index fde113c3..0e6bc19a 100644 --- a/miniprogram/pages/gift-pay/detail.js +++ b/miniprogram/pages/gift-pay/detail.js @@ -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 }) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index 5f244bba..92987653 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -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, diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js index 5ac08243..f7cd7f50 100644 --- a/miniprogram/pages/match/match.js +++ b/miniprogram/pages/match/match.js @@ -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) diff --git a/miniprogram/pages/member-detail/member-detail.js b/miniprogram/pages/member-detail/member-detail.js index 3ab48b40..1edfe3f7 100644 --- a/miniprogram/pages/member-detail/member-detail.js +++ b/miniprogram/pages/member-detail/member-detail.js @@ -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) { diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index 03558ae6..d99fdd40 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -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' }) } } diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index a78cec04..e9611f04 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -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({}), diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml index 2b1182c4..3a5dd744 100644 --- a/miniprogram/pages/read/read.wxml +++ b/miniprogram/pages/read/read.wxml @@ -106,7 +106,7 @@ - 分享后好友购买,你可获得 90% 收益 + 分享后好友购买,你可获得 {{shareRate || 90}}% 收益 diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js index c4d38c43..35736592 100644 --- a/miniprogram/pages/referral/referral.js +++ b/miniprogram/pages/referral/referral.js @@ -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,使用小程序默认截图 } diff --git a/miniprogram/pages/vip/vip.js b/miniprogram/pages/vip/vip.js index e59a8dd1..6354a2f9 100644 --- a/miniprogram/pages/vip/vip.js +++ b/miniprogram/pages/vip/vip.js @@ -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' }) } diff --git a/miniprogram/pages/wallet/wallet.js b/miniprogram/pages/wallet/wallet.js index 1fb2c64d..98b01d8b 100644 --- a/miniprogram/pages/wallet/wallet.js +++ b/miniprogram/pages/wallet/wallet.js @@ -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' }) } diff --git a/miniprogram/utils/chapterAccessManager.js b/miniprogram/utils/chapterAccessManager.js index 30f8fd86..49ed1e24 100644 --- a/miniprogram/utils/chapterAccessManager.js +++ b/miniprogram/utils/chapterAccessManager.js @@ -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 } } diff --git a/miniprogram/utils/contentParser.js b/miniprogram/utils/contentParser.js index 903c3ee5..1544d223 100644 --- a/miniprogram/utils/contentParser.js +++ b/miniprogram/utils/contentParser.js @@ -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 } — 图片 */ diff --git a/miniprogram/utils/soulBridge.js b/miniprogram/utils/soulBridge.js new file mode 100644 index 00000000..eb6fcb6d --- /dev/null +++ b/miniprogram/utils/soulBridge.js @@ -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} 是否提交成功 + */ +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 +} diff --git a/soul-api/internal/handler/admin_dashboard.go b/soul-api/internal/handler/admin_dashboard.go index 9170156a..7db3963a 100644 --- a/soul-api/internal/handler/admin_dashboard.go +++ b/soul-api/internal/handler/admin_dashboard.go @@ -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 { diff --git a/soul-api/internal/handler/book.go b/soul-api/internal/handler/book.go index 8fef420a..fb0e0705 100644 --- a/soul-api/internal/handler/book.go +++ b/soul-api/internal/handler/book.go @@ -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}) +} diff --git a/soul-api/internal/handler/ckb.go b/soul-api/internal/handler/ckb.go index 835ecff2..edd2e8c6 100644 --- a/soul-api/internal/handler/ckb.go +++ b/soul-api/internal/handler/ckb.go @@ -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 diff --git a/soul-api/internal/handler/db.go b/soul-api/internal/handler/db.go index 6c00184a..6ca8854e 100644 --- a/soul-api/internal/handler/db.go +++ b/soul-api/internal/handler/db.go @@ -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{} } diff --git a/soul-api/internal/handler/db_person.go b/soul-api/internal/handler/db_person.go index 60e8e8cb..5cc16a93 100644 --- a/soul-api/internal/handler/db_person.go +++ b/soul-api/internal/handler/db_person.go @@ -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) { diff --git a/soul-api/internal/handler/match.go b/soul-api/internal/handler/match.go index 8bb2236a..66d97477 100644 --- a/soul-api/internal/handler/match.go +++ b/soul-api/internal/handler/match.go @@ -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) diff --git a/soul-api/internal/handler/wechat.go b/soul-api/internal/handler/wechat.go index 799d6852..1cb4d3ae 100644 --- a/soul-api/internal/handler/wechat.go +++ b/soul-api/internal/handler/wechat.go @@ -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{ diff --git a/soul-api/internal/handler/withdraw.go b/soul-api/internal/handler/withdraw.go index 91dda7dd..a74f6ca9 100644 --- a/soul-api/internal/handler/withdraw.go +++ b/soul-api/internal/handler/withdraw.go @@ -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, diff --git a/soul-api/internal/model/person.go b/soul-api/internal/model/person.go index 45413cfd..b931f714 100644 --- a/soul-api/internal/model/person.go +++ b/soul-api/internal/model/person.go @@ -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"` } diff --git a/soul-api/internal/redis/redis.go b/soul-api/internal/redis/redis.go index 14cf574e..6edb2e9e 100644 --- a/soul-api/internal/redis/redis.go +++ b/soul-api/internal/redis/redis.go @@ -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 } diff --git a/soul-api/internal/router/router.go b/soul-api/internal/router/router.go index 4700f732..5e3e498d 100644 --- a/soul-api/internal/router/router.go +++ b/soul-api/internal/router/router.go @@ -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) diff --git a/soul-api/scripts/add-chapters-preview-percent.sql b/soul-api/scripts/add-chapters-preview-percent.sql deleted file mode 100644 index 030f1d0b..00000000 --- a/soul-api/scripts/add-chapters-preview-percent.sql +++ /dev/null @@ -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; diff --git a/soul-api/scripts/add_ckb_lead_error.sql b/soul-api/scripts/add_ckb_lead_error.sql deleted file mode 100644 index bb4fec52..00000000 --- a/soul-api/scripts/add_ckb_lead_error.sql +++ /dev/null @@ -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 '存客宝请求失败时的错误信息'; diff --git a/soul-api/scripts/add_part_label.sql b/soul-api/scripts/add_part_label.sql deleted file mode 100644 index ddec7cd0..00000000 --- a/soul-api/scripts/add_part_label.sql +++ /dev/null @@ -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 '篇角标,如 一、二、派'; diff --git a/soul-api/scripts/add_persons_avatar.sql b/soul-api/scripts/add_persons_avatar.sql deleted file mode 100644 index 198a2c61..00000000 --- a/soul-api/scripts/add_persons_avatar.sql +++ /dev/null @@ -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时使用'; diff --git a/soul-api/scripts/add_preview_percent.sql b/soul-api/scripts/add_preview_percent.sql deleted file mode 100644 index d7cc98b5..00000000 --- a/soul-api/scripts/add_preview_percent.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 文章级未解锁预览百分比:chapters.preview_percent --- 空或0则使用全局 unpaid_preview_percent -ALTER TABLE chapters ADD COLUMN preview_percent INT DEFAULT NULL COMMENT '未解锁显示前N%,空则用全局'; diff --git a/临时需求池/2026-03-20-文章级未解锁预览百分比.md b/临时需求池/2026-03-20-文章级未解锁预览百分比.md deleted file mode 100644 index 69bcf6a2..00000000 --- a/临时需求池/2026-03-20-文章级未解锁预览百分比.md +++ /dev/null @@ -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**:新建章节、编辑章节表单增加「未解锁预览比例 (%)」输入框 -- 占位符「空则用全局」;1–100 有效,空或 0 表示使用全局 - -### 小程序 - -- 无变更,后端按文章级或全局百分比返回预览内容 - -## 验收 - -- [ ] 管理端新建文章时可设置预览百分比 -- [ ] 管理端编辑文章时可修改/清空预览百分比 -- [ ] 未设置时小程序显示按全局配置 -- [ ] 已设置时小程序显示按文章级配置 diff --git a/代付页面/stitch_soul/_1/code.html b/代付页面/stitch_soul/_1/code.html deleted file mode 100644 index a08bf703..00000000 --- a/代付页面/stitch_soul/_1/code.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - -代付订单详情 - 我发起的 - - - - - - - -
- -

代付订单详情

-
- -
- -
-
-
-
-FREE GIFT -

3000万流水如何跑出来(退税模式解析)

-

深入剖析跨境电商退税合规路径,解析三千万级流水规模下的财务闭环与架构...

-
-
- - - -
-
-
-“我已为你买单,点击免费阅读” - - - -
-
-
- - -
-
-

领取进度

-已领取 3 / 5 -
- -
-
-
-
- - -
-

领取详情

-
- -
-
-User Avatar -
-

王小明

-

通过分享链接领取

-
-
-10-24 14:20 -
- -
-
-User Avatar -
-

Li Hua

-

通过微信群领取

-
-
-10-24 15:05 -
- -
-
-User Avatar -
-

张大伟

-

通过分享链接领取

-
-
-10-24 16:45 -
- -
-
-
-? -
-
-

待领取

-

剩余 2 个名额

-
-
-
-
-
- -
- - - - \ No newline at end of file diff --git a/代付页面/stitch_soul/_1/screen.png b/代付页面/stitch_soul/_1/screen.png deleted file mode 100644 index 44d4c7f6..00000000 Binary files a/代付页面/stitch_soul/_1/screen.png and /dev/null differ diff --git a/代付页面/stitch_soul/_2/code.html b/代付页面/stitch_soul/_2/code.html deleted file mode 100644 index 7d0e007e..00000000 --- a/代付页面/stitch_soul/_2/code.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - -生成代付链接 - 支付确认 - - - - - - - -
- - - -
- - - - - \ No newline at end of file diff --git a/代付页面/stitch_soul/_2/screen.png b/代付页面/stitch_soul/_2/screen.png deleted file mode 100644 index f086a12e..00000000 Binary files a/代付页面/stitch_soul/_2/screen.png and /dev/null differ diff --git a/代付页面/stitch_soul/_3/code.html b/代付页面/stitch_soul/_3/code.html deleted file mode 100644 index b110690c..00000000 --- a/代付页面/stitch_soul/_3/code.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - -订单列表 - 代付共享 - - - - - - - -
-
- -

订单列表

-
-
-
- -
- -
- -
-
-

3000万流水如何跑出来(退税模式解析)

-¥99.00 -
-
-
-总共 5 份 -· -还剩 2 份 -
- -
-
- -
-
-

跨境电商财务合规与架构设计实战手册

-¥128.00 -
-
-
-总共 3 份 -· -已领完 -
- -
-
-
- - -
- -
-
-

2024全球供应链趋势分析报告

-¥59.00 -
- -
-
-
-Avatar -
-
-

为好友代付

-

张小龙

-
-
- -
-
- -
-
-

高保真原型设计进阶课

-¥199.00 -
-
-
-
-Avatar -
-
-

为好友代付

-

李建国

-
-
- -
-
-
- -
- -
- -
- - - \ No newline at end of file diff --git a/代付页面/stitch_soul/_3/screen.png b/代付页面/stitch_soul/_3/screen.png deleted file mode 100644 index d986c388..00000000 Binary files a/代付页面/stitch_soul/_3/screen.png and /dev/null differ diff --git a/代付页面/stitch_soul/image.png/screen.png b/代付页面/stitch_soul/image.png/screen.png deleted file mode 100644 index 5927d7a0..00000000 Binary files a/代付页面/stitch_soul/image.png/screen.png and /dev/null differ diff --git a/开发文档/10、项目管理/README.md b/开发文档/10、项目管理/README.md index 7a742e13..9bd5de56 100644 --- a/开发文档/10、项目管理/README.md +++ b/开发文档/10、项目管理/README.md @@ -6,3 +6,5 @@ |------|------| | [项目落地推进表](项目落地推进表.md) | 当前阶段、里程碑、风险与阻塞、永平落地 | | [运营与变更](运营与变更.md) | 运营数据、soul-admin 变更、近期讨论、技术决策 | +| [产品意图与功能闭环分析](产品意图与功能闭环分析.md) | 从代码推导产品意图,识别未闭环功能,后端/API 补齐清单 | +| [需求未补齐清单](需求未补齐清单.md) | 以代码为准的待办项、文档类缺口 | diff --git a/开发文档/10、项目管理/运营与变更.md b/开发文档/10、项目管理/运营与变更.md index fc03b270..bc16dc68 100644 --- a/开发文档/10、项目管理/运营与变更.md +++ b/开发文档/10、项目管理/运营与变更.md @@ -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 为真正待办 | +| 《需求基准》 | 需求汇总增加「以代码为准」原则 | diff --git a/开发文档/10、项目管理/项目落地推进表.md b/开发文档/10、项目管理/项目落地推进表.md index 96bac05f..d317c940 100644 --- a/开发文档/10、项目管理/项目落地推进表.md +++ b/开发文档/10、项目管理/项目落地推进表.md @@ -1,7 +1,7 @@ # 项目落地推进表(里程碑) > 作为《需求汇总》《以界面定需求》《运营与变更》的“执行层”补充:记录**当前阶段目标**、**里程碑**、**阻塞项**与**下一步**。 -> 更新日期:2026-03-18 +> **以代码为准**:需求滞后时从代码反向补齐文档。更新日期:2026-03-24 --- diff --git a/开发文档/1、需求/2026-03-20-需求.md b/开发文档/1、需求/2026-03-20-需求.md index a9237fb8..f666fc30 100644 --- a/开发文档/1、需求/2026-03-20-需求.md +++ b/开发文档/1、需求/2026-03-20-需求.md @@ -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 页 | --- diff --git a/开发文档/1、需求/以界面定需求.md b/开发文档/1、需求/以界面定需求.md index b0718845..9f49ce39 100644 --- a/开发文档/1、需求/以界面定需求.md +++ b/开发文档/1、需求/以界面定需求.md @@ -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-modal(read/my/gift-pay 引入);getPhoneNumber 需耦合 agreePrivacyAuthorization。 | +| 2026-03-24 | **以代码为准反向补齐**:补充 avatar-nickname、gift-pay 系列、wallet、link-preview;§4.5 资料完善引导、§4.6 购买≥3章解锁全书;config.shareRate 分润展示。 | diff --git a/开发文档/索引.md b/开发文档/索引.md index 2f123d4f..74b24c68 100644 --- a/开发文档/索引.md +++ b/开发文档/索引.md @@ -1,6 +1,6 @@ # Soul 创业派对 - 开发文档索引 -> 按目录与文件标注用途,便于快速定位。最后更新:2026-03-20 +> 按目录与文件标注用途,便于快速定位。**以代码为准**:需求滞后时反向补齐文档。最后更新:2026-03-24 ---