2026年3月14日14:37:06

This commit is contained in:
Alex-larget
2026-03-14 14:37:17 +08:00
parent db4b4b8b87
commit 8778a42429
452 changed files with 92375 additions and 1014 deletions

View File

@@ -0,0 +1,61 @@
# 后端工程师 经验记录 - 2026-03-12
## 1. persons 表 token 字段与 DB 迁移
### 问题
新增 @ 人物时报错:`Unknown column 'token' in 'field list'`。GORM model 已加 `Token` 字段,但数据库未执行迁移。
### 解决方案
- **迁移脚本**`soul-api/scripts/add-persons-token.sql`
- **执行**`node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add-persons-token.sql`
- **内容**`ALTER TABLE persons ADD COLUMN token VARCHAR(36) NOT NULL DEFAULT '' AFTER person_id` + 唯一索引
### 规则
- **Model 新增字段后**:需编写并执行 ALTER 脚本GORM AutoMigrate 不一定自动生效(取决于启动时机与连接)
- **迁移脚本位置**`soul-api/scripts/`,命名 `add-xxx.sql`
- **执行方式**db-exec 脚本读取 soul-api/.env 的 DB_DSN
---
## 2. CKBLead 用 token 兑换真实密钥
- `targetUserId` 现为 persons.token非 person_id
- 查询:`db.Where("token = ?", body.TargetUserID).First(&p)`
-`p.CkbApiKey` 调用存客宝
---
## 3. 9.9 买断与后端开关hasFullBook
### 场景
- 小程序已通过 `hasFullBook` 标识「买断全书」,权限判断和文案都依赖该字段(由 `/api/miniprogram/user/purchase-status``/api/miniprogram/user/check-purchased` 返回)。
- 现在需要在用户资料里增加一个布尔开关:运维/客服手动打开后,相当于该用户已经买过 9.9,全书可看,后续不再需要支付。
### 设计要点
- **统一事实来源**9.9 买断是否生效完全由后端计算,前端只认:
- `purchase-status` 返回的 `hasFullBook = true`
-`check-purchased` 返回 `isPurchased = true``reason = "has_full_book"`
- **用户资料开关**
-`users` 表或 user profile 中新增布尔字段(例如 `manual_fullbook`,具体命名按现有规范调整)。
- 仅管理端/运维修改该字段,小程序不直接写入。
- **接口契约调整**
- `/api/miniprogram/user/purchase-status`
- 计算 `hasFullBook` 时,将订单表中的全书订单结果与 `manual_fullbook`**OR**,只要任一为真就返回 `hasFullBook = true`
- `/api/miniprogram/user/check-purchased`
- 对章节做权限判断时,如果由 `manual_fullbook` 推导出可看,应返回:
- `isPurchased = true`
- `reason = "has_full_book"`
- 前端的 `chapterAccessManager.syncLocalCache` 会据此把 `app.globalData.hasFullBook = true` 并同步到 `userInfo.hasFullBook`
- **与 VIP 的边界**
- `hasFullBook`9.9 买断)与 `isVip`(会员)继续解耦:手动开 fullbook 开关不会自动授予 VIP。
- VIP 相关逻辑只看 `isVip` / `vipExpireDate`,不受 `manual_fullbook` 影响。
### 规则
- 不在前端增加「跳过支付」开关,所有免 9.9 行为都通过后端折叠到 `hasFullBook/has_full_book` 暴露给小程序。
- 满足以上约定后,小程序现有代码无需修改即可支持「后台手动赠送 9.9 买断」。

View File

