chore: 恢复开发文档全目录进库并同步阅读页与脚本改动

Made-with: Cursor
This commit is contained in:
卡若
2026-04-14 16:00:31 +08:00
parent f26c2aedc3
commit 3dd75746e6
519 changed files with 22167 additions and 8 deletions

4
.gitignore vendored
View File

@@ -10,9 +10,7 @@ __pycache__/
log/
tmp/
# 开发文档:仅「10、项目管理」进库并镜像 Gitea其余子目录与根下 md/资源仅保留在本机
开发文档/*
!开发文档/10、项目管理/
# 开发文档:全目录纳入版本库并同步 Gitea同步 GitHub 前请自行审阅敏感内容)
.obsidian/
# 二进制/压缩/临时产物

View File

@@ -78,10 +78,14 @@
<view wx:elif="{{item.length === 1 && item[0].type === 'image'}}" class="paragraph">
<image class="content-image" src="{{item[0].src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{item[0].src}}"></image>
</view>
<view wx:elif="{{item.length === 1 && item[0].type === 'video'}}" class="paragraph content-video-wrap">
<video class="content-video" src="{{item[0].src}}" controls show-center-play-btn object-fit="contain"></video>
</view>
<view wx:else class="paragraph">
<text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
<video wx:elif="{{seg.type === 'video'}}" class="content-video" src="{{seg.src}}" controls show-center-play-btn object-fit="contain"></video>
</block>
</view>
</block>
@@ -153,7 +157,8 @@
<scroll-view wx:elif="{{item.length === 1 && item[0].type === 'table'}}" scroll-x class="table-scroll"><view class="content-table"><view class="table-row table-header-row" wx:if="{{item[0].headers.length}}"><text class="table-cell table-header-cell" wx:for="{{item[0].headers}}" wx:key="index" wx:for-item="hdr">{{hdr}}</text></view><view class="table-row" wx:for="{{item[0].rows}}" wx:key="index" wx:for-item="row"><text class="table-cell" wx:for="{{row}}" wx:key="index" wx:for-item="cell">{{cell}}</text></view></view></scroll-view>
<view wx:elif="{{item.length === 1 && item[0].type === 'listItem'}}" class="seg-list-item"><text class="seg-list-marker" wx:if="{{item[0].ordered}}">{{item[0].number}}.</text><text class="seg-list-marker" wx:else>•</text><text class="seg-list-text" user-select>{{item[0].text}}</text></view>
<view wx:elif="{{item.length === 1 && item[0].type === 'image'}}" class="paragraph"><image class="content-image" src="{{item[0].src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{item[0].src}}"></image></view>
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image></block></view>
<view wx:elif="{{item.length === 1 && item[0].type === 'video'}}" class="paragraph content-video-wrap"><video class="content-video" src="{{item[0].src}}" controls show-center-play-btn object-fit="contain"></video></view>
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image><video wx:elif="{{seg.type === 'video'}}" class="content-video" src="{{seg.src}}" controls show-center-play-btn object-fit="contain"></video></block></view>
</block>
</view>
<view class="fade-mask"></view>
@@ -245,7 +250,8 @@
<scroll-view wx:elif="{{item.length === 1 && item[0].type === 'table'}}" scroll-x class="table-scroll"><view class="content-table"><view class="table-row table-header-row" wx:if="{{item[0].headers.length}}"><text class="table-cell table-header-cell" wx:for="{{item[0].headers}}" wx:key="index" wx:for-item="hdr">{{hdr}}</text></view><view class="table-row" wx:for="{{item[0].rows}}" wx:key="index" wx:for-item="row"><text class="table-cell" wx:for="{{row}}" wx:key="index" wx:for-item="cell">{{cell}}</text></view></view></scroll-view>
<view wx:elif="{{item.length === 1 && item[0].type === 'listItem'}}" class="seg-list-item"><text class="seg-list-marker" wx:if="{{item[0].ordered}}">{{item[0].number}}.</text><text class="seg-list-marker" wx:else>•</text><text class="seg-list-text" user-select>{{item[0].text}}</text></view>
<view wx:elif="{{item.length === 1 && item[0].type === 'image'}}" class="paragraph"><image class="content-image" src="{{item[0].src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{item[0].src}}"></image></view>
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image></block></view>
<view wx:elif="{{item.length === 1 && item[0].type === 'video'}}" class="paragraph content-video-wrap"><video class="content-video" src="{{item[0].src}}" controls show-center-play-btn object-fit="contain"></video></view>
<view wx:else class="paragraph"><text user-select><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image><video wx:elif="{{seg.type === 'video'}}" class="content-video" src="{{seg.src}}" controls show-center-play-btn object-fit="contain"></video></block></view>
</block>
</view>
<view class="fade-mask"></view>

View File

@@ -728,6 +728,20 @@
margin-left: 8rpx;
}
/* 正文内嵌视频(管理端 rich-video-wrap → contentParser type:video */
.content-video-wrap {
width: 100%;
margin: 24rpx 0;
box-sizing: border-box;
}
.content-video {
width: 100%;
max-width: 100%;
min-height: 360rpx;
border-radius: 12rpx;
background: #0a0f14;
}
.paywall-singlepage-note {
display: block;
margin-top: 8rpx;

View File

@@ -7,6 +7,7 @@
* { type: 'mention', userId, nickname } — @某人,点击加好友(提交存客宝见 utils/soulBridge.submitCkbLead
* { type: 'linkTag', label, url, ... } — #链接标签,点击跳转(阅读页 onLinkTagTap外链→link-preview、小程序→navigateToMiniProgram
* { type: 'image', src, alt } — 图片
* { type: 'video', src } — 正文内嵌视频(管理端 rich-video-wrap / <video>
*/
/** 判断内容是否为 HTML */
@@ -249,6 +250,24 @@ function parseHtmlToSegments(html, config) {
return '\n__Q_' + idx + '__\n'
})
// 2.5 内嵌视频去掉管理端「rich-video-caption」说明仅编辑器用整段 <video> 抽成占位
// 须在列表拆解之前执行,否则 li/段落内 video 会在 parseBlockToSegments 里被 strip 成纯文本
const videos = []
text = text.replace(/<div[^>]*\brich-video-caption\b[^>]*>[\s\S]*?<\/div>/gi, '')
text = text.replace(/<video\b[^>]*>[\s\S]*?<\/video>/gi, function (match) {
const sm = match.match(/\bsrc\s*=\s*"([^"]*)"/i) || match.match(/\bsrc\s*=\s*'([^']*)'/i)
const rawSrc = sm ? String(sm[1] || '').trim() : ''
if (!rawSrc) return match
const decoded = decodeEntities(rawSrc)
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
const idx = videos.length
videos.push({ src })
return '\n__VIDEO_' + idx + '__\n'
})
// 3. 列表 → listItem 占位(保留 ordered 标记)
var olDepth = 0
var olCounter = 0
@@ -259,6 +278,11 @@ function parseHtmlToSegments(html, config) {
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, function (_, inner) {
var cleaned = inner.replace(/<p[^>]*>/gi, '').replace(/<\/p>/gi, '').trim()
if (!cleaned) return '\n'
// 列表项内仅有视频占位时,提升为独立视频块(避免 __VIDEO_n__ 当正文展示)
var onlyVid = cleaned.match(/^__VIDEO_(\d+)__$/)
if (onlyVid) {
return '\n__VIDEO_' + onlyVid[1] + '__\n'
}
if (olDepth > 0) {
olCounter++
return '\n__LI_O_' + olCounter + '__ ' + cleaned + '\n'
@@ -292,6 +316,17 @@ function parseHtmlToSegments(html, config) {
continue
}
// video
var vidM = block.trim().match(/^__VIDEO_(\d+)__$/)
if (vidM) {
var vid = videos[parseInt(vidM[1], 10)]
if (vid && vid.src) {
lines.push('')
segments.push([{ type: 'video', src: vid.src }])
}
continue
}
// heading
var hM = block.trim().match(/^__H_(\d+)__$/)
if (hM) {

View File

@@ -8,7 +8,9 @@
SOUL_TEST_ENV=soulapi python3 scripts/content_download.py --id 10.27
python3 scripts/content_download.py 128 --out-dir /path/to/2026每日派对干货
2026 场次第102场起对应 id 10.01、10.02、…、10.27第128场
2026 每日派对章节挂在 part-2026-02 / part-2026-03 / part-2026-04 等篇章下,
业务 id如 10.34)与「第 N 场」不是简单线性公式;若按场次推算 404脚本会
自动在上述 part 里按 sectionTitle「第N场」解析真实 id。
"""
import argparse
import os
@@ -34,15 +36,40 @@ DEFAULT_BOOK_2026 = Path(
def field_to_id(field: int) -> str:
"""第 N 场(≥102)→ 10.xx第101场及以前在第九章为 9.xx。API 上第128场可能为 9.28"""
"""首猜 id≥102 10.(field-102+1)与线上一致性不保证404 时会走 resolve"""
if field >= 102:
n = field - 102 + 1
return f"10.{n:02d}"
if field >= 1:
return f"9.{field:02d}" # 第9章 9.019.99,按场次
return f"9.{field:02d}"
raise ValueError("场次请用 1999")
def resolve_2026_chapter_id_by_field(base: str, field: int, requests_mod) -> str | None:
"""在 part-2026-* 下按标题「第N场」查真实 chapters.id。"""
parts_url = f"{base}/api/miniprogram/book/parts"
pr = requests_mod.get(parts_url, timeout=30)
if pr.status_code != 200:
return None
pdata = pr.json()
parts = pdata.get("parts") or []
needle = f"{field}"
for p in parts:
pid = p.get("id") or ""
if not pid.startswith("part-2026-"):
continue
url = f"{base}/api/miniprogram/book/chapters-by-part?partId={pid}"
cr = requests_mod.get(url, timeout=30)
if cr.status_code != 200:
continue
cdata = cr.json().get("data") or []
for row in cdata:
title = row.get("sectionTitle") or ""
if needle in title:
return row.get("id")
return None
def main():
parser = argparse.ArgumentParser(description="从小程序 API 下载单章为 md")
parser.add_argument("field", nargs="?", type=int, help="场次号,如 128 表示第128场")
@@ -83,6 +110,13 @@ def main():
r = requests.get(url, timeout=30)
if r.status_code == 200:
chapter_id = fallback_id
if r.status_code == 404 and args.field is not None and not args.id:
resolved = resolve_2026_chapter_id_by_field(base, args.field, requests)
if resolved:
chapter_id = resolved
url = f"{base}/api/miniprogram/book/chapter/by-id/{chapter_id}"
print(f"404按 2026 篇章标题解析到 id: {chapter_id} | GET {url}")
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.json()
if not data.get("success"):

View File

@@ -0,0 +1,36 @@
name: Sync from Coding
on:
schedule:
- cron: '0 */2 * * *' # 每2小时执行一次
workflow_dispatch: # 允许手动触发
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write # 确保此行存在,赋予工作流写入仓库内容的权限,这是解决 403 权限问题的基础
steps:
- name: 检出 GitHub 仓库
uses: actions/checkout@v4
with:
ref: develop # 明确检出 develop 分支,确保在正确的分支上操作
- name: 配置 Git 用户并合并 Coding 代码到 GitHub
run: |
# 配置 Git 用户信息
git config user.name "zhiqun@qq.com"
git config user.email "zhiqun@qq.com"
# 添加 Coding 仓库为一个新的远程源
git remote add coding-origin https://${{ secrets.CODING_USERNAME }}:${{ secrets.CODING_TOKEN }}@e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3.git
# 从 Coding 远程仓库获取 develop 分支的最新信息
git fetch coding-origin develop
# 合并 Coding 的 develop 分支到本地的 develop 分支
# --allow-unrelated-histories 允许合并两个没有共同历史的分支
git merge --no-ff --allow-unrelated-histories coding-origin/develop
# 将合并后的本地 develop 分支推送到 GitHub 的 develop 分支
git push origin develop

View File

@@ -0,0 +1,74 @@
# Soul 创业派对 - 需求汇总
> **主需求文件**(日期最新)。基准:《[以界面定需求](以界面定需求.md)》
---
## 需求基准
- **以代码为准**:需求文档滞后于实现时,以实际代码行为为准,**反向补齐文档**。
- 需求以《以界面定需求》为界面级基准;新增/变更功能时先对齐界面再落需求清单。
- 需求文件命名:`YYYY-MM-DD-需求.md`**日期最新的为主需求文件**
---
## 需求清单
| 日期 | 描述 | 状态 | 备注 |
|------|------|------|------|
| 2026-02 | 内容管理页仅保留「API 接口」按钮 | 已完成 | soul-admin ContentPage |
| 2026-02 | 侧栏与分销页「交易中心」→「推广中心」 | 已完成 | AdminLayout、DistributionPage |
| 2026-02 | 推广中心/我的收益:绑定中、已付款、已过期清晰展示 | 已有 | referral 页 |
| 2026-02 | 海报小程序码带用户 IDscene ref=userId | 已完成 | referral.js generatePoster |
| 2026-02 | 复制朋友圈文案去掉「专属邀请码」展示 | 已完成 | 海报 |
| 2026-02 | 我的页:待领收益→我的收益、头像/昵称/ID 一键获取 | 已有 | my.wxml |
| 2026-02 | 设置页:手机/微信号一键获取、自动提现默认开启 | 已有 | settings.js |
| 2026-02 | 后台与前台参数一致(绑定有效期、自动提现、免费章节等) | 已检查 | 推广设置 |
| 2026-02 | 找伙伴匹配后台用户库、资源对接两步 | 已有 | match 页 |
| 2026-02 | VIP 手动设置 + 支付设置 + 日志 | 已完成 | 用户详情弹窗、支付回调 |
| 2026-02 | 管理端设置 VIP 必填到期日 | 已完成 | 前后端校验 |
| 2026-02 | 会员订单分润差异化(会员 20% / 非会员 10% | 已完成 | computeOrderCommission |
| 2026-02 | VIP 设置入口拆分、SetVipModal、VIP 角色管理 | 已完成 | UserDetailModal、SetVipModal |
| 2026-02 | VIP 排序vip_activated_at、vip_sort | 已完成 | VipMembers |
| 2026-02 | VIP 角色:可选择 + 可手动填写 | 已完成 | vip_roles 表 |
| 2026-03-08 | 文章阅读付费规则:免费章节以 free_chapters 为准VIP 全章免费 | 已完成 | soul-api book.go |
| 2026-03-10 | 我的页阅读统计改为后端接口(真实数据) | 已完成 | loadDashboardStats |
| 2026-03-10 | 富文本渲染升级TipTap HTML → rich-text保留 @mention | 待实施 | 确认 DB 格式 |
| 2026-03-16 | 文章编辑 @某人/#标签 自动创建并同步存客宝 | 已完成 | ParseAutoLinkContent |
| 2026-03-16 | 编辑资料页分享名片Canvas 封面、标题「昵称+为您分享名片」 | 已完成 | profile-edit.js |
| 2026-03-16 | 链接人与事列表table、planId/apiKey 列、删除 Dialog | 已完成 | ContentPage.tsx |
| 2026-03-16 | 存客宝创建计划参数planType=1、sceneId=9、status=1 | 已完成 | db_person.go |
| 2026-03-16 | @mention 存储格式span 必须含 data-label | 已完成 | ParseAutoLinkContent |
| 2026-03-17 | 代付统一到代付页 | 已完成 | read.js onLoad |
| 2026-03-18 | 代付流程:阅读页弹窗选择名额→支付→分享;好友自动领取 | 已完成 | read 页、singlePage 引导 |
| 2026-03-18 | 代付退款后禁用status=refunded | 已完成 | gift_pay_requests |
| 2026-03-20 | 提现审批逻辑修复:批准时校验「累计-已提现>=待审核」 | 已完成 | doApproveWithdrawal |
| 2026-03-20 | 我的页「我的收益」取 availableEarnings | 已完成 | my.wxml pendingEarnings |
| 2026-03-20 | 推广设置:提现手续费、自动提现开关 | 已完成 | ReferralSettingsPage |
| 2026-03-20 | 提现审核列表:自动审批开关、备注列 | 已完成 | DistributionPage |
| 2026-03-20 | 提现失败记录fail_reason/error_message 落库 | 已完成 | admin_withdrawals |
| 2026-03-18 | 资料完善引导checkVipContactRequiredAndGuide、avatar-nickname、profile-edit、VIP 支付成功引导、新用户强制引导 | 已完成 | app.js、miniprogram/docs/资料完善引导流程图.md |
| 2026-03-18 | 购买≥3 章显示「解锁全书」按钮 | 已完成 | read.wxml wx:if="{{purchasedCount >= 3}}" |
| 2026-03-24 | 分润比例前端从 config 读取shareRate | 已完成 | config/core、read/referral 页 |
---
## 三端需求速查
| 端 | 主需求 |
|----|--------|
| 小程序 | 我的收益、推广中心、代付、阅读统计、VIP |
| 管理端 | 推广设置、提现审核、VIP 设置、链接人与事 |
| 后端 | 提现审批、referral_config、代付、分润 |
---
## 文档同步记录
| 日期 | 说明 |
|------|------|
| 2026-04-07 | 与 [开发文档索引](../索引.md)、[闭环与文档同步-2026-04-07.md](../10、项目管理/闭环与文档同步-2026-04-07.md) 交叉对齐;主需求文件未更名,仍以本文件为日期最新主需求 |
---
**最后更新**2026-04-07清单内容基准日仍为 2026-03-20同步记录见上表

View File

@@ -0,0 +1,60 @@
# AI 剪辑分发功能需求
**记录日期:** 2026-03-28
**所属项目:** 卡若创业派对
**状态:** 需求待评审
---
## 功能概述
在「卡若创业派对」App 中新增 AI 对话式视频剪辑 + 多平台自动分发模块。用户通过与 AI 对话描述剪辑需求AI 自动完成剪辑并推送结果,用户确认后一键分发到各平台,实现持续收益。
---
## 核心功能
### 1. AI 对话式剪辑
- 用户与 AI 直接对话,描述剪辑需求
- 输入格式示例:「帮我剪 3月20日 第2场从10分钟到25分钟」
- AI 理解意图后自动执行剪辑
- 剪辑完成后推送视频 + 对应文字内容给用户确认
### 2. 多平台自动分发
- 剪辑内容一键分发到各平台(抖音、视频号、小红书等)
- 通过保存各平台 Cookie 实现免登录自动发布
- Cookie 与卡若创业派对账号绑定,长期保存
- 每次发布后平台产生的收益归属用户
---
## 用户流程
```
用户与AI对话
→ 指定日期 / 场次 / 片段时间范围
→ AI 自动剪辑
→ 推送视频 + 文字内容给用户确认
→ 用户确认
→ 一键分发到各平台
→ 平台自动发布
→ 产生收益
```
---
## 技术要点(待细化)
- AI 剪辑引擎ffmpeg / 云剪辑 API
- 平台 Cookie 管理与自动刷新机制
- 多平台发布 API 或自动化方案
- 收益数据回流追踪
---
## 待办
- [ ] 技术方案评审
- [ ] 优先级排期
- [ ] UI/UX 设计
- [ ] 平台 Cookie 合规性确认

View File

@@ -0,0 +1,11 @@
# 1、需求 - 归档
专项需求、技术分析、已合并或过时文档,保留供追溯。
| 文件 | 说明 |
|------|------|
| 链接人与事-实现方案.md | 实现方案 |
| 链接人与事-存客宝同步-需求规划.md | 存客宝同步需求规划 |
| 链接人与事-所有同步需求.md | 链接人与事所有同步需求 |
| 链接人与事-置顶功能-技术分析.md | 置顶功能技术分析 |
| 文章详情-阅读页线框图.md | 阅读页线框图 |

View File

@@ -0,0 +1,158 @@
# 文章详情(阅读页)线框图
> Soul 创业派对 - 小程序 `pages/read/read` 界面结构
---
## 一、完整内容态(免费/已购买)
```
┌─────────────────────────────────────────┐
│ ████████████░░░░░░░░░░ 阅读进度 60% │ ← 顶部固定进度条
├─────────────────────────────────────────┤
│ ← 第 4 章 真实的行业 │ ← 导航栏
├─────────────────────────────────────────┤
│ [4] [免费] │ ← 章节元信息
│ 4.1 旅游号30天10万粉的真实逻辑 │ ← 章节标题(可长按复制)
├─────────────────────────────────────────┤
│ │
│ 这是一段正文内容,文中包含 @卡若 和 │
│ #创业资源 可以点击。支持长按复制文字。 │ ← @ 高亮可点,# 金色可点
│ │
│ 第二段纯文本,无特殊标记。 │
│ │
│ ┌─────────────────────────────────┐ │ ← 图片(可点击全屏预览)
│ │ [ 插图 ] │ │
│ └─────────────────────────────────┘ │
│ │
├─────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────────┐│
│ │ 上一篇 │ │ 下一篇 ││ ← 章节导航
│ │ 3.5 桶装水 │ │ 4.2 美业整合 → ││
│ └──────────────┘ └──────────────────┘│
│ │
│ [ 📣 分享到朋友圈 ] [ 🖼️ 生成海报 ] │ ← 操作区
└─────────────────────────────────────────┘
```
---
## 二、付费墙态(未登录)
```
┌─────────────────────────────────────────┐
│ ← 第 4 章 真实的行业 │
├─────────────────────────────────────────┤
│ 这是一段预览内容,显示前 50%... │
│ 第二段预览... │
│ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← 渐变遮罩
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
├─────────────────────────────────────────┤
│ 🔒 │
│ 登录后继续阅读 │
│ 已阅读50%,登录后查看完整内容 │
│ │
│ [ 立即登录 ] │
├─────────────────────────────────────────┤
│ [ 上一篇 ] [ 下一篇 → ] │
└─────────────────────────────────────────┘
```
---
## 三、付费墙态(已登录未购买)
```
┌─────────────────────────────────────────┐
│ (同上:预览内容 + 渐变遮罩) │
├─────────────────────────────────────────┤
│ 🔒 │
│ 解锁完整内容 │
│ 已阅读50%,购买后继续阅读 │
│ │
│ [ 购买本章 ¥1.0 ] │
│ [ ✨ 解锁全部 62 章 ¥9.9 省82% ] │ ← 购买≥3章才显示
│ │
│ 分享给好友一起学习,还能赚取佣金 │
├─────────────────────────────────────────┤
│ [ 上一篇 ] [ 下一篇 → ] │
└─────────────────────────────────────────┘
```
---
## 四、交互说明
| 元素 | 交互 |
|------|------|
| **@卡若** | 点击 → 确认弹窗「是否添加 @卡若?」→ 调 CKB 留资加好友 |
| **#创业资源** | 点击 → 按类型:内链跳转 / 外链复制 / 小程序唤醒 / CKB 加好友 |
| **正文/标题** | 长按 → 选中复制user-select |
| **图片** | 点击 → 全屏预览;长按 → 保存菜单 |
| **上一篇/下一篇** | 点击 → 切换章节 |
---
## 五、@ / # 三端数据流线框图
```mermaid
flowchart TB
subgraph 管理端["管理端 soul-admin"]
A1[链接人与事 Person]
A2[链接标签 LinkTag]
A3[RichEditor 编辑]
A4[autoLinkContent 转换]
A1 --> A3
A2 --> A3
A3 --> A4
A4 --> |"content HTML"| DB
end
subgraph 后端["后端 soul-api"]
DB[(chapters.content)]
API[GET /api/miniprogram/book/chapter]
CKB[POST /api/miniprogram/ckb/lead]
CFG[GET /api/miniprogram/config]
DB --> API
end
subgraph 小程序["小程序 miniprogram"]
B1[contentParser 解析]
B2[contentSegments 渲染]
B3[onMentionTap]
B4[onLinkTagTap]
API --> B1
B1 --> B2
B2 --> B3
B2 --> B4
B3 --> CKB
B4 --> |"内链"| Nav[wx.navigateTo]
B4 --> |"外链"| Clip[复制剪贴板]
B4 --> |"miniprogram"| MP[wx.navigateToMiniProgram]
CFG --> B4
end
```
---
## 六、数据流文字说明
```
管理端 ContentPage
→ 编辑插入 @[名称](token) / #标签
→ 保存 contentTipTap HTML
后端 chapters.content
→ 原样存储、原样返回
小程序 contentParser
→ 解析 → contentSegments
→ WXML 渲染 text / mention / linkTag / image
用户点击 @
→ onMentionTap → POST /api/miniprogram/ckb/lead
用户点击 #
→ onLinkTagTap → 按 tagType 分支处理
```

View File

@@ -0,0 +1,296 @@
# 链接人与事 — 存客宝同步 需求规划
> 参与角色:产品经理、管理端开发工程师、后端开发
> 创建日期2026-03-13
> **同步需求汇总**:见 `链接人与事-所有同步需求.md`
---
## 一、需求概述
**模块**:内容管理 → 链接AI → AI列表链接人与事
**核心变更**
1. **创建 Person 时**:同步在存客宝创建「场景获客计划」
2. **创建后**:数据(含密钥)记录到本地 `persons`
3. **编辑时**:管理端可编辑,编辑数据同步到存客宝
4. **UI 布局**:考虑是否另起独立 tab/页面,便于操作
---
## 二、现状梳理
| 项目 | 现状 |
|------|------|
| Person 模型 | `person_id``token`32 位)、`name``label``ckb_api_key` |
| 创建流程 | 管理端填表 → `POST /api/db/persons` → 仅落本地库 |
| 密钥 | `ckb_api_key` 手动填写,留空则用全局 `CKB_LEAD_API_KEY` |
| 存客宝 API | 开放接口:`/v1/open/auth/token` 获取 JWT → `POST /v1/open/scenarios` 创建/提交 |
| 链接AI 位置 | ContentPage 的「链接AI」tab 下,与「#链接标签」同屏 |
---
## 三、产品建议:是否另起 Tab/页面
### 方案 A保持链接AI tab在现有卡片内增强
- **优点**:改动小,与编辑器 @ 场景就近
- **缺点**链接AI 已含「AI 人物」+「链接标签」两块,再加 CKB 同步、编辑同步逻辑会拥挤
### 方案 B独立「链接人与事」页面推荐
- **优点**
- 与「关联小程序」对称LinkedMpPage 独立在设置下)
- 空间充足可做创建表单、CKB 同步状态、编辑同步、密钥管理、列表筛选
- 后续扩展(如批量同步、同步日志)更方便
- **缺点**:需新增路由与导航入口
**建议**:采用 **方案 B**在「设置」下新增「链接人与事」tab或与「关联小程序」并列`LinkedMpPage` 结构类似。
---
## 四、业务流程(产品 + 后端)
### 4.1 创建 Person 流程
```
管理端填写名称、人物ID可选、标签、存客宝账号account
后端 DBPersonSave
1. 生成 token已有
2. 调用存客宝开放 API
- POST /v1/open/auth/tokenapiKey + account + sign
- POST /v1/open/scenarios 创建场景获客计划(需确认 CKB 创建计划接口)
3. 存客宝返回 planId、apiKey若 CKB 有返回)
4. 落库 personsperson_id、token、name、label、ckb_api_key、ckb_plan_id新增
```
**待确认**:存客宝「创建场景获客计划」的具体接口路径、请求/响应格式。当前 `open-api-sign.md` 示例为「提交 lead」到已有 plan创建 plan 的接口需向存客宝方确认。
### 4.2 编辑 Person 流程
```
管理端编辑:名称、标签、其他可编辑字段
后端 DBPersonSave更新逻辑
1. 更新本地 persons
2. 若该 Person 有 ckb_plan_id调用存客宝更新接口同步变更
```
**待确认**存客宝是否有「更新计划」接口PUT/PATCH
### 4.3 本地存储扩展
| 字段 | 类型 | 说明 |
|------|------|------|
| `ckb_plan_id` | int/varchar | 存客宝场景获客计划 ID创建时由 CKB 返回 |
| `ckb_api_key` | varchar | 存客宝 API Key已有创建时由 CKB 返回或沿用全局配置) |
| `ckb_account` | varchar | 存客宝账号,用于鉴权(可选,若每个 Person 独立账号) |
---
## 五、后端开发任务
| 序号 | 任务 | 说明 |
|------|------|------|
| 1 | 实现 CKB 开放 API 客户端 | `internal/ckb/open_client.go`:鉴权签名、获取 token、调用 plan 接口 |
| 2 | 扩展 Person 模型 | 新增 `ckb_plan_id`;创建计划时 name=SOUL链接人与事-{name} |
| 3 | 改造 DBPersonSave | 新增:调 CKB 创建计划 → 落库;更新:调 CKB 更新计划 |
| 4 | 改造 DBPersonDelete | 删除:若有 ckb_plan_id先调 CKB 删除计划,再删本地 |
| 5 | 配置项 | `CKB_OPEN_API_KEY``CKB_OPEN_ACCOUNT`(全局) |
---
## 六、管理端开发任务
| 序号 | 任务 | 说明 | 状态 |
|------|------|------|------|
| 1 | 扩展现有链接人与事卡片 | 在 ContentPage 的 link-person tab 内 | ✅ |
| 2 | **添加/编辑弹窗** | 点击「添加」或「编辑」打开弹窗,配置与存客宝 API 获客一致(见 7.4 | ✅ |
| 3 | 列表展示 | 展示 token、ckb_plan_id、同步状态、密钥状态 | ✅ |
| 4 | **编辑计划入口** | 每行「编辑计划」按钮,有 ckbPlanId 时跳转存客宝编辑页 | ✅ |
| 5 | 编辑能力 | 弹窗内编辑全部配置;编辑计划跳转存客宝 | ✅ |
---
## 七、存客宝 API 接口(来自 Cunkebao 前端项目)
> 来源:`Cunkebao/src/pages/mobile/scenarios/` 及 `step.api.ts`、`index.api.ts`
### 7.1 计划Plan相关
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取场景类型 | GET | `/v1/plan/scenes` | 场景列表,用于新建时选择 |
| **创建计划** | **POST** | **`/v1/plan/create`** | 创建获客计划 |
| **更新计划** | **PUT** | **`/v1/plan/update`** | 更新获客计划 |
| **删除计划** | **DELETE** | **`/v1/plan/delete`** | 删除获客计划body: planId |
| 获取计划详情 | GET | `/v1/plan/detail?planId=xxx` | 计划详情 |
### 7.2 场景Scenario相关
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 获取场景列表 | GET | `/v1/plan/scenes` | 同 plan/scenes |
| 获取场景详情 | GET | `/v1/scenarios/{id}` | 场景详情 |
| 创建场景 | POST | `/v1/scenarios` | 创建场景 |
| 更新场景 | PUT | `/v1/scenarios/{id}` | 更新场景 |
| 删除场景 | DELETE | `/v1/scenarios/{id}` | 删除场景 |
### 7.3 创建计划请求体createPlan
来自 `NewPlan/index.tsx``index.data.ts`,核心字段:
```ts
{
name: string; // 计划名称
sceneId: number; // 场景 ID1=海报等)
scenario: number; // 同 sceneId
remarkType: string; // 备注类型,如 "phone"
greeting: string; // 打招呼语
addInterval: number; // 加人间隔
startTime: string; // 如 "09:00"
endTime: string; // 如 "18:00"
enabled: boolean; // 是否启用
tips?: string; // 获客成功提示
distributionEnabled?: boolean; // 是否开启分销
distributionChannels?: number[]; // 分销渠道 ID 列表
customerRewardAmount?: number; // 获客奖励金额
addFriendRewardAmount?: number; // 添加奖励金额
// 编辑时需传
id?: number;
planId?: number;
}
```
### 7.4 场景获客创建 — 用户需输入项Soul 管理端)
| 字段 | 必填 | 说明 | 默认值 |
|------|------|------|--------|
| **名称** | 是 | 计划名称,对应 Person 的 name | - |
| **人物ID** | 否 | 本地 person_id不传则自动生成 | 自动 |
| **标签** | 否 | 身份/角色,如「超级个体」 | - |
| **场景类型** | 否 | sceneId1=海报 11=API获客 等 | 11API获客 |
| **存客宝账号** | 是* | account用于开放 API 鉴权 | 全局配置 |
| **打招呼语** | 否 | greeting | 「你好,请通过」 |
| **获客成功提示** | 否 | tips | 「请注意消息,稍后加你微信」 |
| **存客宝密钥** | 否 | 创建后 CKB 返回,可手动覆盖 | 创建后落库 |
\* 若使用全局 CKB_OPEN_ACCOUNT则用户可不填。
### 7.5 列表展示(简化)
| 字段 | 说明 |
|------|------|
| **token** | 32 位唯一标识,文章 @ 时存此值 |
| **@的人** | 人物名称,编辑器 @ 时展示 |
| **获客计划活动名** | 固定格式:`SOUL链接人与事-{名称}` |
删除人物时,后端需同时删除存客宝对应获客计划。
### 7.6 列表 — 编辑计划入口
创建后,列表每行需提供**编辑计划**入口:
| 入口类型 | 说明 |
|----------|------|
| **跳转存客宝** | 按钮「编辑计划」→ 新开 `https://h5.ckb.quwanzhi.com/scenarios/edit/{planId}`(需确认 Cunkebao 编辑页 URL |
| **管理端内嵌** | 按钮「编辑计划」→ 弹窗拉取计划详情,表单编辑后提交,后端调 CKB 更新 |
建议:优先**跳转存客宝**,用户直接在存客宝完整编辑;若需闭环在 Soul 内,再做内嵌表单。
### 7.7 鉴权方式
Cunkebao 前端使用 `Authorization: Bearer <token>`token 来自登录接口。Soul 对接时需确认:
- **开放 API**`open-api-sign.md`):先 `POST /v1/open/auth/token` 获取 JWT再 Bearer 调用
- **内部 API**Cunkebao 管理端登录后直接 Bearer可能与开放 API 不同
---
## 八、已确认项2026-03-13
| 项 | 结论 |
|----|------|
| 创建计划接口 | `POST /v1/plan/create` |
| 更新计划接口 | `PUT /v1/plan/update` |
| 鉴权 | **开放 API**:先 `POST /v1/open/auth/token` 获取 JWT再 Bearer 调用 plan 接口 |
| 响应体 | **是**`createPlan` 返回 `planId``apiKey`,需落库到 persons |
| 创建参数 | **标准创建**:按 Cunkebao 标准参数创建,不精简 |
---
## 九、验收标准(草案)
| 场景 | 验收点 |
|------|--------|
| 创建 Person | 填写名称等 → 提交 → 本地 persons 有记录,且 ckb_plan_id 有值 |
| 编辑 Person | 修改名称/标签 → 提交 → 本地更新,存客宝计划同步更新 |
| 密钥展示 | 列表显示「密钥 ✓」或「用默认密钥」 |
| 小程序 @ 流程 | 文章 @ 人物 → 小程序点击 → 留资 → 正确推送到对应存客宝计划(现有逻辑保持) |
---
## 十、链接人与事 — 三端流程总结
### 10.1 管理端soul-admin
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 内容管理 → 链接人与事 → 添加 | 打开弹窗填写名称、人物ID、标签、存客宝账号、密钥、打招呼语、获客提示 |
| 2 | 提交 | 后端创建 Person + 调 CKB 创建获客计划name=SOUL链接人与事-{名称}sceneId=11 API获客 |
| 3 | 列表 | 仅显示 token、获客计划活动名SOUL链接人与事-{名称} |
| 4 | 编辑 | 打开弹窗修改,后端同步更新存客宝计划 |
| 5 | 编辑计划 | 跳转存客宝编辑页(需 ckbPlanId |
| 6 | 删除 | 删除 Person + 后端调 CKB 删除对应获客计划 |
### 10.2 后端soul-api
| 步骤 | 接口 | 说明 |
|------|------|------|
| 1 | POST /api/db/persons | 新增:生成 token → 调 CKB 开放 API 创建计划name=SOUL链接人与事-{name})→ 落库 person_id、token、name、label、ckb_api_key、ckb_plan_id |
| 2 | POST /api/db/persons | 更新:更新本地 → 若有 ckb_plan_id 调 CKB 更新计划 |
| 3 | DELETE /api/db/persons?personId=xxx | 删除:删本地 → 若有 ckb_plan_id 调 CKB 删除计划 |
| 4 | GET /api/db/persons | 列表:返回 persons含 token、name、ckb_plan_id 等) |
### 10.3 小程序miniprogram
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 文章渲染 | contentParser 解析 @mentiondata-id 存 token |
| 2 | 用户点击 @人物 | onMentionTap → targetUserId 传 token |
| 3 | 留资提交 | POST /api/miniprogram/ckb/leadbody 含 targetUserIdtoken |
| 4 | 后端 | 用 token 查 persons 得 ckb_api_key → 推存客宝 scenarios |
### 10.4 数据流总览
```
管理端添加 Person
→ 后端:生成 token + 调 CKB 创建计划SOUL链接人与事-{name}
→ 落库person_id、token、name、ckb_api_key、ckb_plan_id
管理端编辑文章,写入 @[名称](token)
→ 小程序 contentParser 解析data-id=token
用户点击 @人物
→ 小程序 onMentionTap(targetUserId=token)
→ POST /api/miniprogram/ckb/lead
→ 后端token → persons.ckb_api_key → 推存客宝
管理端删除 Person
→ 后端:删本地 + 调 CKB 删除计划
```
---
## 十一、参考文档
- `open-api-sign.md`:存客宝开放 API 鉴权规范
- `Cunkebao/src/pages/mobile/scenarios/plan/new/index.api.ts`:创建/更新计划接口
- `Cunkebao/src/pages/mobile/scenarios/plan/new/steps/step.api.ts`:场景 CRUD、计划列表
- `Cunkebao/src/api/request.ts`请求封装baseURL=ckbapi.quwanzhi.comBearer token
- `soul-api/internal/handler/db_person.go`:当前 Person 增删改
- `soul-admin/src/pages/linked-mp/LinkedMpPage.tsx`:关联小程序独立页面结构

View File

@@ -0,0 +1,206 @@
# 链接人与事 — 实现方案(综合分析)
> 基于需求规划与存客宝源码分析,整理实现清单与实施顺序
---
## 一、现状与缺口
### 1.1 已完成
| 模块 | 内容 | 状态 |
|------|------|------|
| 管理端 | 链接人与事 tab、添加/编辑弹窗 PersonAddEditModal | ✅ |
| 管理端 | 列表展示 token、@的人、获客计划活动名、planId、apiKeytable 布局、apiKey 复制图标) | ✅ |
| 管理端 | 删除前 Dialog 二次确认弹窗 | ✅ |
| 管理端 | 编辑计划按钮(跳转存客宝) | ✅ |
| 后端 | Person CRUD仅本地未接 CKB | ✅ |
| 后端 | getCkbLeadApiKeysite_settings.ckbLeadApiKey | ✅ |
| 小程序 | @ 用 token、onMentionTap 传 targetUserId | ✅ |
### 1.2 待实现
| 模块 | 内容 |
|------|------|
| 后端 | Person 模型新增 `ckb_plan_id` |
| 后端 | CKB 开放 API 客户端(鉴权 + 创建/更新/删除计划) |
| 后端 | DBPersonSave 接入 CKB 创建/更新 |
| 后端 | DBPersonDelete 接入 CKB 删除计划 |
| 管理端 | PersonAddEditModal 提交时传 greeting、tips、ckbAccount供后端调 CKB |
| 待确认 | 设备选择CKB 创建计划需 deviceGroups开放 API 是否支持Soul 如何获取设备? |
---
## 二、删除二次确认
### 2.1 当前实现2026-03-16 已升级为 Dialog
- 使用 `Dialog` + `DialogFooter` 做确认弹窗,替代原生 `confirm()`
- 按钮:「取消」「确定删除」
- 弹窗尺寸max-w-md、p-4避免过大
---
## 三、后端实现清单
### 3.1 数据库
```sql
-- persons 表新增 ckb_plan_id
ALTER TABLE persons ADD COLUMN ckb_plan_id INT DEFAULT NULL COMMENT '存客宝获客计划ID';
```
### 3.2 Person 模型
```go
// model/person.go
CkbPlanId *int `gorm:"column:ckb_plan_id" json:"ckbPlanId"`
```
### 3.3 CKB 开放 API 客户端
新建 `internal/ckb/open_client.go`
| 函数 | 说明 |
|------|------|
| `GetToken(apiKey, account string) (string, error)` | POST /v1/open/auth/token返回 JWT |
| `CreatePlan(token string, req CreatePlanReq) (*CreatePlanResp, error)` | POST /v1/plan/create |
| `UpdatePlan(token string, planId int, req UpdatePlanReq) error` | PUT /v1/plan/update |
| `DeletePlan(token string, planId int) error` | DELETE /v1/plan/delete |
**鉴权**:按 `open-api-sign.md``sign = MD5(MD5(account+timestamp)+apiKey)`
**配置**`CKB_OPEN_API_KEY``CKB_OPEN_ACCOUNT`(或从 site_settings 读)。
### 3.4 创建计划请求体(按 Cunkebao 标准)
```go
type CreatePlanReq struct {
Name string `json:"name"` // SOUL链接人与事-{name}
SceneId int `json:"sceneId"` // 92026-03-16 修正)
Scenario int `json:"scenario"` // 9
Greeting string `json:"greeting"` // 你好,请通过
Tips string `json:"tips"` // 请注意消息,稍后加你微信
RemarkType string `json:"remarkType"` // phone
AddInterval int `json:"addInterval"` // 1
StartTime string `json:"startTime"` // 09:00
EndTime string `json:"endTime"` // 18:00
Enabled bool `json:"enabled"` // true
DeviceGroups []int `json:"deviceGroups,omitempty"` // 待确认:开放 API 是否必填
}
```
### 3.5 设备选择(待确认)
- Cunkebao 前端创建计划时需选择设备(`deviceGroups`
- Soul 通过开放 API 创建时:
- 开放 API 是否支持 `deviceGroups`
- 若支持Soul 如何获取设备列表?是否有 `/v1/open/devices` 或类似接口?
- 若不支持或可不传,是否允许 `deviceGroups: []` 创建?
**建议**:先不传 `deviceGroups` 或传空数组,实测 CKB 是否接受;若报错再与存客宝确认。
### 3.6 DBPersonSave 改造
**新增**
1. 生成 token
2.`GetToken` 获取 JWT
3.`CreatePlan`name=`SOUL链接人与事-{name}`planType=1、sceneId=9、status=1
4. 落库person_id、token、name、label、ckb_api_keyCKB 返回、ckb_plan_idCKB 返回)
**更新**
1. 更新本地 persons
2. 若有 ckb_plan_id`UpdatePlan` 同步 name、greeting、tips 等
### 3.7 DBPersonDelete 改造
1. 查 Person 得 ckb_plan_id
2. 若有 ckb_plan_id`GetToken``DeletePlan`
3. 删本地 persons
### 3.8 DBPersonSave 请求体扩展
```go
var body struct {
PersonID string `json:"personId"`
Name string `json:"name"`
Label string `json:"label"`
CkbApiKey string `json:"ckbApiKey"`
CkbAccount string `json:"ckbAccount"` // 新增:开放 API 鉴权用
Greeting string `json:"greeting"` // 新增:创建计划用
Tips string `json:"tips"` // 新增:创建计划用
}
```
---
## 四、管理端实现清单
### 4.1 PersonAddEditModal 提交扩展
当前只传 `personId``name``label``ckbApiKey`。需增加:
- `ckbAccount`:存客宝账号(鉴权)
- `greeting`:打招呼语
- `tips`:获客成功提示
弹窗内已有这些字段,只需在 `onSubmit` 的 payload 中带上。
### 4.2 ContentPage 提交 payload
```ts
const payload = {
personId: data.personId || ...,
name: data.name,
label: data.label,
ckbApiKey: data.ckbApiKey || undefined,
ckbAccount: data.ckbAccount || undefined, // 新增
greeting: data.greeting || undefined, // 新增
tips: data.tips || undefined, // 新增
}
```
### 4.3 删除确认(可选升级)
- 方案 A保持 `confirm()`,无需改动
- 方案 B`Dialog` 做确认弹窗,与整体 UI 统一
---
## 五、实施顺序建议
| 阶段 | 任务 | 依赖 |
|------|------|------|
| 1 | 数据库迁移persons 加 ckb_plan_id | - |
| 2 | Person 模型加 CkbPlanId | 1 |
| 3 | 实现 CKB 开放 API 客户端GetToken、CreatePlan、UpdatePlan、DeletePlan | open-api-sign.md |
| 4 | DBPersonSave 扩展 bodyckbAccount、greeting、tips | - |
| 5 | DBPersonSave 新增逻辑:调 CKB 创建计划 → 落库 | 2, 3, 4 |
| 6 | DBPersonSave 更新逻辑:调 CKB 更新计划 | 2, 3 |
| 7 | DBPersonDelete先删 CKB 计划再删本地 | 2, 3 |
| 8 | PersonAddEditModal 提交时传 greeting、tips、ckbAccount | - |
| 9 | ContentPage onSubmit 传完整 payload | 8 |
| 10 | (可选)删除确认改为 Dialog | - |
---
## 六、风险与待确认
| 项 | 说明 |
|----|------|
| 设备选择 | CKB 创建计划是否强制 deviceGroups开放 API 是否支持? |
| 开放 API 路径 | plan 接口是 `/v1/plan/create` 还是 `/v1/open/plan/create`?需确认 baseURL |
| 错误处理 | CKB 创建失败时,是否仍落库 Personckb_plan_id 为空)?建议:创建失败则整体回滚,不落库 |
| 配置来源 | apiKey、account 用 env 还是 site_settings当前 ckbLeadApiKey 已支持 site_settings |
---
## 七、参考
- `开发文档/1、需求/链接人与事-存客宝同步-需求规划.md`
- `open-api-sign.md`
- `Cunkebao/src/pages/mobile/scenarios/plan/new/`
- `soul-api/internal/handler/db_person.go`
- `soul-api/internal/handler/ckb.go`

View File

@@ -0,0 +1,82 @@
# 链接人与事 — 所有同步需求汇总
> 整合自:链接人与事-存客宝同步-需求规划、实现方案、2026-03-16 文章编辑自动创建
---
## 一、同步场景总览
| 场景 | 触发 | 同步动作 | 状态 |
|------|------|----------|------|
| **创建 Person** | 管理端添加 / 文章 @某人 不存在时自动创建 | 调存客宝创建获客计划 → 落库 ckb_plan_id、ckb_api_key | ✅ 已实现 |
| **编辑 Person** | 管理端编辑弹窗保存 | 调存客宝更新计划 | 待实现 |
| **删除 Person** | 管理端删除 | 调存客宝删除计划 → 再删本地 | ✅ 已实现 |
| **文章 @某人 不存在** | 编辑文章输入 @新人物 并保存 | ensureMentionsAndTags → POST persons → 自动创建 + 同步存客宝 | ✅ 已实现 |
---
## 二、已实现同步
### 2.1 创建 Person 时同步存客宝
- **触发**POST /api/db/persons仅传 name 或完整表单)
- **流程**:生成 token → 调 ckbOpenCreatePlandeviceGroups 未传时默认选名为 soul 的设备)→ 落库 person_id、token、name、ckb_api_key、ckb_plan_id
- **实现**`soul-api/internal/handler/db_person.go``ckb_open.go`
### 2.2 删除 Person 时同步删存客宝
- **触发**DELETE /api/db/persons?personId=xxx
- **流程**:若有 ckb_plan_id → 调 ckbOpenDeletePlan → 再删本地
- **实现**`db_person.go` DBPersonDelete
### 2.3 文章 @某人 自动创建并同步
- **触发**管理端保存文章content 含 @新人物persons 中不存在)
- **流程**ensureMentionsAndTags 提取 @name → POST /api/db/persons {name} → 创建 Person + 存客宝计划
- **实现**`soul-admin` ContentPage ensureMentionsAndTags`db_person.go` 按 name 查找/创建
---
## 三、待实现同步
### 3.1 编辑 Person 时同步存客宝
- **触发**:管理端编辑 Person 后保存PUT 逻辑personId 已存在)
- **流程**:更新本地 persons → 若有 ckb_plan_id调存客宝 PUT /v1/plan/update 同步 name、greeting、tips 等
- **参考**`链接人与事-存客宝同步-需求规划.md` 4.2、7.1
---
## 四、配置与前置
| 配置 | 说明 |
|------|------|
| CKB_OPEN_API_KEY | 存客宝开放 API 密钥 |
| CKB_OPEN_ACCOUNT | 存客宝账号(鉴权) |
| 设备 | 创建计划时 deviceGroups 必填;未传时默认选 memo/nickname 含 "soul" 的设备 |
### 创建计划必填参数2026-03-16
| 参数 | 值 | 说明 |
|------|-----|------|
| planType | 1 | 必填 |
| sceneId | 9 | 必填 |
| scenario | 9 | 与 sceneId 一致 |
| status | 1 | 必填 |
---
## 五、mention 存储格式2026-03-16
| 规则 | 说明 |
|------|------|
| **data-label 必填** | TipTap Mention 仅从 `data-label` 解析显示名,缺则回退显示 `data-id`token |
| **ParseAutoLinkContent** | 输出 `data-type="mention"` 的 span 必须含 `data-label` |
| **已损坏内容** | span 内为 token 时,用 token 查 persons 取真实名字补回 data-label |
## 六、相关文档
- `链接人与事-存客宝同步-需求规划.md` — 原始需求与 API 约定
- `链接人与事-实现方案.md` — 实现清单
- `临时需求池/2026-03-16-文章编辑自动创建@和#.md` — 自动创建需求
- `开发文档/存客宝对接逻辑图.md` — 对接逻辑与参数约定

View File

@@ -0,0 +1,226 @@
# 链接人与事 — 置顶功能 技术分析
> 参与角色:管理端开发工程师、后端工程师、小程序开发工程师
> 创建日期2026-03-20
> 需求:管理端「链接人与事」列表增加「置顶」能力,置顶人昵称+头像显示在小程序首页右上角;置顶唯一(只能一人)
---
## 一、现状梳理
| 模块 | 现状 |
|------|------|
| **管理端** | ContentPage「链接人与事」tab列表展示 token、@的人、获客数、planId、apiKey、操作编辑/查看新客户/编辑计划/删除) |
| **Person 模型** | person_id、token、name、label、user_id、ckb_api_key、ckb_plan_id 等;**无 avatar 字段** |
| **小程序首页** | 右上角固定展示 `/assets/images/author-avatar.png` + 文案「点击链接卡若」 |
| **index-lead** | `POST /api/miniprogram/ckb/index-lead` 使用全局 `getCkbLeadApiKey()` 推送到存客宝,文案硬编码「卡若会尽快联系您」 |
| **置顶存储** | 无,需新增 |
---
## 二、改造方案总览
### 2.1 数据层
| 改造项 | 方案 | 说明 |
|--------|------|------|
| 置顶存储 | `system_config` 新增 `pinned_person_token` | config_value 存 `{"token":"xxx"}`token 为 persons.token |
| Person 头像 | 方案 APerson 表加 `avatar` 字段 | 管理端可编辑,小程序展示 |
| Person 头像 | 方案 B有 user_id 时 JOIN users 取 avatar | 无 user_id 时用默认占位图 |
**推荐**Person 表加 `avatar` 字段VARCHAR 255管理端 PersonAddEditModal 可编辑;有 user_id 时可选从 users 同步(后续扩展)。
### 2.2 接口层
| 接口 | 使用方 | 说明 |
|------|--------|------|
| `PUT /api/db/persons/pin` | 管理端 | body: `{ token }`,置顶该人;先清空其他置顶再设置 |
| `GET /api/miniprogram/ckb/pinned-person` | 小程序 | 返回 `{ nickname, avatar, token }` 或空(无置顶时) |
| `POST /api/miniprogram/ckb/index-lead` | 小程序 | 改造:根据 pinned_person_token 查 persons 得 ckb_api_key推送到该人计划无置顶时 fallback 全局密钥 |
### 2.3 管理端
- 操作列新增「置顶」图标Pin 或 Star
- 点击置顶:调用 `PUT /api/db/persons/pin`,成功后刷新列表,已置顶行显示「已置顶」标识
- 置顶互斥:新置顶时,后端自动取消其他人置顶
### 2.4 小程序
- 首页 onLoad/onShow 调用 `GET /api/miniprogram/ckb/pinned-person` 获取置顶人
- 有数据:展示 `avatar`(网络图需配置 downloadFile 域名)+ `nickname` + 文案「点击链接{nickname}」
- 无数据:展示默认头像 + 「点击链接卡若」(兼容旧版)
- 点击留资:仍调 `index-lead`,后端按置顶人 token 推送到对应存客宝计划
---
## 三、流程图
### 3.1 置顶设置流程(管理端)
```mermaid
flowchart TB
subgraph 管理端
A[管理员点击「置顶」] --> B[PUT /api/db/persons/pin]
B --> C{后端处理}
C --> D[读取 system_config.pinned_person_token]
D --> E[更新 config_value = 新 token]
E --> F[返回 success]
F --> G[管理端刷新列表]
G --> H[置顶行显示「已置顶」]
end
subgraph 后端
C --> I[清空原 pinned_person_token]
I --> J[写入新 token]
J --> F
end
```
### 3.2 小程序首页展示与留资流程
```mermaid
flowchart TB
subgraph 小程序首页
A1[onLoad / onShow] --> A2[GET /api/miniprogram/ckb/pinned-person]
A2 --> A3{有置顶人?}
A3 -->|是| A4[展示 nickname + avatar]
A3 -->|否| A5[展示默认「点击链接卡若」]
A4 --> A6[用户点击]
A5 --> A6
A6 --> A7[POST /api/miniprogram/ckb/index-lead]
end
subgraph 后端 index-lead
A7 --> B1[读取 pinned_person_token]
B1 --> B2{有置顶?}
B2 -->|是| B3[查 persons 得 ckb_api_key]
B3 --> B4[用该 key 推存客宝]
B2 -->|否| B5[用全局 getCkbLeadApiKey]
B5 --> B4
B4 --> B6[返回 success]
end
```
### 3.3 三端协同时序图
```mermaid
sequenceDiagram
participant 管理端
participant 后端
participant 小程序
Note over 管理端: 阶段 1置顶设置
管理端->>后端: PUT /api/db/persons/pin { token }
后端->>后端: 更新 system_config.pinned_person_token
后端-->>管理端: success
Note over 小程序: 阶段 2首页展示
小程序->>后端: GET /api/miniprogram/ckb/pinned-person
后端->>后端: 读 pinned_person_token → 查 persons
后端-->>小程序: { nickname, avatar, token }
小程序->>小程序: 渲染右上角
Note over 小程序: 阶段 3用户点击留资
小程序->>后端: POST /api/miniprogram/ckb/index-lead
后端->>后端: 按 pinned token 取 ckb_api_key → 推存客宝
后端-->>小程序: success
```
---
## 四、实施清单(按角色)
### 4.1 后端工程师
| 序号 | 任务 | 说明 |
|------|------|------|
| 1 | Person 模型加 `avatar` | `Avatar string gorm:"column:avatar;size:255" json:"avatar"` |
| 2 | 迁移脚本 | `ALTER TABLE persons ADD COLUMN avatar VARCHAR(255) DEFAULT ''` |
| 3 | `PUT /api/db/persons/pin` | body: `{ token }`,更新 system_config.pinned_person_token |
| 4 | `GET /api/miniprogram/ckb/pinned-person` | 读 pinned token → 查 persons → 返回 nickname、avatar、token |
| 5 | 改造 CKBIndexLead | 有 pinned 时用 persons.ckb_api_key无则 fallback 全局 |
| 6 | PersonAddEditModal 支持 avatar | 管理端提交时传 avatar可选 |
### 4.2 管理端开发工程师
| 序号 | 任务 | 说明 |
|------|------|------|
| 1 | 操作列加「置顶」按钮 | Pin 图标title「设为置顶」 |
| 2 | 调用 PUT /api/db/persons/pin | 成功后 toast + 刷新 loadPersons |
| 3 | 置顶状态展示 | 列表加载后需知「当前置顶是谁」→ 需 GET pinned 接口或列表返回 isPinned |
| 4 | PersonAddEditModal 加 avatar 输入 | 可选URL 输入框 |
**置顶状态获取**:后端 `GET /api/db/persons` 可扩展返回 `pinnedToken`(或单独 `GET /api/db/config?key=pinned_person_token`),管理端据此标亮已置顶行。
### 4.3 小程序开发工程师
| 序号 | 任务 | 说明 |
|------|------|------|
| 1 | 首页 initData 加 loadPinnedPerson | 调用 GET /api/miniprogram/ckb/pinned-person |
| 2 | data 增加 pinnedPerson | `{ nickname, avatar, token }` 或 null |
| 3 | WXML 动态渲染 | `wx:if` 有 pinnedPerson 时用其 avatar + nickname否则默认 |
| 4 | 文案 | 有置顶:「点击链接{{pinnedPerson.nickname}}」;无:「点击链接卡若」 |
| 5 | 头像 | 网络图需配置 downloadFile 合法域名;失败时用占位 |
---
## 五、接口契约
### 5.1 PUT /api/db/persons/pin管理端
**请求**
```json
{ "token": "32位persons.token" }
```
**响应**
```json
{ "success": true }
```
```json
{ "success": false, "error": "该人物不存在" }
```
### 5.2 GET /api/miniprogram/ckb/pinned-person小程序
**响应(有置顶)**
```json
{
"success": true,
"data": {
"nickname": "卡若",
"avatar": "https://xxx/avatar.png",
"token": "xxx"
}
}
```
**响应(无置顶)**
```json
{
"success": true,
"data": null
}
```
### 5.3 管理端获取当前置顶
- 方案 A`GET /api/db/config?key=pinned_person_token` 返回 `{ "token": "xxx" }`
- 方案 B`GET /api/db/persons` 响应增加 `pinnedToken` 字段
推荐方案 A与现有 config 接口一致。
---
## 六、注意事项
1. **缓存**pinned-person 可加入 config 缓存或短 TTL如 1 分钟),置顶变更后需失效。
2. **兼容**无置顶时index-lead 保持现有逻辑(全局密钥),文案可继续「卡若会尽快联系您」或改为通用「提交成功」。
3. **Person 删除**:删除已置顶的 Person 时,后端应同时清空 pinned_person_token。
4. **头像域名**:小程序展示网络头像需在微信后台配置 downloadFile 合法域名。
---
**创建时间**2026-03-20
**适用**:链接人与事置顶、小程序首页动态展示

View File

@@ -0,0 +1,155 @@
# 以界面定需求
> 开发团队对齐业务逻辑:**以实际界面为准定义需求**三端小程序、管理端、soul-api按界面与接口一致实现。
> 本文档为需求与验收的**界面级基准**,新增/变更功能时先对齐界面再落需求文档。
---
## 一、原则
| 原则 | 说明 |
|------|------|
| **界面即需求** | 产品需求以「用户可见的界面与操作」为准;接口与数据模型服务于界面。 |
| **三端路由隔离** | 小程序只调 `/api/miniprogram/*`;管理端只调 `/api/admin/*``/api/db/*``/api/orders` 等;禁止混用。 |
| **资料展示统一** | 用户/VIP 展示资料以**用户资料**为准nickname、avatar、projectIntro、phone 等);不再单独存 vip_name/vip_avatar 等「VIP 资料列」,小程序与接口均优先读用户资料。 |
| **文档同步** | 界面或业务规则变更时,同步更新本文档与《需求汇总》需求清单、运营与变更。 |
---
## 二、小程序界面清单
以下为 miniprogram 当前页面(以 `app.json` 与实际调用为准),每页标注:**功能要点**、**主要接口**(均为 `/api/miniprogram/*`)。
| 页面路径 | 功能要点 | 主要接口 |
|----------|----------|----------|
| **pages/index/index** | 首页:超级个体/VIP 成员、精选推荐、目录入口、最新章节、用户资料弹窗 | `GET /api/miniprogram/vip/members``GET /api/miniprogram/users``GET /api/miniprogram/book/recommended``GET /api/miniprogram/book/all-chapters``GET /api/miniprogram/book/latest-chapters``GET /api/miniprogram/user/profile` |
| **pages/chapters/chapters** | 目录:章节列表、免费/付费/VIP 权限、热门与最新 | `GET /api/miniprogram/book/all-chapters``GET /api/miniprogram/config` |
| **pages/read/read** | 阅读:章节内容、购买状态、支付下单、阅读进度、@提及、代付分享(发起→支付→分享;好友自动领取解锁) | `GET /api/miniprogram/book/chapter``GET /api/miniprogram/user/purchase-status``POST /api/miniprogram/pay``POST /api/miniprogram/user/reading-progress``GET /api/miniprogram/config``POST /api/miniprogram/gift-pay/create``POST /api/miniprogram/gift-pay/initiator-pay``POST /api/miniprogram/gift-pay/redeem``GET /api/miniprogram/gift-pay/detail` |
| **pages/match/match** | 找伙伴:匹配类型、留资/匹配提交、资源对接 | `GET /api/miniprogram/config`match_config`POST /api/miniprogram/match/*` 等 |
| **pages/my/my** | 我的配置、阅读统计、待确认提现、收益、VIP 状态、提现入口 | `GET /api/miniprogram/config``GET /api/miniprogram/user/dashboard-stats``GET /api/miniprogram/withdraw/pending-confirm``GET /api/miniprogram/earnings``GET /api/miniprogram/vip/status``POST /api/miniprogram/user/update``POST /api/miniprogram/withdraw` |
| **pages/referral/referral** | 推广中心:推广数据、海报与小程序码、复制文案、申请提现 | `GET /api/miniprogram/referral/data``POST /api/miniprogram/qrcode``POST /api/miniprogram/withdraw` |
| **pages/settings/settings** | 设置:头像/昵称/手机/微信号、用户资料编辑、一键获取 | `GET /api/miniprogram/user/profile``POST /api/miniprogram/user/update``POST /api/miniprogram/phone` |
| **components/login-modal** | 公用登录弹窗手机号一键登录、隐私协议、协议勾选read/my/gift-pay 等页面引入 | 无独立接口,内部调 `app.loginWithPhone``app.login` |
| **pages/vip/vip** | VIP状态查询、开通支付 | `GET /api/miniprogram/vip/status``POST /api/miniprogram/pay`productType=vip |
| **pages/purchases/purchases** | 购买记录 | `GET /api/miniprogram/orders` |
| **pages/withdraw-records/withdraw-records** | 提现记录、确认收款 | `GET /api/miniprogram/withdraw/records``GET /api/miniprogram/withdraw/confirm-info` |
| **pages/member-detail/member-detail** | 会员详情:展示用户资料(昵称/头像/联系方式/项目介绍,优先用户资料) | `GET /api/miniprogram/user/profile` 或会员接口 |
| **pages/profile-show/profile-show** | 资料展示 | `GET /api/miniprogram/user/profile` |
| **pages/profile-edit/profile-edit** | 资料编辑 | `GET /api/miniprogram/user/profile``GET /api/miniprogram/vip/status``POST /api/miniprogram/user/update` |
| **pages/mentors/mentors** | 导师列表 | `GET /api/miniprogram/mentors` 等 |
| **pages/mentor-detail/mentor-detail** | 导师详情、预约 | 导师详情与预约接口 |
| **pages/about/about** | 关于作者、书籍统计 | `GET /api/miniprogram/about/author``GET /api/miniprogram/book/stats` |
| **pages/addresses/** | 收货地址列表与编辑 | 地址相关 `/api/miniprogram/*` |
| **pages/search/search** | 搜索 | `GET /api/miniprogram/book/*` 或搜索接口 |
| **pages/agreement/agreement** | 用户协议 | 静态或配置 |
| **pages/privacy/privacy** | 隐私政策 | 静态或配置 |
| **pages/avatar-nickname/avatar-nickname** | 头像+昵称引导页(新用户/非 VIP 完善用) | 无接口,跳转自 app.checkAvatarNicknameAndGuide |
| **pages/gift-pay/detail** | 代付详情:发起人分享/好友帮他付款 | `GET /api/miniprogram/gift-pay/detail`、支付与领取接口 |
| **pages/gift-pay/list** | 我的代付列表 | `GET /api/miniprogram/gift-pay/my-requests` 等 |
| **pages/gift-pay/redemption-detail** | 代付领取详情(发起人查看领取明细) | gift-pay 相关接口 |
| **pages/wallet/wallet** | 余额/钱包 | 余额相关 `/api/miniprogram/*` |
| **pages/link-preview/link-preview** | 链接预览(分享/H5 跳转用) | 静态或配置 |
---
## 三、管理端界面清单
以下为 soul-admin 路由(以 `App.tsx``AdminLayout` 为准),每页标注:**功能要点**、**主要接口**`/api/admin/*``/api/db/*``/api/orders` 等)。
| 路由 | 功能要点 | 主要接口 |
|------|----------|----------|
| **/login** | 登录 | `POST /api/admin/login` |
| **/dashboard** | 数据概览:用户/订单/收入、最近订单、新用户 | `GET /api/admin``GET /api/orders`、用户与订单统计 |
| **/content** | 内容管理章节树、API 文档入口 | `GET /api/db/chapters`、内容相关 db 接口 |
| **/users** | 用户管理:列表、搜索、用户详情、设置 VIP、用户规则、超级个体、用户旅程 | `GET /api/db/users``PUT /api/db/users``GET/POST/PUT/DELETE /api/db/user-rules``GET /api/admin/users/:id/balance` |
| **/find-partner** | 找伙伴CKB 配置、匹配池、导师、匹配记录、资源对接等 Tab | `GET /api/db/config/full?key=ckb_config`、匹配与导师相关 db/admin 接口 |
| **/distribution** | 推广中心:分销统计、推广设置入口 | `GET /api/db/distribution`、订单统计 |
| **/orders** | 订单列表:筛选、分页、退款、用户/推荐人信息、支付方式(微信/余额/代付) | `GET /api/orders``PUT /api/admin/orders/refund` |
| **/withdrawals** | 提现列表:审核、打款、状态 | `GET /api/admin/withdrawals`、提现审核接口 |
| **/settings** | 系统设置:作者设置、管理员、功能开关、站点、推广设置、免费章节等 | `GET /api/db/config/full``POST /api/db/config` 等 |
| **/vip-roles** | VIP 角色管理CRUD 预设角色 | `GET /api/db/vip-roles``POST /api/db/vip-roles` 等 |
| **/mentors** | 导师管理 | `GET /api/db/mentors` 等 |
| **/mentor-consultations** | 导师预约单 | `GET /api/db/mentor-consultations` 等 |
| **/payment** | 支付相关配置或日志 | 支付相关 admin/db 接口 |
| **/site** | 站点配置 | `GET /api/db/config`、site 相关 |
| **/qrcodes** | 小程序码管理 | 小程序码相关接口 |
| **/match** | 匹配配置或匹配池 | 匹配相关 |
| **/match-records** | 匹配记录列表 | `GET /api/db/match-records` 等 |
| **/api-doc** | API 文档 | 静态或链接 soul-api 文档 |
---
## 四、业务逻辑对齐(界面驱动)
以下为团队已对齐的规则,**以界面行为为准**,需求与开发文档与之保持一致。
### 4.1 用户/VIP 资料展示
| 规则 | 说明 |
|------|------|
| **展示以用户资料为准** | 昵称、头像、项目介绍、联系方式等一律优先使用用户资料nickname、avatar、projectIntro、phone、wechatId 等)。 |
| **不再单独存 VIP 资料列** | 数据库迁移不再新增 vip_name、vip_avatar、vip_project、vip_contact、vip_bio已有库可保留兼容。 |
| **VIP 身份与状态仍存 users** | is_vip、vip_expire_date、vip_activated_at、vip_sort、vip_role 仍保留,用于「是否 VIP、到期时间、排序、角色」。 |
| **小程序展示** | 首页/会员详情等:`name: u.nickname \|\| u.vipName \|\| '会员'`,以用户资料优先。 |
### 4.2 三端 API 边界
| 端 | 允许路径 | 禁止 |
|----|----------|------|
| 小程序 | `/api/miniprogram/*` | 禁止调用 `/api/admin/*``/api/db/*` |
| 管理端 | `/api/admin/*``/api/db/*``/api/orders` 等 | 禁止调用 `/api/miniprogram/*` |
| soul-api | 路由分组 miniprogram / admin / db按使用方挂载 | 禁止混用路径语义 |
### 4.3 免费章节与 VIP 阅读
| 规则 | 说明 |
|------|------|
| 免费章节 | 以管理端「系统设置 → 免费章节」配置为准free_chapters / chapter_config.freeChapters后端合并到章节接口。 |
| VIP 全章免费 | is_vip=1 且 vip_expire_date>NOW() 时check-purchased 视为已购买,无需再按章付费。 |
### 4.4 分销与提现
| 规则 | 说明 |
|------|------|
| 推广中心 | 管理端「推广中心」对应 distribution小程序「推广中心」对应 referral 页(海报、数据、提现)。 |
| 会员分润 | 会员订单推广者 20%、非会员 10%(可配置);内容订单推广者 90%可配置config.shareRate。 |
| 提现 | 小程序申请提现走 `/api/miniprogram/withdraw`;管理端审核/打款走 `/api/admin/withdrawals`。 |
### 4.5 资料完善引导(代码已实现)
| 规则 | 说明 |
|------|------|
| 入口统一 | `app.checkVipContactRequiredAndGuide`onLaunch 1.5s / onShow 0.5s 节流 5min / 登录成功 1.2s / VIP 支付成功) |
| 非 VIP | `checkAvatarNicknameAndGuide`:头像/昵称未完善且今日未提示 → 弹窗「请设置头像和昵称」→ navigateTo avatar-nickname |
| VIP | 头像/昵称未改 → 弹窗「完善资料」→ redirectTo profile-edit无手机号 → 弹窗引导;无微信号 → 弹窗引导 |
| 新用户 | 登录返回 isNewUser 且头像昵称未改 → redirectTo avatar-nickname无弹窗 |
| VIP 支付成功 | 弹窗「请填写好资料」→ redirectTo profile-edit?from=vip |
| 页面分工 | avatar-nickname仅头像+昵称profile-edit完整资料手机、微信号、MBTI 等) |
### 4.6 购买≥3 章解锁全书
| 规则 | 说明 |
|------|------|
| 展示条件 | 阅读页付费墙:`purchasedCount >= 3` 时显示「解锁全部 X 章」按钮 |
| 无独立弹窗 | 当前实现为按钮直接展示,购买第 3 章后自动出现;无「购买成功弹窗引导解锁全书」 |
---
## 五、与需求文档的关系
| 文档 | 关系 |
|------|------|
| **本文档(以界面定需求)** | 界面级需求基准;新增/改版界面或业务规则时先更新本文档。 |
| **1、需求/索引.md** | 主需求 = 日期最新的需求文件(如 2026-03-20-需求.md需求描述与验收标准应与本文档界面及§四业务逻辑一致。 |
| **运营与变更.md** | 近期变更、讨论结论、技术决策;涉及界面或规则时同步引用本文档。 |
---
## 六、变更记录
| 日期 | 变更内容 |
|------|----------|
| 2026-03-11 | 初版小程序与管理端界面清单、业务逻辑对齐VIP 资料以用户资料为准、三端路由、免费章与 VIP、分销提现与需求汇总、README、运营与变更同步。 |
| 2026-03-17 | 管理端清单补充:用户规则、用户余额、订单支付方式;详见《管理端迁移分析-基于小程序功能.md》。 |
| 2026-03-20 | 小程序:登录改为手机号一键登录;新增公用组件 login-modalread/my/gift-pay 引入getPhoneNumber 需耦合 agreePrivacyAuthorization。 |
| 2026-03-24 | **以代码为准反向补齐**:补充 avatar-nickname、gift-pay 系列、wallet、link-preview§4.5 资料完善引导、§4.6 购买≥3章解锁全书config.shareRate 分润展示。 |

View File

@@ -0,0 +1,19 @@
这个目录不要移动到其他地方。这个需求目录。
功能一:
![](images/2026-04-06-15-12-49.png)首先用户管理这里的这个获客列表已经移动到这个那个推广中心里面,并且把整一个获客利表移动到推广中心,并且把这个页面重构一下,然后这边的话算法配置,最终把这个算法配置给它隐藏掉,给他只显示按钮,不是隐藏掉显示按钮就不要整显示按钮。点击展开就可以了,跟那个是一样的。
![](images/2026-04-06-15-13-14.png)这个推广中心把这个上面的标签作为一些融合跟整合,然后把这个整个推广中心的页面变得那个界面风格变得简洁一些,嗯
![](images/2026-04-06-15-14-04.png)首页这里的话只保持前面总用户数到转化率,这边像纯客保获客的标签和这个你个找伙伴相应的摇钱树重新的整合到用户标签,点击统计,然后这个标签体系下老,然后重新的把这个整个的结构变得清晰可见。那变得结构清晰。
![](images/2026-04-06-15-14-42.png)包括这里的话,用户管理的话,这个用户的旅程重重难,这个也是属于数据统计,也是放到这个数据概览的标签里面帮我那个转移,并且把这个界面
![](images/2026-04-06-15-16-48.png)那我把着火犯的这个算法设计清楚,把着火的算法那匹配值这边的算法设计清晰,是随机匹配的,随机匹配按要按照这个人填写的那个标签去匹配用户,填写在用户管理里面,这个用户点击匹配标签,类似的人默认匹配标签内类似的人。如果没有标签,尽可能的去标签类似,然后男生算法是这样的,男生匹配女生,第一个的话是匹配标签算法,标签类似的人,用户旅程类似的人,行为类似的人,那匹配那个 emmbt 还性格互补的人。这个按这个算法去匹配第二个在如果这一切没有填写在匹配男女第三个的话,在匹配那个。在匹配最后没有的话再随机匹配它整个的找伙伴的算法是这样,帮我把这个重新设计一下,并且这个算法是可编辑的。这个把这个算法。解放那个右上角,然后把这个存克宝的右上角,这个存克宝的这个功能跟推广中心的功能做一个融合,直接把功能不要重复,直接融合到推广中心里面。
![](images/2026-04-06-15-17-39.png)把整个推广中心的所有的目录跟结构变得更有序,核心的一点是知道谁获客多少,谁绑定了多少,然后谁是最佳的那个推广的人,以及他的绑定收益和提现相关的功能,把整个推广中心做一个深度的一个重构。
![](images/2026-04-06-15-18-46.png)然后一定要确定这个,这里面就不用头像,就不要有这么多选项了,确保不要是裂开的头像。在 EMBTI 头像那,确保每一用户都有相应的那个 MBTI 的头像。然后把整个头像重构的极度简单,然后确保整个的那个用户在前端也是可以选择在后台的头像的,没有选择,有选就自动按照他的那个性格直接匹配,没有的话就直接选择。然后把整个界面包括那些说明缩进一点简介简洁一点,缩进说说一些进去。

View File

@@ -0,0 +1,30 @@
> **承接**`-4` 已归档 `已完成/用户管理 20260406-2.plan.md`。
> 有下一批网关/观测需求时在此表排期;无则保持空模板。
---
## 待排期
| 项 | 状态 | 说明 |
|:---|:---|:---|
![](images/2026-04-06-09-33-10.png)这个应,这个要更新一个 go购买的购买状态的用户旅程的一个模块用户旅程的模块放到里面比如他购买的任何包括我们新增的任何的那个收费项目都要显示在这个购买的状态里面那我知道所有的那个购买的人的那个收益清晰的知道他的那个收益。
功能二:
/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验-永平/static
用户的头像用户的这个头像NBTI 默认用这16个头像的NBTI 默认就用这16个头像直接就可以在后台是可以直接选择这个头像没有如果没有设置性格的话就有设置性格就匹配性格没有设置性格的话是随机选一张图片男男的女的都可以。是可以选男版可以选女版分开把这个头像做优化迭代然后把所有的这些会员没有头像的会员全部设置一下
功能三:
![](images/2026-04-06-10-31-25.png)这个 BTI 的这个头像两版根据那个男的女的自由的去分配,有选择男的女的在,可以只有这两个版本,其他的版本全部删掉,那男的女的版本,然后确保它是显示可以显示出来,那我把那个 SVG 的格式改成那个 PNG 的格式。那把这个 NBT 这个头像库,这一个简洁一点的,弄得简洁一点,现在看稿件有点太复杂了,弄得简洁一些。
![](images/2026-04-06-10-31-58.png)然后把用户,那个用户的里面去随机匹配用户列表,随机匹配,把这个没有的头像的这一些用户没有头像的随机匹配掉,然后购买状态这边要跟咱们有付款行为的都可以都要显示在这个购买状态里面,最近一次购买只显示这一个,不要显示未购买的状态。目标是确保有相应的那个头像和人设
功能四:迁移
![](images/2026-04-06-10-32-42.png)然后这个获客列表。霍克利,表帮我放到那个推广中心里面,并且整个页面重构的剪辑一些。
功能五:
![](images/2026-04-06-10-34-23.png)那个超级个体的一些那个获客情况跟那个超级个体的这个获客情况。在这个我的里面找一个比较一个不那么显眼的一个地方的页面,帮我做一个那个超级个体的一个获客情况,以及它的一个热度。这怎么一个界面在上面的一个功能?
![](images/2026-04-06-10-37-33.png)
![](images/2026-04-06-10-37-52.png)然后根据这整个的这个用户旅程总览这边的话,根据这整个的那个用户列表和这个用户目的是让我能清楚的知道所有的这个用户哪一个流量池好过什么用户流旅程哪个流量池,然后的整个用户的一个关系,目的是让我知道这一些客户的一个具体的一些详细的一个情况。然后以及表格行为表格一个分析,然后这些分析触发,最终得到这个用户估值的一整个分析的一个解决方案,然后通过用户里程来进行各个板块的分析,然后这个把这整个的用户里程的界面帮我做一些,做一下那个重构。

View File

@@ -0,0 +1,25 @@
> **承接**`-4` 已归档 `已完成/用户管理 20260406-2.plan.md`。
> 有下一批网关/观测需求时在此表排期;无则保持空模板。
---
## 待排期
| 项 | 状态 | 说明 |
|:---|:---|:---|
功能一:
这首夜路口默认的位置,这个。这个直接是要并到那个超级个体的那个目录底下的,直接就是并到超级个体的那个目录底下。超级的直接就是变成超级个体的,指定的超级个体的配置里面。而不是独立一项去做选择,就每一个超级个体都可以像做小检选择,然后把整个页面那个重构一下,简洁一点![](images/2026-04-06-04-17-32.png)
然后这里首页的入口默认这个,就不要选这一个目录,就直接合并到这里,就不要再显示了。
配置简洁一些,配置界面简洁一些,不要搞得这么复杂
功能二:
![](images/2026-04-06-05-53-15.png)这个搜索会员用户没有搜索到有问题,并不精确。嗯,搜索这个名字并不精确,没有搜索到,帮我处理一下这个问题,并且详细检查一下一系列的相关的可能出现的一个问题的一个处理。
![](images/2026-04-06-05-54-12.png)然后这个超级,这个点击这边不精确点击,就是实际的点击,现在不可能只点击都放到这个流光底下的,其他的那个点击的情况你看一下到底是什么,什么什么情况,帮我修复一下这个点击的一个问题。点击了一个相关的问题。帮你,帮我处理一下。脑以及存克宝相关的这个配置。
![](images/2026-04-06-05-55-07.png)然后还有一点的话,就是这个点,那个获客这里的话要需要和纯客保那边的获客是那个那协同的纯科宝那边具体的那个获客的数据就这里要能点击并获客,这里要可以点击展开。然后并且这个成员这里的话,也是需要能直接点击就打开这个会员的那个用户的相关的那个数据,这个需要跟那个前端的用户页,用户管理里面实际的那个用户是捆绑关系,超级个体也是归属于用户的用户列表里面的一种。
![](images/2026-04-06-05-55-49.png)然后这个功能完善之后,在咱们的那个小程序这里要反映过来,小程序这边的话能直接显示并且配置清楚那个所有的点击的这个功能可以直接展开,包括超级个体里面的话。这个功能每个超级个体点点击上去的话,配置好都是可以使用这个的功能的。

View File

@@ -0,0 +1,17 @@
功能一:
![](images/2026-04-08-04-40-30.png)
这个用户的用户详情的这个旅程与鬼用户旅程跟轨迹和用户没匹配不上。检查一下。
功能二:
![](images/2026-04-08-04-43-15.png)这里的话要确保这个行为标签是中文的,像这个 form 点击了一个按钮,不清楚。以后这个点击的这个标签的按钮得清晰一些。要变成中文的,具体点击谁的,哪一个的清晰。
![](images/2026-04-08-04-45-27.png)
小伙伴,找伙伴底下的这个找伙伴的数据概览,这个删除掉。
![](images/2026-04-08-04-46-25.png)推广中心的这个获客列表的页面重构,直接重构掉。
![](images/2026-04-08-04-47-06.png)那前面的话那个排行。推广排行的话,底下这些人编辑可以去掉,可以隐藏掉,可以忽略到排行表上面,把分数去掉,可以按那个按分数去排行。给指定人去做排行。

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Some files were not shown because too many files have changed in this diff Show More