From 1edceda4db72808479171245c7f30ee6024a2fcd Mon Sep 17 00:00:00 2001
From: Alex-larget <33240357+Alex-larget@users.noreply.github.com>
Date: Sat, 14 Mar 2026 17:13:06 +0800
Subject: [PATCH] Update evolution indices and enhance user experience in mini
program
- Added new entries for content ranking algorithm adjustments and cross-platform reuse in the evolution indices for backend, team, and mini program development.
- Improved user interface elements in the mini program, including the addition of a hidden settings entry and refined guidance for profile modifications.
- Enhanced reading statistics display with formatted numbers for better clarity and user engagement.
- Updated reading tracker logic to prevent duplicate duration accumulation and ensure accurate reporting of reading progress.
---
.../agent/后端工程师/evolution/2026-03-14.md | 33 +++
.cursor/agent/后端工程师/evolution/索引.md | 1 +
.cursor/agent/团队/evolution/2026-03-14.md | 17 ++
.cursor/agent/团队/evolution/索引.md | 1 +
.../evolution/2025-03-14-文本长按复制.md | 40 +++
.../小程序开发工程师/evolution/2026-03-14.md | 18 ++
.../agent/小程序开发工程师/evolution/索引.md | 2 +
.cursor/agent/开发助理/经验清单.md | 4 +-
.cursor/agent/开发助理/项目索引/后端.md | 3 +-
.cursor/agent/开发助理/项目索引/团队.md | 3 +-
.cursor/agent/开发助理/项目索引/小程序.md | 4 +-
.cursor/agent/开发助理/项目索引/管理端.md | 3 +-
.../管理端开发工程师/evolution/2026-03-14.md | 17 ++
.../agent/管理端开发工程师/evolution/索引.md | 1 +
.cursor/skills/miniprogram-dev/SKILL.md | 11 +-
miniprogram/app.js | 8 +-
miniprogram/app.json | 3 +-
.../pages/avatar-nickname/avatar-nickname.js | 155 ++++++++++
.../avatar-nickname/avatar-nickname.json | 4 +
.../avatar-nickname/avatar-nickname.wxml | 67 +++++
.../avatar-nickname/avatar-nickname.wxss | 271 ++++++++++++++++++
miniprogram/pages/index/index.js | 37 +--
miniprogram/pages/my/my.js | 29 +-
miniprogram/pages/my/my.wxml | 10 +-
.../pages/profile-edit/profile-edit.js | 13 +
.../pages/profile-edit/profile-edit.wxss | 7 +-
miniprogram/utils/readingTracker.js | 8 +-
miniprogram/utils/util.js | 13 +
soul-admin/src/pages/users/UsersPage.tsx | 4 +-
soul-api/internal/handler/db.go | 13 +-
soul-api/internal/handler/user.go | 44 ++-
.../avatars/1773477415258581700_nbanes.jpeg | Bin 0 -> 3441 bytes
soul-api/wechat/info.log | 4 +
33 files changed, 773 insertions(+), 75 deletions(-)
create mode 100644 .cursor/agent/后端工程师/evolution/2026-03-14.md
create mode 100644 .cursor/agent/团队/evolution/2026-03-14.md
create mode 100644 .cursor/agent/小程序开发工程师/evolution/2025-03-14-文本长按复制.md
create mode 100644 .cursor/agent/小程序开发工程师/evolution/2026-03-14.md
create mode 100644 .cursor/agent/管理端开发工程师/evolution/2026-03-14.md
create mode 100644 miniprogram/pages/avatar-nickname/avatar-nickname.js
create mode 100644 miniprogram/pages/avatar-nickname/avatar-nickname.json
create mode 100644 miniprogram/pages/avatar-nickname/avatar-nickname.wxml
create mode 100644 miniprogram/pages/avatar-nickname/avatar-nickname.wxss
create mode 100644 soul-api/uploads/avatars/1773477415258581700_nbanes.jpeg
diff --git a/.cursor/agent/后端工程师/evolution/2026-03-14.md b/.cursor/agent/后端工程师/evolution/2026-03-14.md
new file mode 100644
index 00000000..b8db6779
--- /dev/null
+++ b/.cursor/agent/后端工程师/evolution/2026-03-14.md
@@ -0,0 +1,33 @@
+# 2026-03-14 - 内容排名算法修正(排名分公式)
+
+## 问题 / 场景
+
+- 管理端「内容排行」与小程序「精选推荐」共用 `computeArticleRankingSections`,原算法错误:
+ - 使用「原始数值 × 权重」:`hot = readCnt×readWeight + payCnt×payWeight + recencyScore×recencyWeight`
+ - `recencyScore` 为 0–1 的天数衰减,非排名分
+- 管理端修改权重后,列表不刷新(只调了 loadList,未调 loadRanking)。
+
+## 解决方案
+
+### 算法修正(db_book.go computeSectionsWithHotScore)
+
+- **公式**:热度积分 = 阅读权重×阅读排名分 + 新度权重×新度排名分 + 付款权重×付款排名分(三权重之和须为 1)
+- **排名分规则**:
+ - 阅读量前 20 名:第 1 名=20 分 … 第 20 名=1 分,其余 0 分
+ - 最近更新前 30 篇:第 1 名=30 分 … 第 30 名=1 分,其余 0 分
+ - 付款数前 20 名:第 1 名=20 分 … 第 20 名=1 分,其余 0 分
+- **权重配置**:从 `system_config.article_ranking_weights` 读取 readWeight、recencyWeight、payWeight
+- **手动覆盖**:若 `chapters.hot_score > 0`,则优先使用该值
+
+### 与前端约定
+
+- 管理端保存权重后需同时调用 `loadList()` 和 `loadRanking()`,并关闭弹窗,列表才能立即刷新。
+
+## 代码位置
+
+- `soul-api/internal/handler/db_book.go`:`computeSectionsWithHotScore`、`computeArticleRankingSections`
+- 管理端 `ContentPage.tsx`:`handleSaveRankingWeights` 中 loadRanking + setShowRankingAlgorithmModal(false)
+
+## 影响
+
+- 管理端内容排行榜、小程序精选推荐(`/api/miniprogram/book/recommended`)均复用该算法,修正后两端同步生效。
diff --git a/.cursor/agent/后端工程师/evolution/索引.md b/.cursor/agent/后端工程师/evolution/索引.md
index 30ba6514..48b0b2cb 100644
--- a/.cursor/agent/后端工程师/evolution/索引.md
+++ b/.cursor/agent/后端工程师/evolution/索引.md
@@ -6,3 +6,4 @@
| 2026-03-05 | 文章详情@某人:content 内嵌 @ 标记、miniprogram 添加好友接口 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-10 | 管理端迁移 Mycontent-temp:接口边界不变;overview 聚合接口可选但需降级 | [2026-03-10.md](./2026-03-10.md) |
| 2026-03-12 | persons token 字段与 DB 迁移;CKBLead 用 token 兑换 ckb_api_key | [2026-03-12.md](./2026-03-12.md) |
+| 2026-03-14 | 内容排名算法修正:排名分公式(阅读/新度/付款前 N 名),支持 hot_score 手动覆盖 | [2026-03-14.md](./2026-03-14.md) |
diff --git a/.cursor/agent/团队/evolution/2026-03-14.md b/.cursor/agent/团队/evolution/2026-03-14.md
new file mode 100644
index 00000000..346a20cc
--- /dev/null
+++ b/.cursor/agent/团队/evolution/2026-03-14.md
@@ -0,0 +1,17 @@
+# 2026-03-14 - 内容排名算法跨端复用约定
+
+## 业务规则
+
+- **热度积分公式**:阅读权重×阅读排名分 + 新度权重×新度排名分 + 付款权重×付款排名分(三权重之和须为 1)
+- **排名分规则**:阅读量前 20 名(20~1 分)、最近更新前 30 篇(30~1 分)、付款数前 20 名(20~1 分)
+
+## 跨端复用
+
+- **管理端**:`/api/db/book?action=ranking` → `computeArticleRankingSections`
+- **小程序**:`/api/miniprogram/book/recommended` → `computeArticleRankingSections`(取前 3 条)
+- 两者共用同一套算法、权重配置(`article_ranking_weights`)、置顶配置(`pinned_section_ids`)
+
+## 约定
+
+- 排名算法修改只需改 soul-api 一处,管理端与小程序自动同步。
+- 管理端保存权重后必须调用 `loadRanking()` 刷新列表,否则用户看不到变化。
diff --git a/.cursor/agent/团队/evolution/索引.md b/.cursor/agent/团队/evolution/索引.md
index 88f99084..4095e819 100644
--- a/.cursor/agent/团队/evolution/索引.md
+++ b/.cursor/agent/团队/evolution/索引.md
@@ -7,3 +7,4 @@
| 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-12 | 密钥/token 设计:关联小程序 key、@ 人物 token,不暴露真实密钥、服务端兑换 | [2026-03-12.md](./2026-03-12.md) |
+| 2026-03-14 | 内容排名算法跨端复用:管理端内容排行与小程序精选推荐共用 computeArticleRankingSections | [2026-03-14.md](./2026-03-14.md) |
diff --git a/.cursor/agent/小程序开发工程师/evolution/2025-03-14-文本长按复制.md b/.cursor/agent/小程序开发工程师/evolution/2025-03-14-文本长按复制.md
new file mode 100644
index 00000000..c58cb917
--- /dev/null
+++ b/.cursor/agent/小程序开发工程师/evolution/2025-03-14-文本长按复制.md
@@ -0,0 +1,40 @@
+# 阅读页文本长按选中复制
+
+> 问题→解决闭环,已升级 miniprogram-dev SKILL
+
+---
+
+## 问题
+
+小程序阅读页正文长按时无法选中、复制文本。
+
+---
+
+## 解决方案
+
+使用 `text` 组件的 **`user-select`** 属性(基础库 2.12.1+,官方推荐;`selectable` 已废弃):
+
+```html
+{{seg.text}}
+```
+
+---
+
+## 实施范围(read.wxml)
+
+- 章节标题:``
+- 正文片段:text / mention / linkTag 均加 `user-select`
+- 预览段落:`{{item}}`(原为 view 内直接 `{{item}}`,需用 text 包裹)
+
+---
+
+## 备选方案
+
+- **selectable**:已废弃但多数环境仍可用,若 `user-select` 导致布局异常(inline-block 换行)可回退
+- **wx.setClipboardData + bindlongpress**:iOS 原生选中失效时,可做长按整段复制兜底
+
+---
+
+## Skill 升级
+
+已写入 miniprogram-dev SKILL §10 文本可选与复制。
diff --git a/.cursor/agent/小程序开发工程师/evolution/2026-03-14.md b/.cursor/agent/小程序开发工程师/evolution/2026-03-14.md
new file mode 100644
index 00000000..4a4ab326
--- /dev/null
+++ b/.cursor/agent/小程序开发工程师/evolution/2026-03-14.md
@@ -0,0 +1,18 @@
+# 2026-03-14 - 我的页设置隐藏与资料引导场景梳理
+
+## 1. 设置入口隐藏
+
+- **需求**:我的页「设置」菜单项隐藏
+- **实现**:`my.wxml` 中设置菜单项加 `wx:if="{{false}}"`
+- **说明**:设置页仍存在,用户可通过推广页「去设置」等入口进入(如绑定微信号用于提现)
+
+## 2. 资料修改引导场景(何时引导用户修改资料)
+
+| 场景 | 位置 | 触发条件 | 行为 |
+|------|------|----------|------|
+| 登录后 | app.js `_ensureProfileCompletedAfterLogin` | 昵称空/默认/头像空 | Toast「请先完善头像和昵称」→ 跳转 profile-edit |
+| 阅读页 @某人 | read.js `_doMentionAddFriend` | 已登录但无手机/微信号 | 弹窗「完善资料」→ 确认跳转 profile-edit |
+| 找伙伴页 | match.js `ensureContactInfo` | 已登录但无手机/微信号 | 页面内弹窗填写联系方式(不跳转) |
+| 首页链接卡若 | index.js | 无手机/微信号 | 弹窗输入手机号(不跳转 profile-edit) |
+
+- **判断依据**:`_isProfileIncomplete(user)` 检查 nickname、avatar;手机/微信号需单独拉 `user/profile` 接口判断。
diff --git a/.cursor/agent/小程序开发工程师/evolution/索引.md b/.cursor/agent/小程序开发工程师/evolution/索引.md
index f63c6986..31be5043 100644
--- a/.cursor/agent/小程序开发工程师/evolution/索引.md
+++ b/.cursor/agent/小程序开发工程师/evolution/索引.md
@@ -3,9 +3,11 @@
| 日期 | 摘要 | 文件 |
|------|------|------|
| 2025-03-14 | 联网吸收:基础库 3.14、Skyline、隐私按需授权、新 API | [2025-03-14-联网吸收小程序最新开发规则与API.md](./2025-03-14-联网吸收小程序最新开发规则与API.md) |
+| 2025-03-14 | 阅读页文本长按选中复制:text 组件 user-select | [2025-03-14-文本长按复制.md](./2025-03-14-文本长按复制.md) |
| 2026-02-28 | input 边距口诀、match 资源对接弹窗修正 | [2026-02-28.md](./2026-02-28.md) |
| 2026-03-03 | 我的页面卡片区边距优化,16rpx 推荐值 | [2026-03-03.md](./2026-03-03.md) |
| 2026-03-05 | 分支合并后核心流程自测;app.json 拆行;orders 接口确认 | [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-12 | 链接标签 mpKey、@ 人物 token 兑换:contentParser、onLinkTagTap、onMentionTap | [2026-03-12.md](./2026-03-12.md) |
+| 2026-03-14 | 我的页设置入口隐藏;资料修改引导场景梳理(登录后、@某人、找伙伴、链接卡若) | [2026-03-14.md](./2026-03-14.md) |
diff --git a/.cursor/agent/开发助理/经验清单.md b/.cursor/agent/开发助理/经验清单.md
index 82b1453a..f84682a0 100644
--- a/.cursor/agent/开发助理/经验清单.md
+++ b/.cursor/agent/开发助理/经验清单.md
@@ -40,6 +40,8 @@
| 2026-03-12 | 后端、团队 | 业务规则 | api-dev SKILL、team three-tier-arch | 9.9 买断统一由后端折叠为 hasFullBook/has_full_book,小程序和管理端只认该信号;可通过用户资料开关 OR 订单实现赠送全书 |
| 2026-03-13 | 小程序、后端、团队 | 业务规则 | api-dev SKILL、miniprogram-dev SKILL、three-tier-arch SKILL | 文章详情预览统一由后端按 50% 截取,小程序按 accessState 使用预览/全文,外层 content 与 data.content 始终一致以避免泄露全文 |
+| 2025-03-14 | 小程序 | 最佳实践 | miniprogram-dev SKILL §10 | 阅读页文本长按选中复制:text 组件 user-select(selectable 已废弃),正文/标题/预览均加 user-select |
+| 2026-03-14 | 后端、管理端、小程序、团队 | 业务规则/bug 修复 | - | 内容排名算法修正(排名分公式);保存权重后 loadRanking 刷新;我的页设置隐藏;资料引导场景梳理 |
---
@@ -50,4 +52,4 @@
---
-**最后更新**:2026-03-13(文章预览规则统一、小程序与后端对齐)
+**最后更新**:2026-03-14(排名算法修正、设置隐藏、资料引导梳理)
diff --git a/.cursor/agent/开发助理/项目索引/后端.md b/.cursor/agent/开发助理/项目索引/后端.md
index cf970151..c19177a8 100644
--- a/.cursor/agent/开发助理/项目索引/后端.md
+++ b/.cursor/agent/开发助理/项目索引/后端.md
@@ -27,9 +27,10 @@ soul-api(Go + Gin + GORM + MySQL)提供三组路由:`/api/miniprogram/*`
| 2026-03-12 | persons 表新增 token 字段(add-persons-token.sql);CKBLead 用 token 兑换 ckb_api_key | 已完成 |
| 2026-03-12 | 9.9 买断后端开关方案:users 增手动 fullbook 开关,purchase-status/check-purchased 折叠为统一 hasFullBook/has_full_book,小程序免改即支持赠送全书 | 已完成 |
| 2026-03-13 | 文章详情预览统一与安全:previewContent 改为截取正文前 50%,findChapterAndRespond 保证外层 content 与 data.content 一致,未授权只返回预览 | 已完成 |
+| 2026-03-14 | 内容排名算法修正:computeSectionsWithHotScore 改为排名分公式(阅读/新度/付款前 N 名得分),支持手动 hot_score 覆盖 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
---
-**最后更新**:2026-03-13
+**最后更新**:2026-03-14
diff --git a/.cursor/agent/开发助理/项目索引/团队.md b/.cursor/agent/开发助理/项目索引/团队.md
index aaf23c42..8def0a39 100644
--- a/.cursor/agent/开发助理/项目索引/团队.md
+++ b/.cursor/agent/开发助理/项目索引/团队.md
@@ -24,9 +24,10 @@ Soul 创业派对全项目架构与约定:路由隔离(miniprogram/admin/db
| 2026-03-12 | 密钥/token 设计:关联小程序 key、@ 人物 token;不暴露真实密钥、服务端兑换 | 已完成 |
| 2026-03-12 | 9.9 买断团队约定:后端统一输出 hasFullBook/has_full_book,小程序和管理端只认该信号;支持通过用户资料开关 OR 订单赠送全书且不影响 VIP 逻辑 | 已完成 |
| 2026-03-13 | 文章详情预览规则统一:预览长度由后端统一按 50% 计算,小程序按 accessState 切换预览/全文,接口约定 content 与 data.content 始终一致 | 已完成 |
+| 2026-03-14 | 内容排名算法跨端复用:管理端内容排行与小程序精选推荐共用 computeArticleRankingSections,排名分公式、权重配置统一 | 已完成 |
> **格式说明**:每次架构级讨论后在此追加一行,日期格式 YYYY-MM-DD
---
-**最后更新**:2026-03-13
+**最后更新**:2026-03-14
diff --git a/.cursor/agent/开发助理/项目索引/小程序.md b/.cursor/agent/开发助理/项目索引/小程序.md
index d7834d26..70e41916 100644
--- a/.cursor/agent/开发助理/项目索引/小程序.md
+++ b/.cursor/agent/开发助理/项目索引/小程序.md
@@ -29,9 +29,11 @@
| 2026-03-11 | 以界面定需求:小程序界面清单纳入《以界面定需求》;展示以用户资料为准,与现有实现一致 | 已完成 |
| 2026-03-12 | 链接标签 mpKey 兑换 appId;@ 人物 token 兑换 ckb_api_key;contentParser、onLinkTagTap、onMentionTap | 已完成 |
| 2026-03-13 | 阅读页文章预览与付费解锁对齐:预览长度改由后端统一计算,前端按 accessState 显示预览/全文,避免 data.content 泄露全文 | 已完成 |
+| 2025-03-14 | 阅读页文本长按选中复制:text 组件 user-select,已升级 SKILL §10 | 已完成 |
+| 2026-03-14 | 我的页设置入口隐藏(wx:if);资料修改引导场景梳理(登录后、@某人、找伙伴、链接卡若) | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
---
-**最后更新**:2026-03-13
+**最后更新**:2026-03-14
diff --git a/.cursor/agent/开发助理/项目索引/管理端.md b/.cursor/agent/开发助理/项目索引/管理端.md
index 9ccd4447..f3d6120a 100644
--- a/.cursor/agent/开发助理/项目索引/管理端.md
+++ b/.cursor/agent/开发助理/项目索引/管理端.md
@@ -24,9 +24,10 @@
| 2026-03-10 | Toast 通知系统落地:创建 utils/toast.ts(纯原生),全系统 18 文件约 90 处 alert 全部替换为 toast.success/error/info | 已完成 |
| 2026-03-11 | 以界面定需求:管理端界面清单纳入《以界面定需求》,作为验收基准 | 已完成 |
| 2026-03-12 | ContentPage TypeScript 严格类型修复;关联小程序 key、@ 人物 token 设计(链接标签存 key、PersonItem.id=token) | 已完成 |
+| 2026-03-14 | 排名算法权重保存后刷新:handleSaveRankingWeights 成功后 loadRanking + 关闭弹窗,列表立即更新 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
---
-**最后更新**:2026-03-12
+**最后更新**:2026-03-14
diff --git a/.cursor/agent/管理端开发工程师/evolution/2026-03-14.md b/.cursor/agent/管理端开发工程师/evolution/2026-03-14.md
new file mode 100644
index 00000000..49672f01
--- /dev/null
+++ b/.cursor/agent/管理端开发工程师/evolution/2026-03-14.md
@@ -0,0 +1,17 @@
+# 2026-03-14 - 内容排行权重保存后刷新
+
+## 问题 / 场景
+
+- 管理端「排名算法」弹窗修改权重并保存后,内容排行榜列表和排序不更新。
+- 原因:`handleSaveRankingWeights` 成功时只调用了 `loadList()`(章节树),未调用 `loadRanking()`(内容排行榜)。
+
+## 解决方案
+
+- 保存成功后:
+ 1. `setShowRankingAlgorithmModal(false)` 关闭弹窗
+ 2. `loadList()` 刷新章节树(含 hotScore)
+ 3. `loadRanking()` 刷新内容排行榜
+
+## 代码位置
+
+- `soul-admin/src/pages/content/ContentPage.tsx`:`handleSaveRankingWeights`
diff --git a/.cursor/agent/管理端开发工程师/evolution/索引.md b/.cursor/agent/管理端开发工程师/evolution/索引.md
index c7c0393d..50202159 100644
--- a/.cursor/agent/管理端开发工程师/evolution/索引.md
+++ b/.cursor/agent/管理端开发工程师/evolution/索引.md
@@ -3,6 +3,7 @@
| 日期 | 摘要 | 文件 |
|------|------|------|
| 2026-03-12 | ContentPage TypeScript 严格类型修复;关联小程序 key、@ 人物 token 设计 | [2026-03-12.md](./2026-03-12.md) |
+| 2026-03-14 | 排名算法权重保存后 loadRanking 刷新、关闭弹窗 | [2026-03-14.md](./2026-03-14.md) |
| 2026-03-05 | 分支合并后全功能自测,404/异常接口记录 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-05 | 文章详情@某人:编辑页插入 @用户、保存约定 content 格式 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-10 | 管理端迁移 Mycontent-temp 菜单/布局:主导航收敛、Settings Tab 承载 author/admin | [2026-03-10.md](./2026-03-10.md) |
diff --git a/.cursor/skills/miniprogram-dev/SKILL.md b/.cursor/skills/miniprogram-dev/SKILL.md
index bd180f45..2caeda60 100644
--- a/.cursor/skills/miniprogram-dev/SKILL.md
+++ b/.cursor/skills/miniprogram-dev/SKILL.md
@@ -84,7 +84,15 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
---
-## 9. 何时使用本 Skill
+## 9. 文本可选与复制(阅读类内容)
+
+- **长按选中复制**:需支持长按选中的文本用 `...`(基础库 2.12.1+,`selectable` 已废弃)。
+- **适用**:章节标题、正文段落、@ 提及、# 链接标签、预览内容等。
+- **注意**:`user-select` 会使 text 显示为 inline-block,若布局异常可回退 `selectable`;iOS 原生选中失效时可用 `bindlongpress` + `wx.setClipboardData` 做整段复制兜底。
+
+---
+
+## 10. 何时使用本 Skill
- 在 **miniprogram/** 下新增或修改页面、组件、utils 时。
- 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。
@@ -92,5 +100,6 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
- 做登录、手机号、推荐码等涉及用户信息的授权时(遵循 §8 隐私按需授权)。
- 做表单、input/textarea 样式时(遵循 §6,用 view 包裹,padding 写在 view 上)。
- 做个人中心、设置页布局时(遵循 §7,卡片区边距 16rpx)。
+- 做阅读、文章等需长按复制的文本时(遵循 §9,text 加 user-select)。
遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。
diff --git a/miniprogram/app.js b/miniprogram/app.js
index 336ad0eb..11b174af 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -217,16 +217,16 @@ App({
return isDefaultNickname || noAvatar
},
- // 登录后若资料未完善,引导跳转到资料编辑页
+ // 登录后若资料未完善,引导跳转到头像昵称引导页
_ensureProfileCompletedAfterLogin(user) {
try {
if (!user || !this._isProfileIncomplete(user)) return
const pages = getCurrentPages()
const current = pages[pages.length - 1]
- // 避免在资料页内重复跳转
- if (current && current.route === 'pages/profile-edit/profile-edit') return
+ // 避免在头像昵称页或资料编辑页内重复跳转
+ if (current && (current.route === 'pages/avatar-nickname/avatar-nickname' || current.route === 'pages/profile-edit/profile-edit')) return
wx.showToast({ title: '请先完善头像和昵称', icon: 'none', duration: 2000 })
- wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
+ wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
} catch (e) {
console.warn('[App] 跳转资料编辑页失败:', e)
}
diff --git a/miniprogram/app.json b/miniprogram/app.json
index 2912c1ff..49f1b392 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -21,7 +21,8 @@
"pages/mentors/mentors",
"pages/mentor-detail/mentor-detail",
"pages/profile-show/profile-show",
- "pages/profile-edit/profile-edit"
+ "pages/profile-edit/profile-edit",
+ "pages/avatar-nickname/avatar-nickname"
],
"window": {
"backgroundTextStyle": "light",
diff --git a/miniprogram/pages/avatar-nickname/avatar-nickname.js b/miniprogram/pages/avatar-nickname/avatar-nickname.js
new file mode 100644
index 00000000..a344a603
--- /dev/null
+++ b/miniprogram/pages/avatar-nickname/avatar-nickname.js
@@ -0,0 +1,155 @@
+/**
+ * Soul创业派对 - 头像昵称引导页
+ * 登录后资料未完善时引导用户修改默认头像和昵称,仅包含头像+昵称两项
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ avatar: '',
+ nickname: '',
+ saving: false,
+ showAvatarModal: false,
+ },
+
+ onLoad() {
+ this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
+ this.loadFromUser()
+ },
+
+ loadFromUser() {
+ const user = app.globalData.userInfo
+ if (!app.globalData.isLoggedIn || !user?.id) {
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ setTimeout(() => getApp().goBackOrToHome(), 1500)
+ return
+ }
+ const nickname = (user.nickname || user.nickName || '').trim()
+ const avatar = user.avatar || user.avatarUrl || ''
+ this.setData({ nickname, avatar })
+ },
+
+ goBack() {
+ getApp().goBackOrToHome()
+ },
+
+ onNicknameInput(e) {
+ this.setData({ nickname: e.detail.value })
+ },
+ onNicknameChange(e) {
+ this.setData({ nickname: e.detail.value })
+ },
+
+ onAvatarTap() {
+ wx.showActionSheet({
+ itemList: ['使用微信头像', '从相册选择'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ this.setData({ showAvatarModal: true })
+ } else if (res.tapIndex === 1) {
+ this.chooseAvatarFromAlbum()
+ }
+ },
+ })
+ },
+
+ closeAvatarModal() {
+ this.setData({ showAvatarModal: false })
+ },
+
+ chooseAvatarFromAlbum() {
+ wx.chooseMedia({
+ count: 1,
+ mediaType: ['image'],
+ sourceType: ['album', 'camera'],
+ success: async (res) => {
+ const tempPath = res.tempFiles[0].tempFilePath
+ await this.uploadAndSaveAvatar(tempPath)
+ },
+ })
+ },
+
+ async onChooseAvatar(e) {
+ const tempAvatarUrl = e.detail?.avatarUrl
+ this.setData({ showAvatarModal: false })
+ if (!tempAvatarUrl) return
+ await this.uploadAndSaveAvatar(tempAvatarUrl)
+ },
+
+ async uploadAndSaveAvatar(tempPath) {
+ wx.showLoading({ title: '上传中...', mask: true })
+ try {
+ const uploadRes = await new Promise((resolve, reject) => {
+ wx.uploadFile({
+ url: app.globalData.baseUrl + '/api/miniprogram/upload',
+ filePath: tempPath,
+ name: 'file',
+ formData: { folder: 'avatars' },
+ success: (r) => {
+ try {
+ const data = JSON.parse(r.data)
+ if (data.success) resolve(data)
+ else reject(new Error(data.error || '上传失败'))
+ } catch {
+ reject(new Error('解析失败'))
+ }
+ },
+ fail: reject,
+ })
+ })
+ const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
+ this.setData({ avatar: avatarUrl })
+ await app.request({
+ url: '/api/miniprogram/user/profile',
+ method: 'POST',
+ data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
+ })
+ if (app.globalData.userInfo) {
+ app.globalData.userInfo.avatar = avatarUrl
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+ wx.hideLoading()
+ wx.showToast({ title: '头像已更新', icon: 'success' })
+ } catch (e) {
+ wx.hideLoading()
+ wx.showToast({ title: e.message || '上传失败', icon: 'none' })
+ }
+ },
+
+ async saveProfile() {
+ const userId = app.globalData.userInfo?.id
+ if (!userId) {
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ return
+ }
+ const nickname = (this.data.nickname || '').trim()
+ const avatar = (this.data.avatar || '').trim()
+ if (!nickname) {
+ wx.showToast({ title: '请输入昵称', icon: 'none' })
+ return
+ }
+ this.setData({ saving: true })
+ try {
+ await app.request({
+ url: '/api/miniprogram/user/profile',
+ method: 'POST',
+ data: { userId, avatar: avatar || undefined, nickname },
+ })
+ if (app.globalData.userInfo) {
+ if (nickname) app.globalData.userInfo.nickname = nickname
+ if (avatar) app.globalData.userInfo.avatar = avatar
+ wx.setStorageSync('userInfo', app.globalData.userInfo)
+ }
+ wx.showToast({ title: '保存成功', icon: 'success' })
+ setTimeout(() => getApp().goBackOrToHome(), 800)
+ } catch (e) {
+ wx.showToast({ title: e.message || '保存失败', icon: 'none' })
+ }
+ this.setData({ saving: false })
+ },
+
+ goToFullProfile() {
+ wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
+ },
+})
diff --git a/miniprogram/pages/avatar-nickname/avatar-nickname.json b/miniprogram/pages/avatar-nickname/avatar-nickname.json
new file mode 100644
index 00000000..4d1ce1a1
--- /dev/null
+++ b/miniprogram/pages/avatar-nickname/avatar-nickname.json
@@ -0,0 +1,4 @@
+{
+ "navigationBarTitleText": "完善资料",
+ "usingComponents": {}
+}
diff --git a/miniprogram/pages/avatar-nickname/avatar-nickname.wxml b/miniprogram/pages/avatar-nickname/avatar-nickname.wxml
new file mode 100644
index 00000000..a0bf5c5c
--- /dev/null
+++ b/miniprogram/pages/avatar-nickname/avatar-nickname.wxml
@@ -0,0 +1,67 @@
+
+
+
+ ‹
+ 完善资料
+
+
+
+
+
+
+
+ 👋
+ 完善头像和昵称
+ 让他人更好地认识你,展示更专业的形象
+
+
+
+
+
+
+
+ {{nickname ? nickname[0] : '?'}}
+
+ 📷
+
+ 点击更换头像
+
+
+
+
+ 昵称
+
+
+
+ 微信用户可点击输入框自动填充昵称,或手动输入
+
+
+
+ {{saving ? '保存中...' : '完成'}}
+
+
+
+ 完善更多资料
+ →
+
+
+
+
+
+
+ ✕
+ 使用微信头像
+ 点击下方按钮,一键同步当前微信头像
+
+ 取消
+
+
+
diff --git a/miniprogram/pages/avatar-nickname/avatar-nickname.wxss b/miniprogram/pages/avatar-nickname/avatar-nickname.wxss
new file mode 100644
index 00000000..83b02442
--- /dev/null
+++ b/miniprogram/pages/avatar-nickname/avatar-nickname.wxss
@@ -0,0 +1,271 @@
+/* Soul创业派对 - 头像昵称引导页 */
+.page {
+ background: #050B14;
+ min-height: 100vh;
+ color: #fff;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 44px;
+ padding: 0 24rpx;
+ background: rgba(5, 11, 20, 0.9);
+ backdrop-filter: blur(8rpx);
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
+}
+.nav-back {
+ width: 60rpx;
+ padding: 16rpx 0;
+}
+.back-icon {
+ font-size: 44rpx;
+ color: #5EEAD4;
+}
+.nav-title {
+ font-size: 36rpx;
+ font-weight: 600;
+}
+.nav-placeholder {
+ width: 60rpx;
+}
+
+.content {
+ padding: 32rpx 24rpx;
+ box-sizing: border-box;
+}
+
+.guide-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: 48rpx 32rpx;
+ background: rgba(94, 234, 212, 0.08);
+ border: 1rpx solid rgba(94, 234, 212, 0.25);
+ border-radius: 24rpx;
+ margin-bottom: 48rpx;
+}
+.guide-icon {
+ font-size: 64rpx;
+ margin-bottom: 16rpx;
+}
+.guide-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #5EEAD4;
+ margin-bottom: 12rpx;
+}
+.guide-desc {
+ font-size: 26rpx;
+ color: rgba(148, 163, 184, 0.95);
+ line-height: 1.5;
+}
+
+.avatar-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 48rpx;
+}
+.avatar-wrap {
+ position: relative;
+ width: 192rpx;
+ height: 192rpx;
+ border-radius: 50%;
+ border: 4rpx solid #5EEAD4;
+ box-shadow: 0 0 30rpx rgba(94, 234, 212, 0.3);
+}
+.avatar-inner {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ overflow: hidden;
+}
+.avatar-img {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+.avatar-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 72rpx;
+ font-weight: bold;
+ color: #5EEAD4;
+ background: rgba(94, 234, 212, 0.2);
+}
+.avatar-camera {
+ position: absolute;
+ bottom: -8rpx;
+ right: -8rpx;
+ width: 56rpx;
+ height: 56rpx;
+ background: #5EEAD4;
+ color: #000;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ border: 4rpx solid #050B14;
+ box-sizing: border-box;
+}
+.avatar-change {
+ font-size: 28rpx;
+ color: #5EEAD4;
+ font-weight: 500;
+ margin-top: 16rpx;
+}
+
+.form-section {
+ margin-bottom: 32rpx;
+}
+.form-label {
+ display: block;
+ font-size: 24rpx;
+ color: #94A3B8;
+ margin-bottom: 12rpx;
+ margin-left: 8rpx;
+}
+.form-input-wrap {
+ padding: 24rpx 32rpx;
+ background: #17212F;
+ border: 1rpx solid rgba(255, 255, 255, 0.08);
+ border-radius: 24rpx;
+ box-sizing: border-box;
+}
+.form-input-inner {
+ width: 100%;
+ font-size: 28rpx;
+ color: #fff;
+ background: transparent;
+ box-sizing: border-box;
+ display: block;
+}
+.input-tip {
+ margin-top: 8rpx;
+ font-size: 22rpx;
+ color: #94A3B8;
+ margin-left: 8rpx;
+ display: block;
+}
+
+.save-btn {
+ width: 100%;
+ height: 96rpx;
+ line-height: 96rpx;
+ text-align: center;
+ background: #5EEAD4;
+ color: #050B14;
+ font-size: 36rpx;
+ font-weight: bold;
+ border-radius: 24rpx;
+ margin-top: 24rpx;
+ margin-bottom: 32rpx;
+}
+.save-btn[disabled] {
+ opacity: 0.6;
+}
+
+.link-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8rpx;
+ padding: 24rpx;
+}
+.link-text {
+ font-size: 28rpx;
+ color: #94A3B8;
+}
+.link-arrow {
+ font-size: 28rpx;
+ color: #5EEAD4;
+}
+
+/* 头像弹窗 */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(16rpx);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: 48rpx;
+ box-sizing: border-box;
+}
+.modal-content {
+ width: 100%;
+ max-width: 640rpx;
+ background: #0b1220;
+ border-radius: 32rpx;
+ padding: 48rpx;
+ position: relative;
+ box-sizing: border-box;
+}
+.modal-close {
+ position: absolute;
+ top: 24rpx;
+ right: 24rpx;
+ width: 56rpx;
+ height: 56rpx;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.08);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.7);
+}
+.avatar-modal-title {
+ display: block;
+ font-size: 36rpx;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 12rpx;
+}
+.avatar-modal-desc {
+ display: block;
+ font-size: 26rpx;
+ color: #94A3B8;
+ text-align: center;
+ margin-bottom: 32rpx;
+}
+.btn-choose-avatar {
+ width: 100%;
+ height: 88rpx;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #5EEAD4;
+ color: #050B14;
+ font-size: 30rpx;
+ font-weight: 600;
+ border-radius: 44rpx;
+ border: none;
+}
+.btn-choose-avatar::after {
+ border: none;
+}
+.avatar-modal-cancel {
+ margin-top: 24rpx;
+ text-align: center;
+ font-size: 28rpx;
+ color: #9CA3AF;
+}
diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js
index 994bf21e..411efa11 100644
--- a/miniprogram/pages/index/index.js
+++ b/miniprogram/pages/index/index.js
@@ -482,49 +482,24 @@ Page({
wx.switchTab({ url: '/pages/match/match' })
},
+ // 最新新增:用 latest-chapters 接口(后端按 updated_at 取前 N 条),不拉全量,支持万级文章
async loadLatestChapters() {
try {
- const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
- const chapters = (res && res.data) || (res && res.chapters) || []
+ const res = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
+ const list = (res && res.data) ? res.data : []
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
- // stitch_soul:优先取 isNew 标记的章节;若无则取最近更新的前 10 章(排除序言/尾声/附录)
- let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
- if (candidates.length === 0) {
- candidates = chapters.filter(exclude)
- }
- // 解析「第X场」用于倒序,最新(场次大)放在最上方
- const sessionNum = (c) => {
- const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
- const m = title.match(/第\s*(\d+)\s*场/) || title.match(/第(\d+)场/)
- if (m) return parseInt(m[1], 10)
- const id = c.id != null ? String(c.id) : ''
- if (/^\d+$/.test(id)) return parseInt(id, 10)
- return 0
- }
- const latest = candidates
- .sort((a, b) => {
- const na = sessionNum(a)
- const nb = sessionNum(b)
- if (na !== nb) return nb - na // 场次倒序:最新在上
- return new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0)
- })
+ const latest = list
+ .filter(exclude)
.slice(0, 10)
.map(c => {
const d = new Date(c.updatedAt || c.updated_at || Date.now())
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
- const rawContent = (c.content || '').replace(/<[^>]+>/g, '').trim()
- // 描述仅用正文摘要,避免 #id 或标题重复;截取 36 字
- let desc = ''
- if (rawContent && rawContent.length > 0) {
- const clean = rawContent.replace(/^#[\d.]+\s*/, '').trim()
- desc = clean.length > 36 ? clean.slice(0, 36) + '...' : clean
- }
return {
id: c.id,
mid: c.mid ?? c.MID ?? 0,
title,
- desc,
+ desc: '', // latest-chapters 不返回 content,避免大表全量加载
price: c.price ?? 1,
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
}
diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js
index 171e48d8..e6954732 100644
--- a/miniprogram/pages/my/my.js
+++ b/miniprogram/pages/my/my.js
@@ -5,6 +5,7 @@
*/
const app = getApp()
+const { formatStatNum } = require('../../utils/util.js')
Page({
data: {
@@ -28,6 +29,9 @@ Page({
// 阅读统计
totalReadTime: 0,
matchHistory: 0,
+ readCountText: '0',
+ totalReadTimeText: '0',
+ matchHistoryText: '0',
// 最近阅读
recentChapters: [],
@@ -128,25 +132,32 @@ Page({
earningsLoading: true,
recentChapters: [],
totalReadTime: 0,
- matchHistory: 0
+ matchHistory: 0,
+ readCountText: '0',
+ totalReadTimeText: '0',
+ matchHistoryText: '0'
})
this.loadDashboardStats()
this.loadMyEarnings()
this.loadPendingConfirm()
this.loadVipStatus()
} else {
+ const guestReadCount = app.getReadCount()
this.setData({
isLoggedIn: false,
userInfo: null,
userIdShort: '',
- readCount: app.getReadCount(),
+ readCount: guestReadCount,
+ readCountText: formatStatNum(guestReadCount),
referralCount: 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: false,
recentChapters: [],
totalReadTime: 0,
- matchHistory: 0
+ matchHistory: 0,
+ totalReadTimeText: '0',
+ matchHistoryText: '0'
})
}
},
@@ -175,10 +186,16 @@ Page({
}))
: []
+ const readCount = Number(res.data.readCount || 0)
+ const totalReadTime = Number(res.data.totalReadMinutes || 0)
+ const matchHistory = Number(res.data.matchHistory || 0)
this.setData({
- readCount: Number(res.data.readCount || 0),
- totalReadTime: Number(res.data.totalReadMinutes || 0),
- matchHistory: Number(res.data.matchHistory || 0),
+ readCount,
+ totalReadTime,
+ matchHistory,
+ readCountText: formatStatNum(readCount),
+ totalReadTimeText: formatStatNum(totalReadTime),
+ matchHistoryText: formatStatNum(matchHistory),
recentChapters
})
} catch (e) {
diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml
index 80371793..cfb60ee8 100644
--- a/miniprogram/pages/my/my.wxml
+++ b/miniprogram/pages/my/my.wxml
@@ -46,7 +46,7 @@
- {{readCount}}
+ {{readCountText}}
已读章节
@@ -94,17 +94,17 @@
- {{readCount}}
+ {{readCountText}}
已读章节
- {{totalReadTime}}
+ {{totalReadTimeText}}
阅读分钟
- {{matchHistory}}
+ {{matchHistoryText}}
匹配伙伴
@@ -154,7 +154,7 @@
-