同步
This commit is contained in:
11
.cursor/agent/产品经理/evolution/2026-03-24.md
Normal file
11
.cursor/agent/产品经理/evolution/2026-03-24.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 产品经理 经验记录 - 2026-03-24
|
||||
|
||||
## 开发进度同步会议
|
||||
|
||||
### 文档同步原则
|
||||
- 实现变更后需同步更新:《需求汇总》《运营与变更》及对应角色项目索引。
|
||||
- 项目索引「最后更新」应与实际变更日期一致,避免滞后。
|
||||
|
||||
### 当前状态
|
||||
- 2026-03-20 需求(提现、我的收益、推广设置等)已与实现对齐。
|
||||
- 主需求、落地推进表已基本同步;项目索引已补齐至 2026-03-24。
|
||||
@@ -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) |
|
||||
|
||||
17
.cursor/agent/后端工程师/evolution/2026-03-24.md
Normal file
17
.cursor/agent/后端工程师/evolution/2026-03-24.md
Normal file
@@ -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。
|
||||
@@ -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) |
|
||||
|
||||
12
.cursor/agent/团队/evolution/2026-03-24.md
Normal file
12
.cursor/agent/团队/evolution/2026-03-24.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 团队 经验记录 - 2026-03-24
|
||||
|
||||
## 开发进度同步会议 - 文档同步原则(跨角色共识)
|
||||
|
||||
### 团队级决议
|
||||
- **实现变更后**需同步更新:`开发文档/1、需求/需求汇总.md`、`开发文档/10、项目管理/运营与变更.md` 及对应角色 `agent/开发助理/项目索引/{角色}.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) |
|
||||
|
||||
10
.cursor/agent/小程序开发工程师/evolution/2026-03-24.md
Normal file
10
.cursor/agent/小程序开发工程师/evolution/2026-03-24.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 小程序开发工程师 经验记录 - 2026-03-24
|
||||
|
||||
## 开发进度同步会议
|
||||
|
||||
### 近期已落地(与文档一致)
|
||||
- 2026-03-19:原生按钮覆盖定位(cover-view)经验。
|
||||
- 2026-03-20:手机号一键登录、login-modal 公用组件;我的收益取 availableEarnings。
|
||||
|
||||
### 技术债
|
||||
- 富文本渲染(rich-text)待实施,需确认 DB 格式后再推进。
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 类型错误(与本次迁移无关),待修复。
|
||||
|
||||
10
.cursor/agent/管理端开发工程师/evolution/2026-03-24.md
Normal file
10
.cursor/agent/管理端开发工程师/evolution/2026-03-24.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 管理端开发工程师 经验记录 - 2026-03-24
|
||||
|
||||
## 开发进度同步会议
|
||||
|
||||
### 提现相关(2026-03-20 已落地,与文档一致)
|
||||
- 提现审核:备注列(fail_reason/error_message)、自动审批开关。
|
||||
- 推广设置:提现手续费、自动提现开关。
|
||||
|
||||
### 待办
|
||||
- DistributionPage Order.description 类型错误待修(与本次迁移无关,有空即修)。
|
||||
8
.cursor/agent/软件测试/evolution/2026-03-24.md
Normal file
8
.cursor/agent/软件测试/evolution/2026-03-24.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 软件测试 经验记录 - 2026-03-24
|
||||
|
||||
## 开发进度同步会议
|
||||
|
||||
### 用例补充待办
|
||||
- 提现审批、手机号登录、我的收益、推广设置、提现审核等新增功能需补充/更新用例。
|
||||
- 边界必测:singlePage 分享进入、getPhoneNumber 隐私协议同意流程。
|
||||
- 下次回归前完成用例更新。
|
||||
114
.cursor/meeting/2026-03-24_开发进度同步会议.md
Normal file
114
.cursor/meeting/2026-03-24_开发进度同步会议.md
Normal file
@@ -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`*
|
||||
@@ -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) |
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
## 2026-03-24
|
||||
|
||||
- Skill 优化:assistant-doc-sync 新增 sync-log 记忆机制、assets 模板、Skill 撰写原则
|
||||
- 会议收尾:开发进度同步会议;项目索引五角色补齐;纪要 `.cursor/meeting/2026-03-24_开发进度同步会议.md`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 === '章节'))
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({}),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,使用小程序默认截图
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } — 图片
|
||||
*/
|
||||
|
||||
|
||||
155
miniprogram/utils/soulBridge.js
Normal file
155
miniprogram/utils/soulBridge.js
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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 '存客宝请求失败时的错误信息';
|
||||
@@ -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 '篇角标,如 一、二、派';
|
||||
@@ -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时使用';
|
||||
@@ -1,3 +0,0 @@
|
||||
-- 文章级未解锁预览百分比:chapters.preview_percent
|
||||
-- 空或0则使用全局 unpaid_preview_percent
|
||||
ALTER TABLE chapters ADD COLUMN preview_percent INT DEFAULT NULL COMMENT '未解锁显示前N%,空则用全局';
|
||||
@@ -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 表示使用全局
|
||||
|
||||
### 小程序
|
||||
|
||||
- 无变更,后端按文章级或全局百分比返回预览内容
|
||||
|
||||
## 验收
|
||||
|
||||
- [ ] 管理端新建文章时可设置预览百分比
|
||||
- [ ] 管理端编辑文章时可修改/清空预览百分比
|
||||
- [ ] 未设置时小程序显示按全局配置
|
||||
- [ ] 已设置时小程序显示按文章级配置
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -6,3 +6,5 @@
|
||||
|------|------|
|
||||
| [项目落地推进表](项目落地推进表.md) | 当前阶段、里程碑、风险与阻塞、永平落地 |
|
||||
| [运营与变更](运营与变更.md) | 运营数据、soul-admin 变更、近期讨论、技术决策 |
|
||||
| [产品意图与功能闭环分析](产品意图与功能闭环分析.md) | 从代码推导产品意图,识别未闭环功能,后端/API 补齐清单 |
|
||||
| [需求未补齐清单](需求未补齐清单.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 为真正待办 |
|
||||
| 《需求基准》 | 需求汇总增加「以代码为准」原则 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 项目落地推进表(里程碑)
|
||||
|
||||
> 作为《需求汇总》《以界面定需求》《运营与变更》的“执行层”补充:记录**当前阶段目标**、**里程碑**、**阻塞项**与**下一步**。
|
||||
> 更新日期:2026-03-18
|
||||
> **以代码为准**:需求滞后时从代码反向补齐文档。更新日期:2026-03-24
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 页 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 分润展示。 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Soul 创业派对 - 开发文档索引
|
||||
|
||||
> 按目录与文件标注用途,便于快速定位。最后更新:2026-03-20
|
||||
> 按目录与文件标注用途,便于快速定位。**以代码为准**:需求滞后时反向补齐文档。最后更新:2026-03-24
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user