@@ -0,0 +1,45 @@
# 2026-03-13 - 文章详情预览统一与内容安全
## 问题 / 场景
- 文章详情目前小程序侧只展示约 20% 内容作为预览,再引导用户付费解锁。
- 历史实现中前端本地按 20% 计算预览,后端曾同时返回外层 `content`(预览)和 `data.content`(全文),存在「接口约定不统一」和「误用 data.content 泄露全文」的风险。
- 需求:统一由后端按业务规则截取预览(改为 50%),小程序只按「是否已付费」选择用预览还是全文;未付费时,无论字段层级都不能拿到全文。
## 解决方案
-`internal/handler/book.go` 中调整章节预览逻辑:
- `previewContent` 改为按字符数取正文前 50%`total/2`),同时保证预览不少于 100 个字符;
- 预览结尾统一追加 `……(购买后阅读完整内容)` 作为提示文案。
-`findChapterAndRespond` 中统一内容返回策略:
- 先根据 system_config.free_chapters / chapter_config.freeChapters / chapters.is_free / price 判断章节是否免费;
- 免费章节:`returnContent = ch.Content`(全文);
- 付费章节:
-`checkUserChapterAccess` 判断用户已购买 / VIP / 全书:`returnContent = ch.Content`(全文);
- 否则:`returnContent = previewContent(ch.Content)`(仅预览);
- 构造响应时,将 `chForResponse.Content = returnContent`,并通过:
- 外层 `content: returnContent`
- 内层 `data: chForResponse`(其中 `content` 也为 `returnContent`
- 确保未授权用户在任意字段上都拿不到完整正文。
## 与前端的接口约定
- 小程序阅读页通过 `userId` 查询章节详情,`accessManager` 基于返回的章节信息与用户购买状态计算 `accessState`
-`accessState``free``unlocked_purchased` 时,前端使用 `res.data.content ?? res.content` 渲染全文;
-`accessState` 为未登录 / 未购买时,前端只使用 `res.content` 渲染预览。
- 预览比例完全由后端控制(当前为 50%),小程序不再自行用 20% 做二次截断,只是把后端提供的预览完整展示出来。
## 代码位置
- 后端:
- `soul-api/internal/handler/book.go`
- 小程序(前端配合):
- `miniprogram/pages/read/read.js`
- `miniprogram/pages/read/read.wxml`
## 对后续开发的约定
- 预览长度(包括比例、最小字符数、提示文案)统一由后端控制;如需调整比例,只需修改 `previewContent`,保持接口字段含义不变。
- 任何需要「只返回部分内容预览」的场景,应优先复用「外层 `content` + 内层 `data.content` 保持一致」的安全模式,避免在不同字段中混放全文与预览内容。
- 涉及付费内容时,优先在后端用「权限判断 + 统一内容裁剪」实现安全边界,前端只根据状态选择展示预览还是全文。

View File

@@ -5,3 +5,4 @@
| 2026-03-05 | soul-api 合并状态确认orders、distribution 接口核对 | [2026-03-05.md](./2026-03-05.md) |
| 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) |

View File

@@ -0,0 +1,47 @@
# 团队共享 经验记录 - 2026-03-12
## 密钥/token 设计:关联小程序与 @ 人物
### 背景
- **关联小程序**:添加时生成 32 位英文+数字密钥,链接标签存 key小程序点击 # 时用 key 查 appId 再跳转
- **@ 人物**:添加时生成 32 位 token文章 @ 时存 token小程序点击 @ 时用 token 兑换真实 ckb_api_key 后加好友
### 设计原则
1. **不暴露真实密钥**管理端配置的真实密钥appId、ckb_api_key不写入文章内容仅存可对外传递的 token/key
2. **服务端兑换**:小程序端只传 token/key后端用其查表得到真实密钥再调用第三方
3. **不兼容旧数据**:项目未上架,无需兼容 person_id、appId 等旧格式
### 数据流
| 场景 | 添加时 | 内容存储 | 小程序点击 | 后端兑换 |
|------|--------|----------|------------|----------|
| 关联小程序 | 生成 key | data-mp-key | 传 mpKey | 查 linked_miniprograms 得 appId |
| @ 人物 | 生成 token | data-id | 传 targetUserId | 查 persons 得 ckb_api_key |
### 适用角色
- 后端persons.token、linked_miniprograms.key、CKBLead 兑换逻辑
- 管理端:链接标签选 key、@ 人物 id=token、列表展示
- 小程序contentParser 解析 mpKey、onLinkTagTap/onMentionTap 传 key/token
---
## 9.9 买断与团队级约定
### 结论
- 「9.9 买断全书」在三端的唯一来源是后端的 `hasFullBook`/`has_full_book` 信号:
- 小程序:只看 `/user/purchase-status``hasFullBook``/user/check-purchased``reason = "has_full_book"`
- 后端:可以通过订单或用户资料开关(如 `manual_fullbook`)计算该状态。
- 管理端:如需赠送 9.9 买断,应只改后端用户资料中的开关,不直接改前端逻辑。
### 约定
- 任意「免 9.9」或「赠送全书」能力都必须:
- 由后端在 purchase-status/check-purchased 中折叠为统一的 `hasFullBook/has_full_book`
- 不允许在小程序或管理端各自新增本地开关绕过后端逻辑。
- VIP 与 9.9 买断继续分离:
- `hasFullBook` → 只代表书的权限;
- `isVip` → 代表会员权益(案例库、增值版等),两者可以独立打开。

