diff --git a/.cursor/agent/开发助理/evolution/2026-03-21-MBTI头像C端全链路兜底.md b/.cursor/agent/开发助理/evolution/2026-03-21-MBTI头像C端全链路兜底.md
new file mode 100644
index 00000000..173f3477
--- /dev/null
+++ b/.cursor/agent/开发助理/evolution/2026-03-21-MBTI头像C端全链路兜底.md
@@ -0,0 +1,13 @@
+# 2026-03-21 MBTI 头像 C 端全链路兜底
+
+## 问题
+系统设置瘦身与 MBTI 映射迁到用户管理后,需在小程序多页面与匹配接口统一「无微信头像 → MBTI 映射」行为,避免仅海报单点生效。
+
+## 做法
+- 新增 `miniprogram/utils/mbtiAvatar.js`(`resolveAvatarWithMbti`);`app.resolveAvatarWithMbti` 封装全局 map。
+- 我的页 `profileAvatarDisplay`;资料编辑 `avatarPreviewUrl`;profile-show、member-detail、referral 海报复用同一逻辑。
+- 后端 `match.go`:`avatar` 为空时用 `getMbtiAvatar`;响应增加 `mbti` 字段;找伙伴卡片 wxml 增加无图占位。
+- 管理端 `MbtiAvatarsManager` 补充 downloadFile 域名说明。
+
+## 可复用规则
+配置驱动展示:公开 `GET /api/miniprogram/config/mbti-avatars` + 本地短时缓存;业务侧只调 `resolveAvatarWithMbti`,避免重复拼接 baseUrl。
diff --git a/.cursor/agent/开发助理/evolution/索引.md b/.cursor/agent/开发助理/evolution/索引.md
index 39773d58..7dfccffb 100644
--- a/.cursor/agent/开发助理/evolution/索引.md
+++ b/.cursor/agent/开发助理/evolution/索引.md
@@ -4,4 +4,5 @@
| 日期 | 摘要 | 文件 |
|------|------|------|
+| 2026-03-21 | MBTI 头像小程序全链路兜底 + 匹配接口回填 | 2026-03-21-MBTI头像C端全链路兜底.md |
| 2026-03-16 | 用户交互习惯分析(基于 agent-transcripts) | 2026-03-16-交互习惯分析.md |
diff --git a/.cursor/docs/feishu_开发群与项目复盘.md b/.cursor/docs/feishu_开发群与项目复盘.md
new file mode 100644
index 00000000..e572a2de
--- /dev/null
+++ b/.cursor/docs/feishu_开发群与项目复盘.md
@@ -0,0 +1,38 @@
+# 飞书「开发群」与 Soul 项目复盘约定
+
+## 绑定关系
+
+- **Soul 创业派对(永平)**、**派对 AI 相关自动化**、**卡若 AI 侧发往本项目的复盘**,默认使用**同一开发群机器人 Webhook**。
+- Webhook 与项目在配置上是**一一绑定**:换群 = 改环境变量或下方脚本中的默认 URL,并确保飞书里该群已添加对应自定义机器人。
+
+## 默认 Webhook(开发群)
+
+环境变量(推荐在本机 shell 或 `scripts/.env.feishu` 同目录的 `.env` 中导出):
+
+| 变量名 | 用途 |
+|--------|------|
+| `FEISHU_DEV_GROUP_WEBHOOK` | **主约定**:开发群统一入口;未设置时各脚本使用内置默认值。 |
+
+当前默认 URL(与飞书群内机器人一致):
+
+`https://open.feishu.cn/open-apis/bot/v2/hook/c558df98-e13a-419f-a3c0-7e428d15f494`
+
+## 已接此 Webhook 的脚本(代码内默认或可读此变量)
+
+| 脚本 | 说明 |
+|------|------|
+| `scripts/send_chapter_poster_to_feishu.py` | 章节摘要 + 海报图(小程序码) |
+| 卡若AI `飞书管理/脚本/send_review_to_feishu_webhook.py` | 卡若 AI 复盘(文本/卡片) |
+| 卡若AI `飞书管理/脚本/soul_party_to_feishu_sheet.py` | 派对运营表同步后的群推送 |
+
+**复盘发哪里**:与 Soul 开发相关的**日终/迭代复盘** → 发 **`FEISHU_DEV_GROUP_WEBHOOK` 对应群**。
+彩民/运营另群如需保留,可通过各脚本 `--webhook` 或单独环境变量覆盖。
+
+## 界面截图发群说明
+
+- 飞书自定义机器人发图需先走**应用上传**得到 `image_key`(见 `send_chapter_poster_to_feishu.py` 内逻辑)。
+- 无现成截图时:在复盘文本中附 **管理端 / 小程序 / API 文档** 等**可点击链接**,与海报一并发出。
+
+## 与「SKR / 开发群」口头约定
+
+- 群内链接、机器人由**项目侧**维护;**派对 AI** 与 **卡若 AI** 推送配置统一指向本开发群,避免复盘散落多个群。
diff --git a/.cursor/skills/karuo-party/SKILL.md b/.cursor/skills/karuo-party/SKILL.md
index acf194c6..b0eabfe1 100644
--- a/.cursor/skills/karuo-party/SKILL.md
+++ b/.cursor/skills/karuo-party/SKILL.md
@@ -7,8 +7,8 @@ description: >
triggers: 运营报表、视频切片、多平台分发、飞书视频下载、派对运营、卡若创业派对、派对填表、视频剪辑、一键分发、妙记下载
owner: 水岸
group: 运营
-version: "1.1"
-updated: "2026-03-21"
+version: "1.3"
+updated: "2026-03-24"
---
# 卡若创业派对运营 Skill 包
@@ -29,7 +29,7 @@ updated: "2026-03-21"
## 一、技能包组成
-本技能包包含以下 4 个核心子技能:
+本技能包包含以下 8 个核心子技能:
| # | 技能名 | 文件路径 | 触发词 | 用途 |
|:--|:---|:---|:---|:---|
@@ -37,6 +37,10 @@ updated: "2026-03-21"
| ② | 飞书视频文字下载 | `skills/飞书视频文字下载_SKILL.md` | 妙记下载、飞书视频、飞书妙记 | 文字+视频→本地 |
| ③ | 视频切片 | `skills/视频切片_SKILL.md` | 视频剪辑、切片发布 | 原视频→转录→高光→成片 |
| ④ | 多平台分发 | `skills/多平台分发_SKILL.md` | 一键分发、全平台发布 | 成片→抖音/B站/视频号/小红书/快手 |
+| ⑤ | 视频号发布 | `skills/平台_视频号_SKILL.md` | 视频号发布、视频号重传 | 账号校验→清理→定时发布 |
+| ⑥ | B站发布 | `skills/平台_B站_SKILL.md` | B站发布、B站补发 | API优先→兜底→定时 |
+| ⑦ | 小红书发布 | `skills/平台_小红书_SKILL.md` | 小红书发布、小红书补发 | UI自动化定时发布 |
+| ⑧ | 抖音发布 | `skills/平台_抖音_SKILL.md` | 抖音发布、抖音补发 | API定时发布+失败重登 |
---
@@ -112,15 +116,32 @@ python3 "$VIDEO_SCRIPT/soul_slice_pipeline.py" --video "<原视频.mp4>" --clips
```bash
DIST_SCRIPT="/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本"
-# 定时排期:第1条立即,后续 30-120min 随机间隔
+# 默认智能错峰 + 静默(不自动弹窗扫视频号)
python3 "$DIST_SCRIPT/distribute_all.py" --video-dir "<成片目录>"
+# 旧版随机间隔:加 --legacy-schedule;需要自动扫视频号:加 --auto-channels-login
+
# 立即全部发布
python3 "$DIST_SCRIPT/distribute_all.py" --now
```
**详细流程**:见 `skills/多平台分发_SKILL.md`
+#### 视频号发布前置(强制)
+
+在执行视频号发布前,固定做以下 3 步:
+
+1. **账号信息校验**:调用 `auth_data` 校验 `nickname` 与 `headImgUrl`,不一致先改到目标值再发。
+2. **线上失败/重复清理**:先查 `post_list`,删除失败条目;同标题仅保留最新一条(去重后再补发)。
+3. **仅定时发布**:禁止立即发布;若页面定时控件失效,使用 `post_create` 注入定时参数并拦截立即发布。
+
+#### 分发统一总规则(强制)
+
+1. **间隔规则**:按“每一条相邻发布时间”计算,必须在 **10 分钟到 120 分钟** 之间。
+2. **账号状态规则**:发布前做账号可用性检查;若出现封禁/登录失效/鉴权失败,先执行重登,再重试失败条。
+3. **平台分治规则**:先按平台触发对应子 Skill,再执行发布命令,不混用平台规则。
+4. **收敛规则**:每轮结束输出成功/失败清单 + 重登命令;失败条必须可继续重试直到收敛。
+
---
## 四、完整流程(派对结束后)
@@ -140,7 +161,7 @@ python3 "$DIST_SCRIPT/distribute_all.py" --now
### Phase 3:视频生产
1. **视频切片**:转录 → 高光识别 → 批量切片 → 增强
-2. **多平台分发**:成片 → 5平台发布(定时排期)
+2. **多平台分发**:成片 → 5 平台发布(默认智能错峰定时)
### Phase 4:文章内容
@@ -167,7 +188,7 @@ python3 auto_log.py
各平台 Cookie 文件位于 `credentials/cookies/` 目录。更新方式:
-1. **视频号**:浏览器登录后,使用 `cookie_manager.py` 提取
+1. **视频号**:`channels_login.py`(Cursor Simple Browser + 可选 CDP);详见 `skills/多平台分发_SKILL.md`
2. **B站**:使用 `bilibili-api-python` 自动获取
3. **小红书/快手**:Playwright 自动化登录后提取
@@ -277,5 +298,7 @@ curl -sS -X POST -H "Content-Type: application/json" -d "$TEXT" "$FEISHU_PARTY_C
| 版本 | 日期 | 说明 |
|:---|:---|:---|
+| 1.3 | 2026-03-24 | 升级为“总规则+平台子Skill”;统一相邻间隔 10~120 分钟;新增账号封禁/重登/重试收敛规则 |
+| 1.2 | 2026-03-23 | 新增视频号发布前置三步:头像昵称校验、失败/重复清理、强制定时发布(含请求注入兜底) |
| 1.1 | 2026-03-21 | 新增 §九 闭环复盘发群:卡若五块复盘 + 飞书 Webhook v2(msg_type 必填) |
| 1.0 | 2026-03-20 | 初版:整合运营报表、视频切片、多平台分发、飞书视频文字下载 4 大技能,统一凭证管理 |
diff --git a/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md b/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
index e0c023df..ef34db9a 100644
--- a/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
+++ b/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
@@ -3,18 +3,25 @@ name: 多平台分发
description: >
一键将视频分发到 5 个平台(抖音、B站、视频号、小红书、快手)。
API 优先策略:视频号纯 API、B站 bilibili-api-python、抖音纯 API。
- 支持定时排期(第1条立即发,后续 30-120 分钟随机间隔)、并行分发、去重、失败自动重试。
+ 支持定时排期(默认智能错峰;可选 legacy)、默认静默不弹窗登录、并行分发、去重、失败自动重试。
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
owner: 木叶
group: 木
-version: "4.0"
-updated: "2026-03-11"
+version: "4.5"
+updated: "2026-03-24"
---
-# 多平台分发 Skill(v4.0)
+# 多平台分发 Skill(v4.5)
> **核心原则**:API 发布为主,Playwright 为辅。确保确定性地分发到各平台。
-> **v4.0 变更**:视频号已切换为纯 API、统一元数据生成器、定时排期优化、简介/标签/分区自动填充。
+> **v4.5**:统一改为“按相邻条目”间隔控制,默认区间 **10~120 分钟**;并要求按平台子 Skill 执行对应规则后再发布。
+
+## 〇、执行原则(第一性原理)
+
+- **视频号两步**:先扫码落盘 Cookie 再上传;`video_channels_resume.py` **默认弹 Chromium 窗**扫码,扫完再继续传;`--silent-login` 无头。
+- **目标优先**:全平台分发 → 直接 `distribute_all.py`;视频号助手态需**事先**手动 `channels_login.py` 或显式 `--auto-channels-login`。
+- **Cookie 优先**:登录成功必须落盘;视频号双路径同步见 `cookie_manager.sync_channels_cookie_files`。
+- **默认静默**:无人值守跑命令时不弹浏览器。
---
@@ -32,6 +39,8 @@ updated: "2026-03-11"
> 按《视频号与腾讯相关 API 整理》结论,微信官方目前**没有开放「短视频上传/发布」接口**;本 Skill 中的视频号发布能力,属于对 `https://channels.weixin.qq.com` 视频号助手网页协议的逆向封装(DFS 上传 + `post_create`),仅在你本机使用,需自行承担协议变更与合规风险。
> 官方可控能力(直播记录、橱窗、留资、罗盘数据、本地生活等)的服务端 API 入口为:`https://developers.weixin.qq.com/doc/channels/api/`,如需做直播/橱窗/留资集成,可基于该文档在单独 Skill 中扩展。
+> **「视频号 API token」与成片上传**:公众号 **`access_token`** **不能**替代视频号助手网页态;`channels_api_publish` 依赖 **`channels_storage_state.json`**。127 场静默全平台:`python3 distribute_all.py --video-dir "/Users/karuo/Movies/soul视频/第127场_20260318_output/成片"`(须各平台 Cookie 已就绪)。
+
---
## 二、一键命令
@@ -39,10 +48,13 @@ updated: "2026-03-11"
```bash
cd /Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本
-# 定时排期:第1条立即,后续 30-120min 随机间隔
+# 默认智能错峰排期
python3 distribute_all.py
-# 立即全部发布
+# 规则间隔(推荐,按相邻条目 10~120 分钟)
+python3 distribute_all.py --legacy-schedule --min-gap 10 --max-gap 120
+
+# 立即全部发布(仅在明确要求时)
python3 distribute_all.py --now
# 只发指定平台
@@ -54,24 +66,34 @@ python3 distribute_all.py --video-dir "/path/to/videos/"
# 检查 Cookie / 重试失败
python3 distribute_all.py --check
python3 distribute_all.py --retry
+
+# 需要脚本自动弹窗扫视频号(默认不弹)
+python3 distribute_all.py --platforms 视频号 --auto-channels-login --video-dir "/path/to/成片"
+# 独立 channels_api_publish 允许自动登录:CHANNELS_AUTO_LOGIN=1
+# 强制永不自动登录:NO_AUTO_CHANNELS_LOGIN=1
+
+# 平台站点上传 CLI(平台级,含账号检测+自动重登+重试)
+python3 site_upload_cli.py check --platforms 抖音 B站 小红书 快手
+python3 site_upload_cli.py publish --platforms 抖音 B站 小红书 --video-dir "/path/to/成片" --min-gap 10 --max-gap 120 --until-success
```
---
-## 三、定时排期(v4.0 优化)
+## 三、定时排期(v4.2+)
-### 3.1 排期规则
-- **第 1 条**:立即发布(`first_delay=0`)
-- **第 2 条起**:前一条 + random(30, 120) 分钟
-- 若总跨度 > 24h,自动按比例压缩
-- 12 条视频典型跨度 ~10-14h
+### 3.1 默认(`generate_smart_schedule`)
+- 第 1 条立即;间隔与总跨度随条数自适应;本地 0–7 点尽量挪到午间(`SCHEDULE_NO_NIGHT_REFINE=1` 关闭)
+- `--legacy-schedule` + `--min-gap` / `--max-gap` / `--max-hours` 为固定区间逻辑(建议 `10~120`)
+- 去重时排期与目录列表下标对齐
-### 3.2 各平台定时实现
+### 3.2 独立 `channels_api_publish.py`:同上智能排期转 Unix
+
+### 3.3 各平台定时实现
| 平台 | 定时方式 | 参数 |
|------|----------|------|
| B站 | API `meta.dtime` | Unix 时间戳(秒) |
-| 视频号 | API 暂不支持原生定时 | 描述中标注时间/手动设置 |
+| 视频号 | API `postTimingInfo.postTime`(秒级 Unix);过近时间自动顺延,不允许立即发 | `channels_api_publish._scheduled_ts_for_channels` |
| 抖音 | API `timing_ts` | Unix 时间戳 |
| 快手 | Playwright UI | `schedule_helper.py` |
| 小红书 | Playwright UI | `schedule_helper.py` |
@@ -91,12 +113,12 @@ meta.description("B站") # 标题 + 标签 + 品牌标记
meta.tags_str("B站") # AI工具,效率提升,Soul派对,...
meta.bilibili_meta() # B站投稿完整 meta(含 tid/tag/desc)
meta.title_short() # 小红书短标题(≤20字)
-meta.hashtags("视频号") # #AI工具 #效率提升 ... #小程序 卡若创业派对
+meta.hashtags("视频号") # … + #小程序卡若创业派对 #公众号卡若-4点起床的男人
```
### 4.1 内容结构
- **标题**:手工优化标题库优先,否则从文件名智能提取
-- **简介**:标题 + 换行 + 话题标签 + `#小程序 卡若创业派对`
+- **简介**:标题 + 换行 + 话题标签;**视频号**固定追加 `#小程序卡若创业派对` `#公众号卡若-4点起床的男人`(其它平台仍为 `#小程序 卡若创业派对`)
- **标签**:基于关键词匹配(AI/创业/副业/Soul 等 12 类)+ 通用标签
- **分区**:B站 tid=160(生活>日常)
- **风控过滤**:`content_filter.py` 自动替换敏感词(70+ 映射,严格/宽松分级)
@@ -122,8 +144,42 @@ meta.hashtags("视频号") # #AI工具 #效率提升 ... #小程序 卡若创
`cookie_manager.py` 统一管理:
- 中央存储:`多平台分发/cookies/{平台}_cookies.json`
- 自动迁移:旧路径 → 中央存储(首次使用时)
-- API 预检:5 平台各自 auth API 校验有效性
-- 防重复登录:有效 Cookie 不触发重新获取
+- **视频号双路径**:预检读中央、发布读 legacy;`sync_channels_cookie_files()` 按 **mtime 新者覆盖旧者**,避免两份不一致
+- **登录后必存**:`channels_login.py` 保存 `channels_storage_state.json` 后 **立即 copy** 到 `cookies/视频号_cookies.json`
+- **登录页只在 Cursor 内**:`channels_login.py` v7 用 `cursor://vscode.simple-browser/show?url=…` 打开 **Simple Browser**,不用系统默认浏览器;**无额外 Chromium** 时需 Cursor 带 `--remote-debugging-port=9223`(或 `CHANNELS_CDP_URL`),脚本 CDP 附着后导出会话;否则 **自动回退** 本机 Chromium 仅用于写 Cookie(`--playwright-only` 强制只走 Chromium)。
+- **视频号登录**:默认不自动执行 `channels_login.py`;需要时加 `--auto-channels-login`(或事先手动登录)
+- API 预检:各平台 auth API 校验有效性
+
+---
+
+## 六点五、视频号发布前置检查(强制)
+
+每次发布视频号前,必须先跑:
+
+1. `auth/auth_data`:校验 `nickname` 与 `headImgUrl`(不一致先改号资料,再执行发布)。
+2. `post/post_list`:筛查失败条目并删除。
+3. 同标题去重:若存在多条,仅保留最新 `objectId`,其余调用 `post/post_delete` 删除。
+4. 发布阶段若页面定时控件失败,改为 `post_create` 请求注入 `postTimingInfo`,继续定时发布;注入也失败则中止该条(防止误发立即)。
+
+---
+
+## 六点六、账号异常自动处理(强制)
+
+1. 发布前先执行 `--check`;若平台 Cookie 无效,先走对应登录脚本。
+2. 发布中若出现封禁/风控/鉴权失败,立即输出处理建议与重登命令。
+3. 重登完成后优先执行 `--retry`,只重试失败条,不重复成功条。
+4. 失败超过 2 轮仍未收敛时,输出平台级阻塞原因并停止盲目重试。
+
+---
+
+## 六点七、平台子Skill分流(强制)
+
+- 视频号:按 `平台_视频号_SKILL.md` 执行。
+- B站:按 `平台_B站_SKILL.md` 执行。
+- 小红书:按 `平台_小红书_SKILL.md` 执行。
+- 抖音:按 `平台_抖音_SKILL.md` 执行。
+
+多平台联发时,先加载各平台子Skill,再统一调度 `distribute_all.py`。
---
diff --git a/.cursor/skills/karuo-party/skills/平台_B站_SKILL.md b/.cursor/skills/karuo-party/skills/平台_B站_SKILL.md
new file mode 100644
index 00000000..1b377cfe
--- /dev/null
+++ b/.cursor/skills/karuo-party/skills/平台_B站_SKILL.md
@@ -0,0 +1,34 @@
+---
+name: 平台_B站发布
+description: B站发布专用规则。API优先,失败降级 Playwright,按相邻间隔 10~120 分钟定时发布。
+triggers: B站发布、B站补发、B站重试
+owner: 木叶
+group: 木
+version: "1.0"
+updated: "2026-03-24"
+---
+
+# 平台子Skill:B站发布
+
+## 一、强制规则
+
+1. 默认 API 投稿,失败才降级 Playwright。
+2. 发布间隔按相邻条目控制在 `10~120` 分钟。
+3. 出现 406/超时等异常时先重试失败条,不重复成功条。
+
+## 二、标准命令
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_all.py" \
+ --platforms B站 \
+ --video-dir "<成片目录>" \
+ --legacy-schedule --min-gap 10 --max-gap 120
+```
+
+## 三、登录异常处理
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/B站发布/脚本/bilibili_login.py"
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_all.py" --retry
+```
+
diff --git a/.cursor/skills/karuo-party/skills/平台_小红书_SKILL.md b/.cursor/skills/karuo-party/skills/平台_小红书_SKILL.md
new file mode 100644
index 00000000..88ba07b4
--- /dev/null
+++ b/.cursor/skills/karuo-party/skills/平台_小红书_SKILL.md
@@ -0,0 +1,34 @@
+---
+name: 平台_小红书发布
+description: 小红书发布专用规则。UI自动化定时发布,按相邻间隔 10~120 分钟执行。
+triggers: 小红书发布、小红书补发
+owner: 木叶
+group: 木
+version: "1.0"
+updated: "2026-03-24"
+---
+
+# 平台子Skill:小红书发布
+
+## 一、强制规则
+
+1. 逐条定时发布,禁止批量立即发布。
+2. 相邻发布时间间隔必须在 `10~120` 分钟。
+3. 若仅返回 likely_published,需在下一轮巡检确认状态。
+
+## 二、标准命令
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_all.py" \
+ --platforms 小红书 \
+ --video-dir "<成片目录>" \
+ --legacy-schedule --min-gap 10 --max-gap 120
+```
+
+## 三、登录异常处理
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/小红书发布/脚本/xiaohongshu_login.py"
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_all.py" --retry
+```
+
diff --git a/.cursor/skills/karuo-party/skills/平台_抖音_SKILL.md b/.cursor/skills/karuo-party/skills/平台_抖音_SKILL.md
new file mode 100644
index 00000000..316b102a
--- /dev/null
+++ b/.cursor/skills/karuo-party/skills/平台_抖音_SKILL.md
@@ -0,0 +1,34 @@
+---
+name: 平台_抖音发布
+description: 抖音发布专用规则。API定时发布,按相邻间隔 10~120 分钟;遇风控/封禁优先账号处理。
+triggers: 抖音发布、抖音补发、抖音重试
+owner: 木叶
+group: 木
+version: "1.0"
+updated: "2026-03-24"
+---
+
+# 平台子Skill:抖音发布
+
+## 一、强制规则
+
+1. 仅按定时发布,不做整批立即。
+2. 相邻发布时间间隔 `10~120` 分钟。
+3. 若出现封禁/风控提示,立刻停止盲目重试并提示账号处理。
+
+## 二、标准命令
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_all.py" \
+ --platforms 抖音 \
+ --video-dir "<成片目录>" \
+ --legacy-schedule --min-gap 10 --max-gap 120
+```
+
+## 三、登录异常处理
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/抖音发布/脚本/douyin_login.py"
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/多平台分发/脚本/distribute_all.py" --retry
+```
+
diff --git a/.cursor/skills/karuo-party/skills/平台_视频号_SKILL.md b/.cursor/skills/karuo-party/skills/平台_视频号_SKILL.md
new file mode 100644
index 00000000..520b5b4f
--- /dev/null
+++ b/.cursor/skills/karuo-party/skills/平台_视频号_SKILL.md
@@ -0,0 +1,38 @@
+---
+name: 平台_视频号发布
+description: 视频号发布专用规则。发布前校验账号,清理密集/失败条目,按相邻间隔 10~120 分钟定时发布。
+triggers: 视频号发布、视频号重传、视频号补发
+owner: 木叶
+group: 木
+version: "1.0"
+updated: "2026-03-24"
+---
+
+# 平台子Skill:视频号发布
+
+## 一、强制规则
+
+1. 发布前必须校验 `auth_data`(昵称、头像、登录态)。
+2. 发布前必须检查 `post_list`:失败条目删除、同标题去重。
+3. 仅允许定时发布,按相邻条目间隔 `10~120` 分钟。
+4. 定时控件失败时允许请求注入;注入失败则中止该条,禁止误发立即。
+
+## 二、标准命令
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_web_cli.py" \
+ publish-dir \
+ --video-dir "<成片目录>" \
+ --legacy-schedule --min-gap 10 --max-gap 120 \
+ --start-after-min 10
+```
+
+## 三、登录异常处理
+
+- 若出现 `300334` / `300002` / `finder_raw` 缺失:立即执行重登。
+- 重登命令:
+
+```bash
+python3 "/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频号发布/脚本/channels_login.py" --playwright-only
+```
+
diff --git a/.gitignore b/.gitignore
index 63783f2e..6d17c5ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,6 @@ soul-api/soul-api-new
# Cursor 索引减负:db-exec 依赖(仓库根已有 node_modules/ 规则,此处显式强调子路径)
.cursor/scripts/db-exec/node_modules/
+
+# 本地技能包临时打包目录
+.tmp_skill_bundle/
diff --git a/miniprogram/app.js b/miniprogram/app.js
index 3098061c..2c1f4a66 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -9,10 +9,10 @@ const { checkAndExecute } = require('./utils/ruleEngine.js')
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
+const PRODUCTION_BASE_URL = 'https://soulapi.quwanzhi.com'
+const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version(正式版),否则用本字段
-const APP_DISPLAY_VERSION = '1.7.1'
-// 章节总数:API 获取失败时的统一兜底,避免 90/62 混用
-const FALLBACK_TOTAL_SECTIONS = 62
+const APP_DISPLAY_VERSION = '1.7.2'
App({
globalData: {
@@ -42,7 +42,7 @@ App({
// 书籍数据(bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters)
bookData: null,
- totalSections: FALLBACK_TOTAL_SECTIONS, // 来自 book/parts 或 book/stats,失败时用常量
+ totalSections: 90,
// 购买记录
purchasedSections: [],
@@ -98,32 +98,34 @@ App({
lastVipContactCheck: 0,
// 头像昵称检测:上次检测时间戳(与 VIP 检测同周期刷新)
lastAvatarNicknameCheck: 0,
- // 登录过期:401 后用户点「去登录」时设为 true,我的页 onShow 会检测并自动弹登录
- pendingLoginAfterExpire: false,
+ /** MBTI → 默认头像 URL(/api/miniprogram/config/mbti-avatars),供推广海报等 */
+ mbtiAvatarsMap: {},
+ mbtiAvatarsExpires: 0,
},
/** 正式版强制生产 API,避免误传 localhost 导致审核/线上全挂 */
initApiBaseUrl() {
- const PRODUCTION = 'https://soulapi.quwanzhi.com'
const KEY = 'apiBaseUrl'
try {
const info = wx.getAccountInfoSync?.()
const env = info?.miniProgram?.envVersion || 'release'
if (env === 'release') {
- this.globalData.baseUrl = PRODUCTION
+ this.globalData.baseUrl = PRODUCTION_BASE_URL
try {
const saved = wx.getStorageSync(KEY)
- if (saved && saved !== PRODUCTION) wx.removeStorageSync(KEY)
+ if (saved && saved !== PRODUCTION_BASE_URL) wx.removeStorageSync(KEY)
} catch (_) {}
return
}
const saved = wx.getStorageSync(KEY)
if (saved && typeof saved === 'string' && /^https?:\/\//.test(saved)) {
this.globalData.baseUrl = String(saved).replace(/\/$/, '')
+ } else {
+ this.globalData.baseUrl = PRODUCTION_BASE_URL
}
} catch (_) {
- this.globalData.baseUrl = PRODUCTION
+ this.globalData.baseUrl = PRODUCTION_BASE_URL
}
},
@@ -397,8 +399,8 @@ App({
if (!this.globalData.isSinglePageMode) return true
wx.showModal({
- title: '请前往完整小程序',
- content: '当前为朋友圈单页,仅支持部分浏览。如需登录和解锁内容,请点击底部「前往小程序」后再操作。',
+ title: '请打开完整小程序',
+ content: '当前是朋友圈预览,无法在这里登录或付款。请先点击屏幕底部「前往小程序」,进入完整版后再解锁本章。',
showCancel: false,
confirmText: '我知道了',
})
@@ -414,7 +416,7 @@ App({
},
/**
- * 头像/昵称未改则引导:老用户弹窗后跳 avatar-nickname;新用户由登录处强制 redirectTo
+ * 头像/昵称未改:老用户弹窗后跳 avatar-nickname;新用户由登录处强制 redirectTo
* VIP 用户不在此处理,统一走 checkVipContactRequiredAndGuide 只跳 profile-edit,避免乱跳
*/
checkAvatarNicknameAndGuide() {
@@ -433,10 +435,10 @@ App({
if (lastDate === today) return
wx.setStorageSync('lastAvatarGuideDate', today)
wx.showModal({
- title: '完善个人资料',
- content: '请设置头像和昵称,让其他创业者更好地认识你',
- confirmText: '去完善',
- cancelText: '稍后',
+ title: '设置头像与昵称',
+ content: '头像与昵称会出现在名片与匹配卡片上,方便伙伴认出你。',
+ confirmText: '去设置',
+ cancelText: '关闭',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
}
@@ -594,7 +596,7 @@ App({
/**
* VIP 用户登录后检测:手机号/头像昵称等需完善时,统一只跳 profile-edit,避免与 avatar-nickname 乱跳。
- * 旧数据(VIP 但头像昵称未改):弹窗「为了更好服务,请完善资料」→ redirectTo profile-edit
+ * 旧数据(VIP 但头像昵称未改):说明原因后 redirectTo profile-edit
*/
async checkVipContactRequiredAndGuide() {
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
@@ -623,12 +625,12 @@ App({
const wechatId = (profileData.wechatId || profileData.wechat_id || this.globalData.userInfo?.wechatId || this.globalData.userInfo?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
const needsAvatarNickname = this._needsAvatarNickname(profileData)
- // VIP 头像/昵称未改(含旧数据):统一只跳 profile-edit,弹窗「为了更好服务,请完善资料」
+ // VIP 头像/昵称未改(含旧数据):统一只跳 profile-edit
if (needsAvatarNickname) {
wx.showModal({
- title: '完善资料',
- content: '为了更好为您服务,请完善资料',
- confirmText: '去完善',
+ title: '补全对外展示信息',
+ content: 'VIP 名片与派对场景会展示头像与昵称,补全后对方更容易认出你。',
+ confirmText: '去填写',
showCancel: false,
success: () => {
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
@@ -641,9 +643,9 @@ App({
// VIP 无手机号:弹窗说明后跳转
if (!phone) {
wx.showModal({
- title: '完善资料',
- content: 'VIP会员需完善手机号,以便使用找伙伴、提现等功能',
- confirmText: '去完善',
+ title: '补全手机号',
+ content: '手机号用于找伙伴、提现验证与重要通知,仅本人可见。',
+ confirmText: '去填写',
showCancel: false,
success: () => {
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
@@ -651,12 +653,12 @@ App({
})
return
}
- // 有手机号但缺微信号:弹窗引导(非强制)
+ // 有手机号但缺微信号:可选补全(非强制)
wx.showModal({
- title: '完善联系方式',
- content: '请到资料页完善微信号,便于他人联系您',
- confirmText: '去完善',
- cancelText: '稍后',
+ title: '补全微信号(可选)',
+ content: '填写微信号后,对方在允许的场景下能更快加到你;不填也可继续使用。',
+ confirmText: '去填写',
+ cancelText: '关闭',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
@@ -695,6 +697,17 @@ App({
if (!forceRefresh && this.globalData.configCache && now < this.globalData.configCacheExpires) {
return this.globalData.configCache
}
+ if (!forceRefresh && !this.globalData.configCache) {
+ try {
+ const local = wx.getStorageSync(CONFIG_CACHE_KEY)
+ const exp = Number(local && local.expiresAt)
+ if (local && local.data && exp > now) {
+ this.globalData.configCache = local.data
+ this.globalData.configCacheExpires = exp
+ return local.data
+ }
+ } catch (_) {}
+ }
try {
const [coreRes, auditRes] = await Promise.all([
this.request({ url: '/api/miniprogram/config/core', silent: true, timeout: 5000 }),
@@ -713,11 +726,45 @@ App({
}
this.globalData.configCache = res
this.globalData.configCacheExpires = now + CACHE_TTL
+ try {
+ wx.setStorageSync(CONFIG_CACHE_KEY, {
+ data: res,
+ expiresAt: this.globalData.configCacheExpires
+ })
+ } catch (_) {}
return res
}
- } catch (e) {
- if (this.globalData.configCache) return this.globalData.configCache
- }
+ } catch (_) {}
+
+ try {
+ const legacy = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 })
+ if (legacy) {
+ const cfg = (legacy.configs && legacy.configs.mp_config) || legacy.mpConfig || {}
+ const res = {
+ success: legacy.success !== false,
+ prices: legacy.prices || (legacy.configs && legacy.configs.chapter_config && legacy.configs.chapter_config.prices) || {},
+ features: legacy.features || (legacy.configs && legacy.configs.feature_config) || {},
+ userDiscount: legacy.userDiscount,
+ mpConfig: cfg,
+ configs: legacy.configs || {}
+ }
+ this.globalData.configCache = res
+ this.globalData.configCacheExpires = now + CACHE_TTL
+ try {
+ wx.setStorageSync(CONFIG_CACHE_KEY, {
+ data: res,
+ expiresAt: this.globalData.configCacheExpires
+ })
+ } catch (_) {}
+ return res
+ }
+ } catch (_) {}
+
+ if (this.globalData.configCache) return this.globalData.configCache
+ try {
+ const local = wx.getStorageSync(CONFIG_CACHE_KEY)
+ if (local && local.data) return local.data
+ } catch (_) {}
return null
},
@@ -780,27 +827,58 @@ App({
async loadMpConfig() {
try {
const res = await this.getConfig()
- if (!res) return
- const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
- if (mp && typeof mp === 'object') {
- if (mp.appId) this.globalData.appId = mp.appId
- if (mp.mchId) this.globalData.mchId = mp.mchId
- if (mp.withdrawSubscribeTmplId) this.globalData.withdrawSubscribeTmplId = mp.withdrawSubscribeTmplId
- this.globalData.auditMode = !!mp.auditMode
- this.globalData.supportWechat = mp.supportWechat || mp.customerWechat || mp.serviceWechat || ''
- // 通知当前已加载的页面刷新 auditMode(从后台切回时配置更新后立即生效)
- try {
- const pages = getCurrentPages()
- pages.forEach(p => {
- if (p && p.data && 'auditMode' in p.data) {
- p.setData({ auditMode: this.globalData.auditMode || false })
- }
- })
- } catch (_) {}
+ if (res) {
+ const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
+ if (mp && typeof mp === 'object') {
+ if (mp.appId) this.globalData.appId = mp.appId
+ if (mp.mchId) this.globalData.mchId = mp.mchId
+ if (mp.withdrawSubscribeTmplId) this.globalData.withdrawSubscribeTmplId = mp.withdrawSubscribeTmplId
+ this.globalData.auditMode = !!mp.auditMode
+ this.globalData.supportWechat = mp.supportWechat || mp.customerWechat || mp.serviceWechat || ''
+ }
}
} catch (e) {
console.warn('[App] loadMpConfig 失败,使用默认值:', e?.message || e)
}
+ // 审核模式不走 5min 本地 config 缓存:始终以独立接口为准,避免后台已开审核端仍显示支付入口
+ try {
+ await this.getAuditMode()
+ } catch (_) {}
+ this.loadMbtiAvatarsMap()
+ },
+
+ /** 拉取后台配置的 16 型 MBTI 默认头像(公开接口,约 5 分钟本地缓存) */
+ async loadMbtiAvatarsMap() {
+ try {
+ const now = Date.now()
+ if (this.globalData.mbtiAvatarsExpires && this.globalData.mbtiAvatarsExpires > now) return
+ const res = await this.request({
+ url: '/api/miniprogram/config/mbti-avatars',
+ silent: true,
+ timeout: 8000,
+ })
+ if (res && res.success && res.avatars && typeof res.avatars === 'object') {
+ this.globalData.mbtiAvatarsMap = res.avatars
+ this.globalData.mbtiAvatarsExpires = now + 5 * 60 * 1000
+ }
+ } catch (e) {
+ console.warn('[App] loadMbtiAvatarsMap:', e?.message || e)
+ }
+ },
+
+ /** 展示用头像:优先用户头像,否则 MBTI 映射(需已 loadMbtiAvatarsMap) */
+ resolveAvatarWithMbti(avatar, mbti) {
+ try {
+ const { resolveAvatarWithMbti } = require('./utils/mbtiAvatar.js')
+ return resolveAvatarWithMbti(
+ avatar,
+ mbti,
+ this.globalData.mbtiAvatarsMap || {},
+ this.globalData.baseUrl || ''
+ )
+ } catch (_) {
+ return (avatar && String(avatar).trim()) || ''
+ }
},
/**
@@ -855,6 +933,79 @@ App({
return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
},
+ _shouldFallbackToProduction(err) {
+ const msg = String((err && err.errMsg) || (err && err.message) || '').toLowerCase()
+ return (
+ msg.includes('connection_failed') ||
+ msg.includes('err_proxy_connection_failed') ||
+ msg.includes('dns') ||
+ msg.includes('name not resolved') ||
+ msg.includes('failed to fetch') ||
+ msg.includes('econnrefused') ||
+ msg.includes('network') ||
+ msg.includes('timeout')
+ )
+ },
+
+ _switchBaseUrlToProduction() {
+ this.globalData.baseUrl = PRODUCTION_BASE_URL
+ try {
+ wx.setStorageSync('apiBaseUrl', PRODUCTION_BASE_URL)
+ } catch (_) {}
+ },
+
+ _requestOnce(url, options = {}, silent = false) {
+ const showError = (msg) => {
+ if (!silent && msg) wx.showToast({ title: msg, icon: 'none', duration: 2500 })
+ }
+ return new Promise((resolve, reject) => {
+ const token = wx.getStorageSync('token')
+ wx.request({
+ url: this.globalData.baseUrl + url,
+ method: options.method || 'GET',
+ data: options.data || {},
+ timeout: options.timeout || 15000,
+ header: {
+ 'Content-Type': 'application/json',
+ 'Authorization': token ? `Bearer ${token}` : '',
+ ...options.header
+ },
+ success: (res) => {
+ const data = res.data
+ if (res.statusCode === 200) {
+ if (data && data.success === false) {
+ const msg = this._getApiErrorMsg(data, '操作失败')
+ if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
+ this.logout()
+ }
+ showError(msg)
+ reject(new Error(msg))
+ return
+ }
+ resolve(data)
+ return
+ }
+ if (res.statusCode === 401) {
+ this.logout()
+ showError('未授权,请重新登录')
+ reject(new Error('未授权'))
+ return
+ }
+ const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
+ showError(msg)
+ reject(new Error(msg))
+ },
+ fail: (err) => {
+ const msg = (err && err.errMsg)
+ ? (String(err.errMsg).indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试')
+ : '网络异常,请重试'
+ showError(msg)
+ reject(err || new Error(msg))
+ }
+ })
+ })
+ },
+
/**
* 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
* GET 请求 200ms 内相同 url 去重,避免并发重复请求。
@@ -874,98 +1025,21 @@ App({
}
const method = (options.method || 'GET').toUpperCase()
const silent = !!options.silent
- const showError = (msg) => {
- if (!silent && msg) {
- wx.showToast({ title: msg, icon: 'none', duration: 2500 })
- }
- }
- // GET 短时去重:相同 url 的并发请求共享同一 promise
if (method === 'GET') {
const dedupKey = url + (options.data ? JSON.stringify(options.data) : '')
const pending = this._requestPending || (this._requestPending = {})
- if (pending[dedupKey]) {
- return pending[dedupKey].promise
- }
+ if (pending[dedupKey]) return pending[dedupKey].promise
}
- const promise = new Promise((resolve, reject) => {
- const token = wx.getStorageSync('token')
- wx.request({
- url: this.globalData.baseUrl + url,
- method: options.method || 'GET',
- data: options.data || {},
- timeout: options.timeout || 15000,
- header: {
- 'Content-Type': 'application/json',
- 'Authorization': token ? `Bearer ${token}` : '',
- ...options.header
- },
- success: (res) => {
- const data = res.data
- if (res.statusCode === 200) {
- // 业务失败:success === false,soul-api 用 message 或 error 返回原因
- if (data && data.success === false) {
- const msg = this._getApiErrorMsg(data, '操作失败')
- // 登录态不一致:本地有 token/userInfo,但后端查不到该用户
- // 典型原因:切换环境(baseUrl)、换库/清库、用户被删除、token 与用户不匹配
- if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
- this.logout()
- }
- 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)
- return
- }
- if (res.statusCode === 401) {
- this.logout()
- if (!silent) {
- wx.showModal({
- title: '登录已过期',
- content: '请重新登录后继续使用',
- confirmText: '去登录',
- cancelText: '稍后',
- success: (modalRes) => {
- if (modalRes.confirm) {
- const pages = getCurrentPages()
- const cur = pages[pages.length - 1]
- const route = (cur && cur.route) || ''
- if (route === 'pages/my/my' && typeof cur.showLogin === 'function') {
- cur.showLogin()
- } else {
- this.globalData.pendingLoginAfterExpire = true
- wx.switchTab({ url: '/pages/my/my' })
- }
- }
- }
- })
- } else {
- showError('未授权,请重新登录')
- }
- reject(new Error('未授权'))
- return
- }
- // 4xx/5xx:优先用返回体的 message/error
- const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
- 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 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
- showError(msg)
- reject(new Error(msg))
- }
- })
+ const promise = this._requestOnce(url, options, silent).catch(async (err) => {
+ const currentBase = String(this.globalData.baseUrl || '').replace(/\/$/, '')
+ if (currentBase !== PRODUCTION_BASE_URL && this._shouldFallbackToProduction(err)) {
+ this._switchBaseUrlToProduction()
+ return this._requestOnce(url, options, silent)
+ }
+ const msg = (err && err.message) ? err.message : '网络异常,请重试'
+ throw new Error(msg)
})
if (method === 'GET') {
@@ -1134,11 +1208,6 @@ 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 || []
@@ -1169,7 +1238,6 @@ App({
} else {
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
- setTimeout(() => this.connectWsHeartbeat(), 2000)
}
return res.data
@@ -1209,15 +1277,30 @@ App({
wx.setStorageSync('readSectionIds', list)
},
+ /**
+ * 记录「最近打开过」某章节(含仅预览/未登录),供「我的」最近阅读展示;
+ * 不写入 readSectionIds,避免把「点开预览」算进已读章节数。
+ */
+ touchRecentSection(sectionId) {
+ if (!sectionId) return
+ try {
+ let list = wx.getStorageSync('recent_section_opens')
+ if (!Array.isArray(list)) list = []
+ const now = Date.now()
+ list = list.filter((x) => x && x.id !== sectionId)
+ list.unshift({ id: String(sectionId), t: now })
+ wx.setStorageSync('recent_section_opens', list.slice(0, 40))
+ } catch (_) {}
+ },
+
// 已读章节数(用于首页展示)
getReadCount() {
return (this.globalData.readSectionIds || []).length
},
- // 获取章节总数(优先 API 已加载值,失败时返回统一兜底常量)
+ // 获取章节总数
getTotalSections() {
- const v = this.globalData.totalSections
- return (v != null && v > 0) ? v : FALLBACK_TOTAL_SECTIONS
+ return this.globalData.totalSections
},
// 切换TabBar
diff --git a/miniprogram/app.json b/miniprogram/app.json
index feeafc49..8dcb6322 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -4,8 +4,8 @@
"login-modal": "/components/login-modal/login-modal"
},
"pages": [
- "pages/chapters/chapters",
"pages/index/index",
+ "pages/chapters/chapters",
"pages/match/match",
"pages/my/my",
"pages/read/read",
@@ -14,6 +14,7 @@
"pages/privacy/privacy",
"pages/referral/referral",
"pages/purchases/purchases",
+ "pages/reading-records/reading-records",
"pages/settings/settings",
"pages/search/search",
"pages/addresses/addresses",
@@ -29,7 +30,8 @@
"pages/avatar-nickname/avatar-nickname",
"pages/gift-pay/detail",
"pages/gift-pay/list",
- "pages/gift-pay/redemption-detail"
+ "pages/gift-pay/redemption-detail",
+ "pages/dev-login/dev-login"
],
"window": {
"backgroundTextStyle": "light",
diff --git a/miniprogram/assets/images/karuo-link-avatar.png b/miniprogram/assets/images/karuo-link-avatar.png
new file mode 100644
index 00000000..e059c5b6
Binary files /dev/null and b/miniprogram/assets/images/karuo-link-avatar.png differ
diff --git a/miniprogram/assets/images/part-books/0.png b/miniprogram/assets/images/part-books/0.png
new file mode 100644
index 00000000..ed3dc4a0
Binary files /dev/null and b/miniprogram/assets/images/part-books/0.png differ
diff --git a/miniprogram/assets/images/part-books/1.png b/miniprogram/assets/images/part-books/1.png
new file mode 100644
index 00000000..29cad917
Binary files /dev/null and b/miniprogram/assets/images/part-books/1.png differ
diff --git a/miniprogram/assets/images/part-books/2.png b/miniprogram/assets/images/part-books/2.png
new file mode 100644
index 00000000..0bc8f47e
Binary files /dev/null and b/miniprogram/assets/images/part-books/2.png differ
diff --git a/miniprogram/assets/images/part-books/3.png b/miniprogram/assets/images/part-books/3.png
new file mode 100644
index 00000000..ed3dc4a0
Binary files /dev/null and b/miniprogram/assets/images/part-books/3.png differ
diff --git a/miniprogram/assets/images/part-books/4.png b/miniprogram/assets/images/part-books/4.png
new file mode 100644
index 00000000..29cad917
Binary files /dev/null and b/miniprogram/assets/images/part-books/4.png differ
diff --git a/miniprogram/components/login-modal/login-modal.wxml b/miniprogram/components/login-modal/login-modal.wxml
index b07bc3c7..f821e40e 100644
--- a/miniprogram/components/login-modal/login-modal.wxml
+++ b/miniprogram/components/login-modal/login-modal.wxml
@@ -6,14 +6,14 @@
登录 卡若创业派对
{{desc}}
-
分享后好友购买,你可获得 {{shareRate || 90}}% 收益
@@ -122,23 +122,36 @@
-
-
+
+
- 解锁完整内容
- 已预览部分内容,登录并购买后阅读全文
-
-
- 购买本章
- ¥{{section && section.price != null ? section.price : sectionPrice}}
+
+ 解锁全文
+
+
+ 购买本章
+ ¥1
+
-
+ 审核中,暂不支持购买
+ 预览不可付款,请点底部「前往小程序」。
+
-
- 手机号登录后购买
-
- 分享给好友一起学习,还能赚取佣金
+
+ 解锁完整内容
+ 已预览部分内容,登录并支付 ¥1 后阅读全文
+
+
+ 购买本章
+ ¥{{section && section.price != null ? section.price : sectionPrice}}
+
+
+
+ 手机号登录后购买
+
+ 分享给好友一起学习,还能赚取佣金
+
@@ -182,39 +195,47 @@
-
+
- 解锁完整内容
- 已阅读50%,购买后继续阅读
-
-
-
-
- 购买本章
- ¥{{section && section.price != null ? section.price : sectionPrice}}
-
-
-
-
-
-
- 解锁全部 {{totalSections}} 章
-
-
- ¥{{fullBookPrice || 9.9}}
- 省82%
+
+ 解锁全文
+
+
+ 购买本章
+ ¥1
-
- 审核中,暂不支持购买
+ 审核中,暂不支持购买
+ 预览不可付款,请点底部「前往小程序」。
+
- 分享给好友一起学习,还能赚取佣金
-
-
-
- 代付分享
-
+
+ 解锁完整内容
+ 已阅读50%,购买后继续阅读
+
+
+ 购买本章
+ ¥{{section && section.price != null ? section.price : sectionPrice}}
+
+
+
+
+ 解锁全部 {{totalSections}} 章
+
+
+ ¥{{fullBookPrice}}
+ 省82%
+
+
+
+ 审核中,暂不支持购买
+ 分享给好友一起学习,还能赚取佣金
+
+
+ 代付分享
+
+
@@ -270,9 +291,9 @@
-
-
-
+
+
+
-
-
-
+
+
+ ↘
+
+
+
+
diff --git a/miniprogram/pages/read/read.wxss b/miniprogram/pages/read/read.wxss
index 55b58ec0..2fe7a67a 100644
--- a/miniprogram/pages/read/read.wxss
+++ b/miniprogram/pages/read/read.wxss
@@ -280,6 +280,36 @@
margin-bottom: 16rpx;
}
+/* 朋友圈单页:与完整小程序同款购买行,留白略紧 */
+.paywall--single-preview {
+ padding-top: 40rpx;
+ padding-bottom: 40rpx;
+}
+.paywall--single-preview .paywall-icon {
+ margin-bottom: 24rpx;
+}
+.paywall--single-preview .paywall-title {
+ margin-bottom: 28rpx;
+}
+.paywall-desc--moments-expanded {
+ margin-top: 28rpx !important;
+ margin-bottom: 0 !important;
+ font-size: 26rpx !important;
+ line-height: 1.45;
+ padding: 0 8rpx;
+}
+
+/* 朋友圈单页:未点解锁前的一行轻提示 */
+.paywall-hint-compact {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.48);
+ text-align: center;
+ display: block;
+ margin-bottom: 36rpx;
+ line-height: 1.55;
+ padding: 0 16rpx;
+}
+
.paywall-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
@@ -360,6 +390,33 @@
margin-left: 8rpx;
}
+.paywall-singlepage-note {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.45);
+ text-align: center;
+ line-height: 1.5;
+}
+
+/* 朋友圈单页付费墙底部:与正文文末「分享赚收益」文案一致 */
+.paywall-share-earn-wrap {
+ margin-top: 28rpx;
+ padding-top: 24rpx;
+ border-top: 1rpx solid rgba(255, 255, 255, 0.08);
+ text-align: center;
+}
+.paywall-share-earn-wrap .share-tip-text {
+ display: block;
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+ line-height: 1.5;
+}
+.paywall-share-earn-sub {
+ margin-top: 12rpx !important;
+ display: block;
+}
+
.paywall-tip {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
@@ -470,7 +527,14 @@
.action-row-inline {
display: flex;
flex-wrap: nowrap;
- gap: 16rpx;
+ align-items: stretch;
+ gap: 12rpx;
+}
+
+/* 底部三按钮:同一底纹与描边(好友 / 海报 / 代付) */
+.action-tile-unified {
+ background: rgba(255, 255, 255, 0.06) !important;
+ border: 2rpx solid rgba(0, 206, 209, 0.28) !important;
}
.action-btn-inline {
@@ -489,21 +553,38 @@
overflow: hidden;
}
-.action-btn-inline::after {
+/* 分享给好友:原生 button + open-type=share,样式与 action-btn-inline 对齐 */
+.action-share-native {
+ flex: 1 1 0;
+ min-width: 0;
+ min-height: 96rpx;
+ margin: 0;
+ padding: 24rpx 12rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8rpx;
+ line-height: normal;
+ font-size: inherit;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+.action-share-native::after {
border: none;
}
-.btn-share-inline {
- background: rgba(7, 193, 96, 0.15);
- border: 2rpx solid rgba(7, 193, 96, 0.3);
+button.action-share-native {
+ color: inherit;
+}
+.action-share-native-hover {
+ opacity: 0.85;
}
-.btn-poster-inline {
- background: rgba(255, 215, 0, 0.15);
- border: 2rpx solid rgba(255, 215, 0, 0.3);
+.action-btn-inline::after {
+ border: none;
}
-
.action-icon-small {
font-size: 28rpx;
flex-shrink: 0;
@@ -597,7 +678,8 @@
display: flex;
align-items: flex-end;
justify-content: center;
- z-index: 1000;
+ /* 高于右下角悬浮钮,避免弹层被盖住或 canvas 不可见 */
+ z-index: 10050;
}
.modal-overlay-center {
@@ -1201,6 +1283,9 @@
/* ===== 海报弹窗 ===== */
.poster-modal {
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
+ max-height: 85vh;
+ overflow-y: auto;
+ box-sizing: border-box;
}
.poster-preview {
@@ -1251,44 +1336,54 @@
display: block;
}
-/* ===== 右下角悬浮分享按钮 ===== */
-.fab-share {
+/* ===== 右下角:分享到朋友圈(固定悬浮小圆钮,不在文末分享行) ===== */
+.fab-share-moments {
position: fixed;
right: 32rpx;
- width:70rpx!important;
bottom: calc(120rpx + env(safe-area-inset-bottom));
- height: 70rpx;
- border-radius: 60rpx;
+ width: 96rpx;
+ height: 96rpx;
+ border-radius: 50%;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
- box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
- padding: 0;
- margin: 0;
- border: none;
- z-index: 9999;
- display:flex;
+ box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.42);
+ z-index: 9980;
+ display: flex;
align-items: center;
justify-content: center;
- transition: transform 0.2s ease, box-shadow 0.2s ease;
+ transition: transform 0.15s ease, opacity 0.15s ease;
}
-
-.fab-share::after {
- border: none;
+.fab-share-moments-hover {
+ opacity: 0.9;
}
-
-.fab-share:active {
- transform: scale(0.95);
- box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
+.fab-share-moments:active {
+ transform: scale(0.94);
}
-
-.fab-icon {
- padding:16rpx;
- width: 50rpx;
- height: 50rpx;
- display: block;
-}
-
-.fab-share-icon {
+.fab-share-moments-icon {
font-size: 44rpx;
line-height: 1;
+ filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.25));
}
+/* 朋友圈单页:点「购买本章」后,箭头指向底栏「前往小程序」(字少,仅符号) */
+.singlepage-launch-pointer {
+ position: fixed;
+ right: 48rpx;
+ bottom: calc(168rpx + env(safe-area-inset-bottom));
+ z-index: 99985;
+ pointer-events: none;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+}
+.singlepage-launch-pointer__arrow {
+ font-size: 56rpx;
+ line-height: 1;
+ color: #00CED1;
+ text-shadow: 0 0 20rpx rgba(0, 206, 209, 0.55);
+ transform: rotate(0deg);
+ animation: singlepage-launch-pulse 1.25s ease-in-out infinite;
+}
+@keyframes singlepage-launch-pulse {
+ 0%, 100% { opacity: 0.75; transform: translate(0, 0) scale(1); }
+ 50% { opacity: 1; transform: translate(8rpx, 10rpx) scale(1.06); }
+}
diff --git a/miniprogram/pages/reading-records/reading-records.js b/miniprogram/pages/reading-records/reading-records.js
new file mode 100644
index 00000000..b8412684
--- /dev/null
+++ b/miniprogram/pages/reading-records/reading-records.js
@@ -0,0 +1,178 @@
+/**
+ * 阅读记录:最近阅读 + 已读章节(与「我的」数据源一致)
+ */
+const app = getApp()
+const { cleanSingleLineField } = require('../../utils/contentParser.js')
+
+function titleFromBookData(sectionId, bookFlat) {
+ const row = bookFlat.find((s) => s.id === sectionId)
+ return cleanSingleLineField(
+ row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
+ ) || `章节 ${sectionId}`
+}
+
+function midFromBookData(sectionId, bookFlat) {
+ const row = bookFlat.find((s) => s.id === sectionId)
+ return row?.mid ?? row?.MID ?? 0
+}
+
+function mergeRecentFromLocal(apiList) {
+ const normalized = Array.isArray(apiList)
+ ? apiList.map((item) => ({
+ id: item.id,
+ mid: item.mid,
+ title: cleanSingleLineField(item.title || '') || `章节 ${item.id}`
+ }))
+ : []
+ if (normalized.length > 0) return normalized
+ try {
+ const progressData = wx.getStorageSync('reading_progress') || {}
+ const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
+ return Object.keys(progressData)
+ .map((id) => ({
+ id,
+ ts: progressData[id]?.lastOpenAt || progressData[id]?.last_open_at || 0
+ }))
+ .filter((e) => e.id)
+ .sort((a, b) => b.ts - a.ts)
+ .slice(0, 20)
+ .map((e) => ({
+ id: e.id,
+ mid: midFromBookData(e.id, bookFlat),
+ title: titleFromBookData(e.id, bookFlat)
+ }))
+ } catch (e) {
+ return []
+ }
+}
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ isLoggedIn: false,
+ focus: 'all',
+ recentList: [],
+ readAllList: [],
+ recentSectionTitle: '最近阅读',
+ readSectionTitle: '已读章节'
+ },
+
+ onLoad(options) {
+ const focus = (options.focus === 'recent' || options.focus === 'read') ? options.focus : 'all'
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44,
+ focus
+ })
+ this._applyMpUiTitles()
+ },
+
+ onShow() {
+ this.setData({ isLoggedIn: !!(app.globalData.isLoggedIn && app.globalData.userInfo?.id) })
+ this._applyMpUiTitles()
+ if (this.data.isLoggedIn) this.loadData()
+ },
+
+ _applyMpUiTitles() {
+ const my = app.globalData.configCache?.mpConfig?.mpUi?.myPage || {}
+ this.setData({
+ recentSectionTitle: my.recentReadTitle || '最近阅读',
+ readSectionTitle: my.readStatLabel || '已读章节'
+ })
+ },
+
+ async _ensureBookFlat() {
+ let flat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
+ if (flat.length) return flat
+ try {
+ const r = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
+ const list = r?.data
+ if (Array.isArray(list) && list.length) {
+ app.globalData.bookData = list
+ return list
+ }
+ } catch (_) {}
+ return []
+ },
+
+ async loadData() {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) return
+ try {
+ const res = await app.request({
+ url: `/api/miniprogram/user/dashboard-stats?userId=${encodeURIComponent(userId)}`,
+ silent: true
+ })
+ const bookFlat = await this._ensureBookFlat()
+ let recent = []
+ let readIds = []
+ if (res?.success && res.data) {
+ const apiRecent = Array.isArray(res.data.recentChapters) ? res.data.recentChapters : []
+ recent = mergeRecentFromLocal(apiRecent)
+ readIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds.filter(Boolean) : []
+ } else {
+ recent = mergeRecentFromLocal([])
+ }
+ try {
+ const progressData = wx.getStorageSync('reading_progress') || {}
+ const fromKeys = Object.keys(progressData).filter(Boolean)
+ const stored = wx.getStorageSync('readSectionIds')
+ const fromStored = Array.isArray(stored) ? stored.filter(Boolean) : []
+ const fromGlobal = Array.isArray(app.globalData.readSectionIds)
+ ? app.globalData.readSectionIds.filter(Boolean)
+ : []
+ readIds = [...new Set([...readIds, ...fromGlobal, ...fromStored, ...fromKeys])]
+ } catch (_) {}
+ if (readIds.length === 0 && recent.length > 0) {
+ readIds = recent.map((r) => r.id)
+ }
+ const readAllList = readIds.map((id) => ({
+ id,
+ mid: midFromBookData(id, bookFlat),
+ title: titleFromBookData(id, bookFlat)
+ }))
+ this.setData({ recentList: recent, readAllList })
+ } catch (e) {
+ console.warn('[reading-records]', e)
+ try {
+ const bookFlat = await this._ensureBookFlat()
+ let readIds = Array.isArray(app.globalData.readSectionIds) ? [...app.globalData.readSectionIds] : []
+ if (!readIds.length) {
+ try {
+ const stored = wx.getStorageSync('readSectionIds')
+ if (Array.isArray(stored)) readIds = [...stored]
+ } catch (_) {}
+ }
+ const recent = mergeRecentFromLocal([])
+ if (!readIds.length && recent.length) readIds = recent.map((r) => r.id)
+ const readAllList = readIds.map((id) => ({
+ id,
+ mid: midFromBookData(id, bookFlat),
+ title: titleFromBookData(id, bookFlat)
+ }))
+ this.setData({ recentList: recent, readAllList })
+ } catch (_) {
+ this.setData({ recentList: [], readAllList: [] })
+ }
+ }
+ },
+
+ goRead(e) {
+ const id = e.currentTarget.dataset.id
+ const mid = e.currentTarget.dataset.mid
+ if (!id) return
+ const q = mid ? `mid=${mid}` : `id=${id}`
+ wx.navigateTo({ url: `/pages/read/read?${q}` })
+ },
+
+ goChapters() {
+ wx.switchTab({ url: '/pages/chapters/chapters' })
+ },
+
+ goLogin() {
+ wx.switchTab({ url: '/pages/my/my' })
+ },
+
+ goBack() {
+ getApp().goBackOrToHome()
+ }
+})
diff --git a/miniprogram/pages/reading-records/reading-records.json b/miniprogram/pages/reading-records/reading-records.json
new file mode 100644
index 00000000..e90e9960
--- /dev/null
+++ b/miniprogram/pages/reading-records/reading-records.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {},
+ "navigationStyle": "custom"
+}
diff --git a/miniprogram/pages/reading-records/reading-records.wxml b/miniprogram/pages/reading-records/reading-records.wxml
new file mode 100644
index 00000000..d102064e
--- /dev/null
+++ b/miniprogram/pages/reading-records/reading-records.wxml
@@ -0,0 +1,51 @@
+
+
+
+ 阅读记录
+
+
+
+
+
+
+
+ {{recentSectionTitle}}
+ {{recentList.length}} 条
+
+
+
+ {{index + 1}}
+ {{item.title}}
+ 阅读
+
+
+
+ 暂无最近阅读
+ 去目录逛逛
+
+
+
+
+
+ {{readSectionTitle}}
+ 共 {{readAllList.length}} 章
+
+
+
+ {{index + 1}}
+ {{item.title}}
+ 阅读
+
+
+
+ 暂无已读记录
+ 去目录选章阅读
+
+
+
+
+
+ 登录后查看阅读记录
+ 去登录
+
+
diff --git a/miniprogram/pages/reading-records/reading-records.wxss b/miniprogram/pages/reading-records/reading-records.wxss
new file mode 100644
index 00000000..4d3cb328
--- /dev/null
+++ b/miniprogram/pages/reading-records/reading-records.wxss
@@ -0,0 +1,25 @@
+.page { min-height: 100vh; background: #000; padding-bottom: 48rpx; }
+.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.92); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
+.nav-back { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: center; }
+.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; flex: 1; text-align: center; }
+.nav-placeholder { width: 72rpx; }
+.content { padding: 32rpx; }
+.section { margin-bottom: 48rpx; }
+.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 24rpx; }
+.section-title { font-size: 32rpx; font-weight: 600; color: #e5e7eb; }
+.section-count { font-size: 24rpx; color: #6b7280; }
+.list { display: flex; flex-direction: column; gap: 16rpx; }
+.row {
+ display: flex; align-items: center; gap: 20rpx;
+ padding: 24rpx; background: #1c1c1e; border-radius: 20rpx;
+}
+.row:active { opacity: 0.9; }
+.row-idx { font-size: 26rpx; color: #6b7280; font-family: monospace; flex-shrink: 0; }
+.row-title { flex: 1; min-width: 0; font-size: 28rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.row-link { font-size: 24rpx; color: #00CED1; flex-shrink: 0; }
+.empty { padding: 48rpx 24rpx; text-align: center; }
+.empty-t { display: block; font-size: 28rpx; color: #6b7280; margin-bottom: 24rpx; }
+.empty-a { font-size: 28rpx; color: #00CED1; }
+.guest { padding: 120rpx 48rpx; text-align: center; }
+.guest-t { display: block; font-size: 30rpx; color: #9ca3af; margin-bottom: 32rpx; }
+.guest-btn { display: inline-block; padding: 20rpx 48rpx; background: #00CED1; color: #000; font-size: 28rpx; font-weight: 600; border-radius: 24rpx; }
diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js
index 35736592..7cbf7de9 100644
--- a/miniprogram/pages/referral/referral.js
+++ b/miniprogram/pages/referral/referral.js
@@ -35,7 +35,7 @@ Page({
minWithdrawAmount: 10, // 最低提现金额,从 referral/data 获取
bindingDays: 30, // 绑定期天数,从 referral/data 获取
userDiscount: 5, // 好友购买优惠%,从 referral/data 获取
- hasWechatId: false, // 是否已绑定微信号(未绑定时需引导去设置)
+ hasWechatId: false, // 是否已绑定微信号(未绑定时提示去设置页)
// === 统计数据 ===
referralCount: 0, // 总推荐人数
@@ -46,6 +46,8 @@ Page({
// 绑定用户列表
showBindingList: true,
+ showShareSection: false,
+ showEarningsDetails: false,
activeTab: 'active',
activeBindings: [],
convertedBindings: [],
@@ -63,10 +65,13 @@ Page({
posterReferralLink: '',
posterNickname: '',
posterNicknameInitial: '',
- posterCaseCount: 0 // 来自 app.getTotalSections(),initData 后更新
+ /** 推广海报推荐人头像:优先微信头像,否则按 MBTI 用后台映射 */
+ posterDisplayAvatar: '',
+ posterCaseCount: 62
},
- onLoad() {
+ onLoad(options) {
+ this._focusTarget = (options && options.focus) || ""
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
this.initData()
// 启用分享到朋友圈(需同时有 onShareAppMessage 和 onShareTimeline;menus 在 Android 支持,iOS 为 Beta)
@@ -174,7 +179,6 @@ Page({
minWithdrawAmount: minWithdrawAmount,
bindingDays: realData?.bindingDays ?? 30,
userDiscount: realData?.userDiscount ?? 5,
- posterCaseCount: app.getTotalSections(),
// 统计
referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
@@ -208,12 +212,37 @@ Page({
// 隐藏加载提示
wx.hideLoading()
+ this._applyFocusTarget()
} else {
// 未登录时也隐藏loading
this.setData({ isLoading: false })
}
},
+
+ toggleShareSection() {
+ this.setData({ showShareSection: !this.data.showShareSection })
+ },
+
+ toggleEarningsDetails() {
+ this.setData({ showEarningsDetails: !this.data.showEarningsDetails })
+ },
+
+ _applyFocusTarget() {
+ const t = this._focusTarget || ''
+ if (!t) return
+ if (t === 'bindings') {
+ this.setData({ showBindingList: true, activeTab: 'active', currentBindings: this.data.activeBindings })
+ setTimeout(() => wx.pageScrollTo({ selector: '#binding-card', duration: 220 }), 80)
+ this._focusTarget = ''
+ return
+ }
+ if (t === 'earnings') {
+ setTimeout(() => wx.pageScrollTo({ selector: '#earnings-card', duration: 220 }), 80)
+ this._focusTarget = ''
+ return
+ }
+ },
// 切换Tab
switchTab(e) {
const tab = e.currentTarget.dataset.tab
@@ -343,11 +372,20 @@ Page({
console.log('[Poster] 小程序码已保存到本地:', filePath)
+ try {
+ if (app.loadMbtiAvatarsMap) await app.loadMbtiAvatarsMap()
+ } catch (_) {}
+ const u = this.data.userInfo || app.globalData.userInfo || {}
+ const posterDisplayAvatar = app.resolveAvatarWithMbti
+ ? app.resolveAvatarWithMbti(u.avatar, u.mbti)
+ : ''
+
this.setData({
posterQrSrc: filePath,
posterReferralLink: '', // 小程序版本不再使用 H5 链接
posterNickname: nickname,
posterNicknameInitial: (nickname || '用').charAt(0),
+ posterDisplayAvatar,
isGeneratingPoster: false
})
wx.hideLoading()
@@ -355,7 +393,7 @@ Page({
console.error('[Poster] 生成二维码失败:', e)
wx.hideLoading()
wx.showToast({ title: '生成失败', icon: 'none' })
- this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterReferralLink: '' })
+ this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterReferralLink: '', posterDisplayAvatar: '' })
}
},
@@ -546,10 +584,9 @@ Page({
// 分享到朋友圈 - 随机文案
shareToMoments() {
- const total = app.getTotalSections()
// 10条随机文案,基于书的内容
const shareTexts = [
- `🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n${total}个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
+ `🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《卡若创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
@@ -600,7 +637,7 @@ Page({
}
// 任意金额可提现,不再设最低限额
- // 未绑定微信号时引导去设置
+ // 未绑定微信号:说明提现到账核对所需
if (!hasWechatId) {
wx.showModal({
title: '请先绑定微信号',
@@ -656,31 +693,37 @@ Page({
method: 'POST',
data: { userId, amount }
})
-
+
wx.hideLoading()
- wx.showModal({
- title: '提现申请已提交 ✅',
- content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
- showCancel: false,
- confirmText: '知道了'
- })
- this.initData()
+
+ 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 })
+ }
+ }
} catch (e) {
wx.hideLoading()
console.error('[Referral] 提现失败:', e)
- 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 })
+ wx.showToast({ title: '提现失败,请重试', icon: 'none' })
}
},
@@ -856,9 +899,8 @@ Page({
onShareTimeline() {
const ref = this.data.referralCode || app.getMyReferralCode()
console.log('[Referral] 分享到朋友圈,推荐码:', ref)
- const total = app.getTotalSections()
return {
- title: `卡若创业派对 - ${total}个真实商业案例`,
+ title: `卡若创业派对 - 62个真实商业案例`,
query: ref ? `ref=${ref}` : ''
// 不设置 imageUrl,使用小程序默认截图
}
diff --git a/miniprogram/pages/referral/referral.wxml b/miniprogram/pages/referral/referral.wxml
index d1bb3d0d..a33b68bb 100644
--- a/miniprogram/pages/referral/referral.wxml
+++ b/miniprogram/pages/referral/referral.wxml
@@ -25,8 +25,7 @@
-
-
+