View File

@@ -0,0 +1,42 @@
# 2026-03-13 - 文章详情预览规则统一(小程序 + 后端)
## 场景
- 文章详情页采用「部分内容预览 + 付费解锁全文」的收费模式。
- 历史实现中,预览比例由小程序本地按 20% 行数截取,后端可能同时返回预览与全文两个 content 字段,存在:
- 三端对「预览长度」的理解不一致;
- 前端误用 `data.content` 导致未付费用户拿到全文的潜在风险。
- 本次目标:将「预览长度 + 安全边界」下沉到后端统一控制,小程序只按「是否已解锁」选择展示预览或全文。
## 团队级决策
- 预览长度统一由 **soul-api** 控制:
- 当前规则:对付费章节,未解锁用户默认看到正文前 **50%**,且不少于 100 个字符,末尾追加「购买后阅读完整内容」提示;
- 未来如需改为 40% / 30% 等,只需调整后端 `previewContent`,前端逻辑保持不变。
- 接口约定统一:
- 章节详情接口(含 `/api/miniprogram/book/chapter/:id``/api/miniprogram/book/chapter/by-mid/:mid`)返回的:
- 外层 `content` 字段,
- 内层 `data.content`Chapter.Content字段
- **在同一次响应中必须保持一致**
- 免费或已解锁:两处都是全文;
- 未解锁两处都是预览50%)。
- 不允许出现「外层是预览、`data.content` 是全文」这类混合返回,避免前端误用泄露付费内容。
- 小程序阅读页约定:
- 只根据 `accessState` 判断是否有权看全文;
- 有权限时使用 `data.content` / `content` 渲染全文;
- 无权限时仅使用后端返回的预览内容,不再在前端重新按 20% 行数切割。
## 影响角色
- **后端工程师soul-api**
- 负责实现和维护 `previewContent` 预览算法及权限判断逻辑;
- 在新增/修改与付费内容相关的接口时,必须遵守「外层 content 与 data.content 一致」的安全约定。
- **小程序开发工程师**
- 阅读页不自行决定预览比例,只展示后端返回的预览内容;
- 通过 `accessManager` 与章节详情响应判断权限,严格按照 `accessState` 切换「预览 / 全文」视图。
## 对后续迭代的提示
- 若未来引入「增值版内容」「章节试读长度差异化」等新收费形态,优先在后端扩展 `previewContent` 与权限判断逻辑,对前端暴露统一、稳定的字段语义。
- 若管理端需要控制预览比例或提示文案,可在配置中心增加相关配置项,由后端读取后影响 `previewContent` 行为,保持三端一致。

View File

@@ -6,3 +6,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) |

View File

@@ -0,0 +1,15 @@
# 小程序开发工程师 经验记录 - 2026-03-12
## 链接标签与 @ 人物key/token 兑换
### 链接标签(# 跳转小程序)
- **contentParser**:解析 `data-mp-key`seg.mpKey 传给 wxml
- **onLinkTagTap**`mpKey` 从 dataset`linkedMiniprograms.find(m => m.key === mpKey)` 查 appId
- **config 预加载**read.js onLoad 时拉取 `linkTags``linkedMiniprograms` 存 globalData
### @ 人物(加好友)
- **contentParser**mention 的 `data-id` 为 tokenseg.userId = token
- **onMentionTap**`targetUserId` 传 tokenCKBLead 接口用 token 兑换 ckb_api_key
- 无需在 config 中拉取 personstoken 已嵌入 content

View File

@@ -7,3 +7,4 @@
| 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) |

View File

@@ -30,9 +30,17 @@
| 2026-03-10 | 后端 | bug 修复 | api-dev SKILL | 聚合接口三处修复recentChapters 去重、totalReadMinutes 最小1分钟、DB 错误返回 500 |
| 2026-03-10 | 团队 | 方法论 | - | 新旧版代码对比:以功能完整性为基准,批量 diff + 分类取舍,不以日期判优劣 |
| 2026-03-10 | 管理端 | 最佳实践 | admin-dev SKILL §toast | Toast 通知系统落地utils/toast.ts 纯原生实现,全系统 18 文件 alert → toast 批量替换 |
| 2026-03-12 | 管理端 | bug 修复 | admin-dev SKILL §ts | ContentPage TypeScript 严格类型:可选字段 ?? 兜底、接口补字段、API 映射、setState 传参 |
| 2026-03-12 | 团队 | 架构/设计 | - | 密钥/token 设计:关联小程序 key、@ 人物 token不暴露真实密钥、服务端兑换 |
| 2026-03-12 | 后端 | 最佳实践 | mysql-direct SKILL | persons token 字段Model 新增字段后需执行 ALTER 迁移脚本 |
| 2026-03-12 | 管理端 | 设计落地 | - | 关联小程序存 key、@ 人物 id=token链接标签与 PersonItem 约定 |
| 2026-03-12 | 小程序 | 设计落地 | - | contentParser mpKey/tokenonLinkTagTap、onMentionTap 传 key/token 兑换 |
| 2026-03-10 | 后端 | bug 修复 | api-dev SKILL | chapters 表新增 hot_score 列,修复 1054 Unknown column 错误DB 变更 SOPALTER→model→重启 |
| 2026-03-10 | 团队 | 方法论 | - | DB 变更 SOP + Toast 批量替换方法论(脚本替换后必须人工复查 toast.info 语义) |
| 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 始终一致以避免泄露全文 |
---
## 已吸收经验(历史)
@@ -42,4 +50,4 @@
---
**最后更新**2026-03-10Toast + hot_score 经验入库
**最后更新**2026-03-13文章预览规则统一、小程序与后端对齐

View File

@@ -24,9 +24,12 @@ soul-apiGo + Gin + GORM + MySQL提供三组路由`/api/miniprogram/*`
| 2026-03-10 | 新增 GET /api/miniprogram/user/dashboard-stats聚合阅读统计接口含去重、min1分钟、500错误码修复 | 已完成 |
| 2026-03-10 | chapters 表新增 hot_score 列ALTER TABLE + model 同步),修复前端保存章节报 1054 错误 | 已完成 |
| 2026-03-11 | users 迁移仅 VIP 身份/状态五字段,不再新增 VIP 资料列chapters 补 hot_scoresync 脚本与 README-schema-sync 更新 | 已完成 |
| 2026-03-12 | persons 表新增 token 字段add-persons-token.sqlCKBLead 用 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 一致,未授权只返回预览 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-11
**最后更新**2026-03-13

View File

@@ -21,9 +21,12 @@ Soul 创业派对全项目架构与约定路由隔离miniprogram/admin/db
| 2026-03-10 | 新旧版代码对比方法论:以功能完整性而非日期判断优劣,批量 diff + 分类取舍 + 移植修复 | 已完成 |
| 2026-03-11 | 以界面定需求文档建立;需求基准与业务逻辑对齐(用户/VIP 资料、三端路由);会议收尾纪要与各角色经验入库 | 已完成 |
| 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 始终一致 | 已完成 |
> **格式说明**:每次架构级讨论后在此追加一行,日期格式 YYYY-MM-DD
---
**最后更新**2026-03-11
**最后更新**2026-03-13

View File

@@ -27,9 +27,11 @@
| 2026-03-10 | 移植 loadDashboardStats()my.js 阅读统计改为后端接口,去除随机数时间/标题占位 | 已完成 |
| 2026-03-10 | 富文本渲染技术债分析contentParser.js 剥除 HTML 格式,建议改 rich-text 组件(待实施) | 待续 |
| 2026-03-11 | 以界面定需求:小程序界面清单纳入《以界面定需求》;展示以用户资料为准,与现有实现一致 | 已完成 |
| 2026-03-12 | 链接标签 mpKey 兑换 appId@ 人物 token 兑换 ckb_api_keycontentParser、onLinkTagTap、onMentionTap | 已完成 |
| 2026-03-13 | 阅读页文章预览与付费解锁对齐:预览长度改由后端统一计算,前端按 accessState 显示预览/全文,避免 data.content 泄露全文 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-11
**最后更新**2026-03-13

View File

@@ -23,9 +23,10 @@
| 2026-03-10 | 会议:管理端迁移 Mycontent-temp 新菜单/布局主导航收敛、author/admin 并入 Settings Tab | 待续 |
| 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 | 已完成 |
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD状态用已完成 / 进行中 / 待续 / 搁置
---
**最后更新**2026-03-11
**最后更新**2026-03-12

View File

@@ -0,0 +1,43 @@
# 管理端开发工程师 经验记录 - 2026-03-12
## ContentPage TypeScript 严格类型修复
### 问题
`soul-admin` 构建时 `ContentPage.tsx` 出现多处 TS2322 类型错误:
1. **可选字段赋给必填字段**`LinkTagItem``appId``pagePath` 为可选(`string | undefined``setNewLinkTag` 期望 `string`
2. **接口缺字段**`SectionListItem``isPinned`,但 ranking API 返回该字段
3. **API 映射类型**`loadPersons``p.token``p.label``p.ckbApiKey` 可能为 `undefined`,映射后 `PersonItem.id` 等需为 `string`
4. **可选参数传 setState**`setEditingPersonKey(p.personId)``personId` 可选setter 期望 `string | null`
### 解决方案
| 场景 | 写法 |
|------|------|
| 可选字段 → 必填 string | `t.appId ?? ''``t.pagePath ?? ''` |
| 接口补字段 | 在 `SectionListItem` 添加 `isPinned?: boolean` |
| API 映射兜底 | `id: p.token ?? p.personId ?? ''``label: p.label ?? ''``ckbApiKey: p.ckbApiKey ?? ''` |
| 可选 → setState(string\|null) | `setEditingPersonKey(p.personId ?? null)` |
### 规则提炼
- 从可选类型(`T | undefined`)赋给必填类型(`T`)时,用 `?? defaultValue` 兜底
- 接口类型需与 API 返回字段对齐,缺字段时补 `field?: Type`
- `useState<string | null>` 的 setter 传参时,`undefined` 需显式转为 `null`
---
## 关联小程序与 @ 人物:密钥/token 设计
### 关联小程序
- 添加时生成 32 位 key链接标签选择小程序时存 key非 appId
- 列表展示名称、密钥、AppID、路径编辑/删除用 key
- 链接标签下拉:选项显示 name + key选中后 `appId` 字段存 key
### @ 人物
- 添加时生成 32 位 tokenPersonItem.id = tokenRichEditor 插入用)
- 列表展示 token编辑/删除用 personIdAPI 仍用 personId
- 文章 @ 时 data-id 存 token

View File

@@ -2,6 +2,7 @@
| 日期 | 摘要 | 文件 |
|------|------|------|
| 2026-03-12 | ContentPage TypeScript 严格类型修复;关联小程序 key、@ 人物 token 设计 | [2026-03-12.md](./2026-03-12.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) |