feat: 定合并的稳定版本
This commit is contained in:
8
.cursor/agent/产品经理/evolution/2026-03-17.md
Normal file
8
.cursor/agent/产品经理/evolution/2026-03-17.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 产品经理 经验记录 - 2026-03-17
|
||||
|
||||
## 新版管理端迁移到稳定版(会议:实施方案确认)
|
||||
|
||||
- **需求基准**:以稳定版小程序为准,管理端支撑余额、代付、VIP、规则引擎等
|
||||
- **内容管理**:以稳定版为主,不采纳新版
|
||||
- **新版独有**:API 文档、OSS、编辑禁用、鉴权 → 全部吸纳(用户决议「新版有的就迁移」)
|
||||
- **待确认**:RFM、用户旅程、神射手是否继续使用
|
||||
@@ -26,3 +26,10 @@
|
||||
|
||||
- 供小程序区分发起人/好友,展示不同 UI
|
||||
- 字段:`initiatorUserId`(发起人 user_id)
|
||||
|
||||
---
|
||||
|
||||
## 新版管理端迁移 - 后端任务(会议:2026-03-17)
|
||||
|
||||
- **router 补齐**:迁移前注册 5 个路由:`db.GET("/users/rfm")`、`db.GET("/users/journey-stats")`、`admin.GET("/shensheshou/query")`、`admin.POST("/shensheshou/enrich")`、`admin.POST("/shensheshou/ingest")`
|
||||
- **待确认**:/api/admin/settings 是否已支持 ossConfig,若不支持需补充
|
||||
|
||||
@@ -19,3 +19,19 @@
|
||||
|
||||
- 小程序:代付详情页双态 UI、读页跳转
|
||||
- 后端:PayNotify beneficiaryUserID、detail initiatorUserId
|
||||
|
||||
---
|
||||
|
||||
## 新版管理端迁移到稳定版(会议:2026-03-17)
|
||||
|
||||
### 决议
|
||||
|
||||
- **内容管理**:以稳定版为主,不采纳新版
|
||||
- **新版独有**:API 文档 Tab、api-docs 独立页、OSS 配置、编辑时手机号禁用、鉴权逻辑 → **全部吸纳**
|
||||
- **后端**:迁移前补 router(users/rfm、journey-stats、shensheshou 共 5 个)
|
||||
|
||||
### 影响角色
|
||||
|
||||
- 管理端开发工程师:主导迁移
|
||||
- 后端开发:router 补齐、ossConfig 确认
|
||||
- 测试人员:迁移后验收
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
| 2026-03-17 | 吸收需求:代付美团式流程、PayNotify 权益归属、目录 loading、最新新增 5 条折叠 → 开发文档与 agent | 已完成 |
|
||||
| 2026-03-17 | 乘风吸收经验与交互:迁移完成度与待办清单、运营与变更第十二部分 | 已完成 |
|
||||
| 2026-03-17 | 吸收新需求:代付统一到代付页(gift=1&ref redirectTo)→ 需求汇总、找朋友代付流程、运营与变更 | 已完成 |
|
||||
| 2026-03-17 | 会议收尾:新版管理端迁移到稳定版实施方案确认;纪要、各角色经验入库、项目索引更新 | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-17(吸收代付与体验优化需求)
|
||||
**最后更新**:2026-03-17(会议收尾:新版管理端迁移实施方案)
|
||||
|
||||
@@ -32,6 +32,7 @@ soul-api(Go + Gin + GORM + MySQL)提供三组路由:`/api/miniprogram/*`
|
||||
| 2026-03-16 | ParseAutoLinkContent 添加 data-label;存客宝 create planType=1 sceneId=9 status=1 | 已完成 |
|
||||
| 2026-03-16 | 会议:new-soul 新需求与当前项目差异分析;content_upload.py 与 chapters 一致性待核对 | 待续 |
|
||||
| 2026-03-17 | 代付 PayNotify 权益归属修复:beneficiaryUserID(代付=发起人);gift-pay detail 返回 initiatorUserId | 已完成 |
|
||||
| 2026-03-17 | 会议:新版管理端迁移;router 补齐 users/rfm、journey-stats、shensheshou 共 5 个;确认 ossConfig | 进行中 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
管理端(React + Vite + Tailwind)主要功能:用户管理、订单管理、提现审核、VIP 管理、内容/章节管理、配置项管理、数据统计。调用 `/api/admin/*` 与 `/api/db/*` 接口,JWT Bearer 鉴权。
|
||||
|
||||
### 项目路径定义
|
||||
|
||||
| 版本 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| **稳定版(主用)** | `soul-admin/` | 根目录,线上部署 |
|
||||
| **新版(参考)** | `new-soul/soul-admin/` | 迁移时对照,含 ApiDocsPage 完整版、OSS region 等 |
|
||||
|
||||
---
|
||||
|
||||
## 开发进度
|
||||
@@ -28,9 +35,14 @@
|
||||
| 2026-03-16 | 乘风发起例行开发进度同步 | 已完成 |
|
||||
| 2026-03-16 | 链接人与事:table 布局、planId/apiKey 列、复制图标、删除 Dialog 弹窗 | 已完成 |
|
||||
| 2026-03-16 | 会议:new-soul 新需求与当前项目差异分析;派对AI 不新增管理端需求 | 已完成 |
|
||||
| 2026-03-17 | 会议:新版管理端迁移到稳定版实施方案确认;新版独有全部吸纳,内容管理以稳定版为主 | 已完成 |
|
||||
| 2026-03-17 | 吸收新版管理端定义(new-soul/soul-admin);迁移 ApiDocsPage 完整版、OSS region、鉴权失败 clearAdminToken | 已完成 |
|
||||
| 2026-03-17 | 修复 DistributionPage Order.description;用户余额人工调整(后端 adjust API + 用户详情入口);代付列表页(后端 gift-pay-requests + 推广中心 Tab) | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-16
|
||||
**最后更新**:2026-03-17
|
||||
|
||||
> 注:soul-admin 构建仍有 DistributionPage Order.description 类型错误(与本次迁移无关),待修复。
|
||||
|
||||
15
.cursor/agent/管理端开发工程师/evolution/2026-03-17.md
Normal file
15
.cursor/agent/管理端开发工程师/evolution/2026-03-17.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 管理端开发工程师 经验记录 - 2026-03-17
|
||||
|
||||
## 新版管理端迁移到稳定版(会议:实施方案确认)
|
||||
|
||||
### 迁移策略
|
||||
|
||||
- **内容管理**:以稳定版为主,不采纳新版,ContentPage 及关联组件不覆盖
|
||||
- **新版独有全部吸纳**:API 文档 Tab、api-docs 独立页、OSS 配置、编辑时手机号禁用、鉴权逻辑
|
||||
- **必须保留**:用户详情余额、订单支付方式/代付、RechargeAlert、LinkedMp、用户规则、超级个体
|
||||
|
||||
### 实施任务
|
||||
|
||||
1. 从 new-soul/soul-admin 迁移:ApiDocsPage、OSS 配置、api-docs 路由、编辑时手机号禁用、鉴权逻辑
|
||||
2. 以稳定版为基准合并,内容管理不覆盖
|
||||
3. 按模块分批:Layout/Dashboard/设置 → 用户/订单/推广/提现/找伙伴
|
||||
8
.cursor/agent/软件测试/evolution/2026-03-17.md
Normal file
8
.cursor/agent/软件测试/evolution/2026-03-17.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 软件测试 经验记录 - 2026-03-17
|
||||
|
||||
## 新版管理端迁移验收(会议:实施方案确认)
|
||||
|
||||
- **验收清单**:按《新版管理端迁移到稳定版-需求评估》§七
|
||||
- **回归范围**:提现、分销、找伙伴、导师、设置等
|
||||
- **风险**:合并时避免误覆盖稳定版独有逻辑,建议 diff 逐模块核对
|
||||
- **三端联调**:管理端 ↔ soul-api 重点验证;用户规则、订单、余额展示
|
||||
106
.cursor/meeting/2026-03-17_新版管理端迁移到稳定版实施方案确认.md
Normal file
106
.cursor/meeting/2026-03-17_新版管理端迁移到稳定版实施方案确认.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 会议纪要 - 2026-03-17 | 新版管理端迁移到稳定版实施方案确认
|
||||
|
||||
> 本文件由**助理橙子**在会议结束后自动生成。
|
||||
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **时间**:2026-03-17
|
||||
- **议题**:新版管理端迁移到稳定版 - 确认实施方案
|
||||
- **触发方式**:乘风调动开发人员开会,并确认实施方案
|
||||
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员
|
||||
|
||||
---
|
||||
|
||||
## 各角色发言
|
||||
|
||||
### 【产品经理】
|
||||
|
||||
需求基准以稳定版小程序为准;内容管理以稳定版为主;验收标准见《需求评估》§七;RFM、journey、神射手是否保留需运营确认。
|
||||
|
||||
### 【后端开发】
|
||||
|
||||
以稳定版为主,后端无需新增接口;若保留 RFM/journey/神射手需补 5 个 router;OSS 需确认 /api/admin/settings 是否支持 ossConfig;建议先补 router 再迁移。
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
内容管理以稳定版为准不覆盖;必须保留用户详情余额、订单支付方式/代付、RechargeAlert、LinkedMp 等;可吸纳编辑禁用、鉴权、API 文档、OSS;按模块分批合并。
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
本次主要影响管理端,小程序无需改动;迁移后做三端联调验证用户规则、订单、余额。
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
按《需求评估》§七验收;回归提现、分销、找伙伴等;合并时避免误覆盖稳定版独有逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- 乘风确认:后端 router 补齐建议迁移前完成
|
||||
- 产品经理:OSS 按实际部署需求,用户决议「新版有的就迁移」→ OSS 纳入
|
||||
- 管理端:同意 OSS 纳入,新版独有能力全部吸纳
|
||||
|
||||
---
|
||||
|
||||
## 会议决议
|
||||
|
||||
1. **新版有的就迁移**:API 文档 Tab、api-docs 独立页、OSS 配置、编辑时手机号禁用、鉴权逻辑优化,全部吸纳到稳定版
|
||||
2. **内容管理**:以稳定版为主,不采纳新版
|
||||
3. **后端 router**:迁移前补齐 users/rfm、users/journey-stats、shensheshou 共 5 个路由(若运营使用)
|
||||
4. **实施顺序**:Phase 0 后端补 router → Phase 1 基础模块 → Phase 2 业务模块 → Phase 3 内容保持 → Phase 4 验收
|
||||
|
||||
---
|
||||
|
||||
## 待办事项(乘风指派)
|
||||
|
||||
| 责任角色 | 任务 | 优先级 | 截止建议 |
|
||||
|---------|------|--------|---------|
|
||||
| 后端开发 | soul-api router 注册 users/rfm、users/journey-stats、shensheshou 共 5 个路由 | 高 | 迁移前 |
|
||||
| 后端开发 | 确认 /api/admin/settings 是否支持 ossConfig,若不支持则补充 | 中 | 迁移前 |
|
||||
| 管理端开发工程师 | 从 new-soul/soul-admin 迁移:ApiDocsPage、OSS 配置、api-docs 路由、编辑时手机号禁用、鉴权逻辑 | 高 | - |
|
||||
| 管理端开发工程师 | 以稳定版为基准合并,内容管理不覆盖,其他模块选择性合并 | 高 | - |
|
||||
| 测试人员 | 迁移完成后按《需求评估》§七执行验收,三端联调 | 中 | 迁移后 |
|
||||
|
||||
---
|
||||
|
||||
## 问题与作答区
|
||||
|
||||
| # | 问题 | 责任角色 | 作答 |
|
||||
|---|------|---------|------|
|
||||
| 1 | RFM、用户旅程、神射手是否继续使用? | 产品/运营 | (待补充) |
|
||||
| 2 | /api/admin/settings 是否已支持 ossConfig? | 后端开发 | (待补充) |
|
||||
|
||||
---
|
||||
|
||||
## 各角色经验与业务理解更新
|
||||
|
||||
### 产品经理
|
||||
|
||||
- 新版管理端迁移以稳定版为基准,内容管理以稳定版为主;新版独有能力(API 文档、OSS、编辑禁用、鉴权)全部吸纳
|
||||
|
||||
### 后端开发
|
||||
|
||||
- 迁移前需补 router:users/rfm、users/journey-stats、shensheshou 共 5 个;需确认 settings 的 ossConfig 支持
|
||||
|
||||
### 管理端开发工程师
|
||||
|
||||
- 迁移策略:内容管理不覆盖;其他模块以稳定版为主;吸纳新版 ApiDocsPage、OSS、编辑禁用、鉴权;按模块分批合并
|
||||
|
||||
### 小程序开发工程师
|
||||
|
||||
- 管理端迁移不影响小程序;迁移后做三端联调验证
|
||||
|
||||
### 测试人员
|
||||
|
||||
- 验收按《需求评估》§七;合并时需 diff 核对避免误覆盖
|
||||
|
||||
### 团队共享
|
||||
|
||||
- 新版管理端迁移到稳定版:内容管理以稳定版为主,新版独有能力全部吸纳;详见 agent/团队/evolution/2026-03-17.md
|
||||
|
||||
---
|
||||
|
||||
*会议纪要由助理橙子生成 | 各角色经验已同步至 `agent/{角色}/evolution/2026-03-17.md`*
|
||||
@@ -75,3 +75,4 @@ YYYY-MM-DD_会议主题.md
|
||||
| 2026-03-11 | 开发团队对齐业务逻辑与以界面定需求·会议收尾 | 产品、后端、管理端、小程序、团队 | [2026-03-11_开发团队对齐业务逻辑与以界面定需求会议收尾.md](2026-03-11_开发团队对齐业务逻辑与以界面定需求会议收尾.md) |
|
||||
| 2026-03-16 | 链接人与事与存客宝对接优化 | 管理端、后端、团队 | [2026-03-16_链接人与事与存客宝对接优化.md](2026-03-16_链接人与事与存客宝对接优化.md) |
|
||||
| 2026-03-16 | new-soul 新需求与当前项目差异分析 | 产品、后端、管理端、小程序、测试 | [2026-03-16_new-soul新需求与当前项目差异分析.md](2026-03-16_new-soul新需求与当前项目差异分析.md) |
|
||||
| 2026-03-17 | 新版管理端迁移到稳定版实施方案确认 | 产品、后端、管理端、小程序、测试 | [2026-03-17_新版管理端迁移到稳定版实施方案确认.md](2026-03-17_新版管理端迁移到稳定版实施方案确认.md) |
|
||||
|
||||
@@ -15,9 +15,10 @@ alwaysApply: true
|
||||
| 子项目 | 目录 | 用途 | 后端对接 |
|
||||
|--------|------|------|----------|
|
||||
| 小程序 | miniprogram/ | 微信原生小程序 C 端 | soul-api |
|
||||
| 管理端 | soul-admin/ | React 管理后台 | soul-api |
|
||||
| 管理端 | soul-admin/ | React 管理后台(稳定版,主用) | soul-api |
|
||||
| API 后端 | soul-api/ | Go + Gin + GORM 接口服务 | - |
|
||||
| 预览/参考 | next-project/ | 仅预览,非线上 | 不依赖 |
|
||||
| **新版管理端** | **new-soul/soul-admin/** | 新版参考实现,迁移时对照 | soul-api |
|
||||
|
||||
## 核心原则
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"pages/my/my",
|
||||
"pages/read/read",
|
||||
"pages/link-preview/link-preview",
|
||||
"pages/about/about",
|
||||
"pages/agreement/agreement",
|
||||
"pages/privacy/privacy",
|
||||
"pages/referral/referral",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<text class="doc-p">我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。</text>
|
||||
|
||||
<text class="doc-section">七、联系我们</text>
|
||||
<text class="doc-p">如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
<text class="doc-p">如有疑问,请通过 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -44,7 +44,10 @@ Page({
|
||||
dailyChapters: [],
|
||||
|
||||
// book/parts 加载中
|
||||
partsLoading: true
|
||||
partsLoading: true,
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -57,6 +60,24 @@ Page({
|
||||
this.loadVipStatus()
|
||||
this.loadParts()
|
||||
this.loadDailyChapters()
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
|
||||
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
|
||||
return
|
||||
}
|
||||
const res = await app.request({ url: '/api/miniprogram/config', silent: true })
|
||||
const features = (res && res.features) || {}
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
this.setData({ searchEnabled })
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 懒加载:仅拉取篇章列表 + totalSections + fixedSections
|
||||
@@ -280,6 +301,7 @@ Page({
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
if (!this.data.searchEnabled) return
|
||||
trackClick('chapters', 'nav_click', '搜索')
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<view class="nav-left">
|
||||
<view class="search-btn" bindtap="goToSearch">
|
||||
<view class="search-btn" wx:if="{{searchEnabled}}" bindtap="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -65,7 +65,10 @@ Page({
|
||||
featuredExpanded: false,
|
||||
latestExpanded: false,
|
||||
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
|
||||
featuredExpandedLoading: false
|
||||
featuredExpandedLoading: false,
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -84,6 +87,7 @@ Page({
|
||||
}
|
||||
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.loadFeatureConfig()
|
||||
this.initData()
|
||||
},
|
||||
|
||||
@@ -308,8 +312,26 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
|
||||
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
|
||||
return
|
||||
}
|
||||
const res = await app.request({ url: '/api/miniprogram/config', silent: true })
|
||||
const features = (res && res.features) || {}
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
this.setData({ searchEnabled })
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
if (!this.data.searchEnabled) return
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
@@ -330,10 +352,6 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
goToAbout() {
|
||||
wx.navigateTo({ url: '/pages/about/about' })
|
||||
},
|
||||
|
||||
async onLinkKaruo() {
|
||||
const app = getApp()
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" bindtap="goToSearch">
|
||||
<!-- 搜索栏(根据配置显示) -->
|
||||
<view class="search-bar" wx:if="{{searchEnabled}}" bindtap="goToSearch">
|
||||
<view class="search-icon-wrap">
|
||||
<text class="search-icon-text">🔍</text>
|
||||
</view>
|
||||
|
||||
@@ -38,6 +38,8 @@ Page({
|
||||
|
||||
// 功能配置
|
||||
matchEnabled: false,
|
||||
referralEnabled: true,
|
||||
searchEnabled: true,
|
||||
|
||||
// VIP状态
|
||||
isVip: false,
|
||||
@@ -113,10 +115,14 @@ Page({
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/config')
|
||||
const features = (res && res.features) || (res && res.data && res.data.features) || {}
|
||||
this.setData({ matchEnabled: features.matchEnabled === true })
|
||||
const matchEnabled = features.matchEnabled === true
|
||||
const referralEnabled = features.referralEnabled !== false
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
app.globalData.features = { matchEnabled, referralEnabled, searchEnabled }
|
||||
this.setData({ matchEnabled, referralEnabled, searchEnabled })
|
||||
} catch (error) {
|
||||
console.log('加载功能配置失败:', error)
|
||||
this.setData({ matchEnabled: false })
|
||||
this.setData({ matchEnabled: false, referralEnabled: true, searchEnabled: true })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -759,7 +765,7 @@ Page({
|
||||
handleMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
if (!this.data.isLoggedIn && id !== 'about') {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
@@ -770,7 +776,6 @@ Page({
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
wallet: '/pages/wallet/wallet',
|
||||
about: '/pages/about/about',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
@@ -792,11 +797,6 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 跳转到关于页
|
||||
goToAbout() {
|
||||
wx.navigateTo({ url: '/pages/about/about' })
|
||||
},
|
||||
|
||||
// 跳转到匹配
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
@@ -808,6 +808,7 @@ Page({
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
if (!this.data.referralEnabled) return
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
<text class="profile-stat-val">{{readCountText}}</text>
|
||||
<text class="profile-stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="goToReferral">
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="goToReferral">
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
@@ -148,7 +148,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的订单 + 关于作者 + 设置 -->
|
||||
<!-- 我的订单 + 设置 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="menu-left">
|
||||
@@ -164,13 +164,6 @@
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="about">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-blue"><image class="menu-icon-img" src="/assets/icons/info-blue.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">关于作者</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" wx:if="{{showSettingsEntry}}" bindtap="handleMenuTap" data-id="settings">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-gray"><image class="menu-icon-img" src="/assets/icons/settings-gray.svg" mode="aspectFit"/></view>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<text class="doc-p">我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。</text>
|
||||
|
||||
<text class="doc-section">八、联系我们</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
914
soul-admin/dist/assets/index-BC7lp6bO.js
vendored
Normal file
914
soul-admin/dist/assets/index-BC7lp6bO.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-BHi-SnBy.css
vendored
1
soul-admin/dist/assets/index-BHi-SnBy.css
vendored
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-BcWuvM_a.css
vendored
Normal file
1
soul-admin/dist/assets/index-BcWuvM_a.css
vendored
Normal file
File diff suppressed because one or more lines are too long
779
soul-admin/dist/assets/index-C2K6IQif.js
vendored
779
soul-admin/dist/assets/index-C2K6IQif.js
vendored
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Soul创业派对</title>
|
||||
<script type="module" crossorigin src="/assets/index-C2K6IQif.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BHi-SnBy.css">
|
||||
<script type="module" crossorigin src="/assets/index-BC7lp6bO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BcWuvM_a.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MentorsPage } from './pages/mentors/MentorsPage'
|
||||
import { MentorConsultationsPage } from './pages/mentor-consultations/MentorConsultationsPage'
|
||||
import { FindPartnerPage } from './pages/find-partner/FindPartnerPage'
|
||||
import { ApiDocPage } from './pages/api-doc/ApiDocPage'
|
||||
import { ApiDocsPage } from './pages/api-docs/ApiDocsPage'
|
||||
import { NotFoundPage } from './pages/not-found/NotFoundPage'
|
||||
|
||||
function App() {
|
||||
@@ -47,6 +48,7 @@ function App() {
|
||||
<Route path="match" element={<MatchPage />} />
|
||||
<Route path="match-records" element={<MatchRecordsPage />} />
|
||||
<Route path="api-doc" element={<ApiDocPage />} />
|
||||
<Route path="api-docs" element={<ApiDocsPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -123,6 +123,12 @@ export function UserDetailModal({
|
||||
const [vipForm, setVipForm] = useState({ isVip: false, vipExpireDate: '', vipRole: '', vipName: '', vipProject: '', vipContact: '', vipBio: '' })
|
||||
const [vipRoles, setVipRoles] = useState<{ id: number; name: string }[]>([])
|
||||
|
||||
// 调整余额
|
||||
const [adjustBalanceOpen, setAdjustBalanceOpen] = useState(false)
|
||||
const [adjustAmount, setAdjustAmount] = useState('')
|
||||
const [adjustRemark, setAdjustRemark] = useState('')
|
||||
const [adjustLoading, setAdjustLoading] = useState(false)
|
||||
|
||||
// 用户资料完善(神射手)
|
||||
const [sssLoading, setSssLoading] = useState(false)
|
||||
const [sssData, setSssData] = useState<ShensheShouData | null>(null)
|
||||
@@ -287,6 +293,29 @@ export function UserDetailModal({
|
||||
} catch { toast.error('修改失败') } finally { setPasswordSaving(false) }
|
||||
}
|
||||
|
||||
async function handleAdjustBalance() {
|
||||
if (!user) return
|
||||
const amt = parseFloat(adjustAmount)
|
||||
if (Number.isNaN(amt) || amt === 0) { toast.error('请输入有效金额(正数增加、负数扣减)'); return }
|
||||
setAdjustLoading(true)
|
||||
try {
|
||||
const res = await post<{ success?: boolean; error?: string }>(`/api/admin/users/${user.id}/balance/adjust`, {
|
||||
amount: amt,
|
||||
remark: adjustRemark || undefined,
|
||||
})
|
||||
if (res?.success) {
|
||||
toast.success('余额已调整')
|
||||
setAdjustBalanceOpen(false)
|
||||
setAdjustAmount('')
|
||||
setAdjustRemark('')
|
||||
loadUserDetail()
|
||||
onUserUpdated?.()
|
||||
} else {
|
||||
toast.error('调整失败: ' + (res?.error || ''))
|
||||
}
|
||||
} catch { toast.error('调整失败') } finally { setAdjustLoading(false) }
|
||||
}
|
||||
|
||||
// 用户资料完善查询(支持多维度)
|
||||
async function handleSSSQuery() {
|
||||
if (!sssQueryPhone && !sssQueryOpenId && !sssQueryWechatId) {
|
||||
@@ -382,6 +411,7 @@ export function UserDetailModal({
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={() => onClose()}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-4xl max-h-[90vh] overflow-hidden">
|
||||
<DialogHeader>
|
||||
@@ -468,7 +498,11 @@ export function UserDetailModal({
|
||||
placeholder="输入手机号"
|
||||
value={editPhone}
|
||||
onChange={(e) => setEditPhone(e.target.value)}
|
||||
disabled={!!user?.phone}
|
||||
/>
|
||||
{user?.phone && (
|
||||
<p className="text-xs text-gray-500">编辑时手机号不可修改</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">昵称</Label>
|
||||
@@ -521,11 +555,21 @@ export function UserDetailModal({
|
||||
¥{(user.pendingEarnings ?? 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||
<p className="text-gray-400 text-sm">当前余额</p>
|
||||
<p className="text-2xl font-bold text-[#38bdac]">
|
||||
¥{(balanceData?.balance ?? 0).toFixed(2)}
|
||||
</p>
|
||||
<div className="p-4 bg-[#0a1628] rounded-lg flex flex-col justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">当前余额</p>
|
||||
<p className="text-2xl font-bold text-[#38bdac]">
|
||||
¥{(balanceData?.balance ?? 0).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2 border-[#38bdac]/50 text-[#38bdac] hover:bg-[#38bdac]/10 text-xs"
|
||||
onClick={() => { setAdjustAmount(''); setAdjustRemark(''); setAdjustBalanceOpen(true) }}
|
||||
>
|
||||
调整余额
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-4 bg-[#0a1628] rounded-lg">
|
||||
<p className="text-gray-400 text-sm">创建时间</p>
|
||||
@@ -1084,5 +1128,44 @@ export function UserDetailModal({
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={adjustBalanceOpen} onOpenChange={setAdjustBalanceOpen}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white" showCloseButton>
|
||||
<DialogHeader>
|
||||
<DialogTitle>调整余额</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label className="text-gray-300 text-sm">调整金额(元)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="bg-[#0a1628] border-gray-700 text-white mt-1"
|
||||
placeholder="正数增加,负数扣减,如 10 或 -5"
|
||||
value={adjustAmount}
|
||||
onChange={(e) => setAdjustAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-300 text-sm">备注(可选)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white mt-1"
|
||||
placeholder="如:活动补偿"
|
||||
value={adjustRemark}
|
||||
onChange={(e) => setAdjustRemark(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setAdjustBalanceOpen(false)} className="border-gray-600 text-gray-300">
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleAdjustBalance} disabled={adjustLoading} className="bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||||
{adjustLoading ? '提交中...' : '确认调整'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
GitMerge,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
import { clearAdminToken } from '@/api/auth'
|
||||
import { clearAdminToken, getAdminToken } from '@/api/auth'
|
||||
import { RechargeAlert } from '@/components/RechargeAlert'
|
||||
|
||||
// 主菜单(5 项平铺,按 Mycontent-temp 新规范)
|
||||
@@ -36,22 +36,31 @@ export function AdminLayout() {
|
||||
if (!mounted) return
|
||||
setAuthChecked(false)
|
||||
let cancelled = false
|
||||
// 鉴权优化:先检查 token,无 token 直接跳登录,避免无效请求
|
||||
if (!getAdminToken()) {
|
||||
navigate('/login', { replace: true })
|
||||
return
|
||||
}
|
||||
get<{ success?: boolean }>('/api/admin')
|
||||
.then((data) => {
|
||||
if (cancelled) return
|
||||
if (data && (data as { success?: boolean }).success !== false) {
|
||||
setAuthChecked(true)
|
||||
} else {
|
||||
navigate('/login', { replace: true })
|
||||
clearAdminToken()
|
||||
navigate('/login', { replace: true, state: { from: location.pathname } })
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) navigate('/login', { replace: true })
|
||||
if (!cancelled) {
|
||||
clearAdminToken()
|
||||
navigate('/login', { replace: true, state: { from: location.pathname } })
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [mounted, navigate])
|
||||
}, [location.pathname, mounted, navigate])
|
||||
|
||||
const handleLogout = async () => {
|
||||
clearAdminToken()
|
||||
|
||||
443
soul-admin/src/pages/api-docs/ApiDocsPage.tsx
Normal file
443
soul-admin/src/pages/api-docs/ApiDocsPage.tsx
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* API 接口完整文档页 - 内容管理相关接口
|
||||
* 深色主题,与 Admin 整体风格一致
|
||||
* 来源:new-soul/soul-admin
|
||||
*/
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { BookOpen, User, Tag, Search, Trophy, Smartphone, Key } from 'lucide-react'
|
||||
|
||||
interface EndpointBlockProps {
|
||||
method: string
|
||||
url: string
|
||||
desc?: string
|
||||
headers?: string[]
|
||||
body?: string
|
||||
response?: string
|
||||
}
|
||||
|
||||
function EndpointBlock({ method, url, desc, headers, body, response }: EndpointBlockProps) {
|
||||
const methodColor =
|
||||
method === 'GET'
|
||||
? 'text-emerald-400'
|
||||
: method === 'POST'
|
||||
? 'text-amber-400'
|
||||
: method === 'PUT'
|
||||
? 'text-blue-400'
|
||||
: method === 'DELETE'
|
||||
? 'text-rose-400'
|
||||
: 'text-gray-400'
|
||||
return (
|
||||
<div className="rounded-lg bg-[#0a1628]/60 border border-gray-700/50 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`font-mono font-semibold ${methodColor}`}>{method}</span>
|
||||
<code className="text-sm text-[#38bdac] break-all">{url}</code>
|
||||
</div>
|
||||
{desc && <p className="text-gray-400 text-sm">{desc}</p>}
|
||||
{headers && headers.length > 0 && (
|
||||
<div>
|
||||
<p className="text-gray-500 text-xs mb-1">Headers</p>
|
||||
<pre className="text-xs text-gray-300 font-mono overflow-x-auto p-2 rounded bg-black/30">
|
||||
{headers.join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{body && (
|
||||
<div>
|
||||
<p className="text-gray-500 text-xs mb-1">Request Body (JSON)</p>
|
||||
<pre className="text-xs text-green-400/90 font-mono overflow-x-auto p-2 rounded bg-black/30 whitespace-pre-wrap">
|
||||
{body}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{response && (
|
||||
<div>
|
||||
<p className="text-gray-500 text-xs mb-1">Response Example</p>
|
||||
<pre className="text-xs text-amber-200/80 font-mono overflow-x-auto p-2 rounded bg-black/30 whitespace-pre-wrap">
|
||||
{response}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApiDocsPage() {
|
||||
const baseHeaders = ['Authorization: Bearer {token}', 'Content-Type: application/json']
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full bg-[#0a1628] text-white">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-white">API 接口文档</h1>
|
||||
<p className="text-gray-400 mt-1">
|
||||
内容管理相关接口 · RESTful · 基础路径 /api · 管理端需 Bearer Token
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 1. Authentication */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Key className="w-5 h-5 text-[#38bdac]" />
|
||||
1. Authentication
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/admin"
|
||||
desc="登录,返回 JWT token"
|
||||
headers={['Content-Type: application/json']}
|
||||
body={`{
|
||||
"username": "admin",
|
||||
"password": "your_password"
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_at": "2026-03-16T12:00:00Z"
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2. Chapters */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<BookOpen className="w-5 h-5 text-[#38bdac]" />
|
||||
2. 章节管理 (Chapters)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/db/book?action=chapters"
|
||||
desc="获取章节树"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "part-1", "title": "第一篇", "children": [...] },
|
||||
{ "id": "section-1", "title": "第1节", "price": 1.0, "isFree": false }
|
||||
]
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/db/book?action=section&id={id}"
|
||||
desc="获取单篇内容"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "section-1",
|
||||
"title": "标题",
|
||||
"content": "正文...",
|
||||
"price": 1.0,
|
||||
"isFree": false,
|
||||
"partId": "part-1",
|
||||
"chapterId": "ch-1"
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/db/book"
|
||||
desc="新建章节 (action=create-section)"
|
||||
headers={baseHeaders}
|
||||
body={`{
|
||||
"action": "create-section",
|
||||
"title": "新章节标题",
|
||||
"content": "正文内容",
|
||||
"price": 0,
|
||||
"isFree": true,
|
||||
"partId": "part-1",
|
||||
"chapterId": "ch-1",
|
||||
"partTitle": "第一篇",
|
||||
"chapterTitle": "第1章"
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "id": "section-new-id", "title": "新章节标题", ... }
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/db/book"
|
||||
desc="更新章节内容 (action=update-section)"
|
||||
headers={baseHeaders}
|
||||
body={`{
|
||||
"action": "update-section",
|
||||
"id": "section-1",
|
||||
"title": "更新后的标题",
|
||||
"content": "更新后的正文",
|
||||
"price": 1.0,
|
||||
"isFree": false
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "id": "section-1", "title": "更新后的标题", ... }
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/db/book"
|
||||
desc="删除章节 (action=delete-section)"
|
||||
headers={baseHeaders}
|
||||
body={`{
|
||||
"action": "delete-section",
|
||||
"id": "section-1"
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"message": "已删除"
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/admin/content/upload"
|
||||
desc="上传图片(管理端)"
|
||||
headers={baseHeaders}
|
||||
body={`FormData: file (binary)`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"url": "/uploads/images/xxx.jpg",
|
||||
"data": { "url", "fileName", "size", "type" }
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3. Persons */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<User className="w-5 h-5 text-[#38bdac]" />
|
||||
3. 人物管理 (@Mentions)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/db/persons"
|
||||
desc="人物列表"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "personId": "p1", "label": "张三", "aliases": ["老张"], ... }
|
||||
]
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/db/person?personId={id}"
|
||||
desc="人物详情"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": {
|
||||
"personId": "p1",
|
||||
"label": "张三",
|
||||
"aliases": ["老张"],
|
||||
"description": "..."
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/db/persons"
|
||||
desc="新增/更新人物(含 aliases 字段)"
|
||||
headers={baseHeaders}
|
||||
body={`{
|
||||
"personId": "p1",
|
||||
"label": "张三",
|
||||
"aliases": ["老张", "张三丰"],
|
||||
"description": "可选描述"
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "personId": "p1", "label": "张三", ... }
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="DELETE"
|
||||
url="/api/db/persons?personId={id}"
|
||||
desc="删除人物"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"message": "已删除"
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 4. LinkTags */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Tag className="w-5 h-5 text-[#38bdac]" />
|
||||
4. 链接标签 (#LinkTags)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/db/link-tags"
|
||||
desc="标签列表"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "tagId": "t1", "label": "官网", "aliases": [], "type": "url", "url": "https://..." }
|
||||
]
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/db/link-tags"
|
||||
desc="新增/更新标签(含 aliases, type: url/miniprogram/ckb)"
|
||||
headers={baseHeaders}
|
||||
body={`{
|
||||
"tagId": "t1",
|
||||
"label": "官网",
|
||||
"aliases": ["官方网站"],
|
||||
"type": "url",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
|
||||
// type 可选: url | miniprogram | ckb`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "tagId": "t1", "label": "官网", "type": "url", ... }
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="DELETE"
|
||||
url="/api/db/link-tags?tagId={id}"
|
||||
desc="删除标签"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"message": "已删除"
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 5. Search */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Search className="w-5 h-5 text-[#38bdac]" />
|
||||
5. 内容搜索
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/search?q={keyword}"
|
||||
desc="搜索(标题优先 3 条 + 内容匹配)"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": {
|
||||
"titleMatches": [{ "id": "s1", "title": "...", "snippet": "..." }],
|
||||
"contentMatches": [{ "id": "s2", "title": "...", "snippet": "..." }]
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 6. Ranking */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Trophy className="w-5 h-5 text-[#38bdac]" />
|
||||
6. 内容排行
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/db/book?action=ranking"
|
||||
desc="排行榜数据"
|
||||
headers={baseHeaders}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "s1", "title": "...", "clickCount": 100, "payCount": 50, "hotScore": 120, "hotRank": 1 }
|
||||
]
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 7. Miniprogram */}
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl mb-6">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Smartphone className="w-5 h-5 text-[#38bdac]" />
|
||||
7. 小程序接口
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/miniprogram/book/all-chapters"
|
||||
desc="全部章节(小程序用)"
|
||||
headers={['Content-Type: application/json']}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": [ { "id": "s1", "title": "...", "price": 1.0, "isFree": false }, ... ]
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="GET"
|
||||
url="/api/miniprogram/balance?userId={id}"
|
||||
desc="查余额"
|
||||
headers={['Content-Type: application/json']}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "balance": 100.50, "userId": "xxx" }
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/miniprogram/balance/gift"
|
||||
desc="代付"
|
||||
headers={['Content-Type: application/json']}
|
||||
body={`{
|
||||
"userId": "xxx",
|
||||
"amount": 10.00,
|
||||
"remark": "可选备注"
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "balance": 110.50 }
|
||||
}`}
|
||||
/>
|
||||
<EndpointBlock
|
||||
method="POST"
|
||||
url="/api/miniprogram/balance/gift/redeem"
|
||||
desc="领取代付"
|
||||
headers={['Content-Type: application/json']}
|
||||
body={`{
|
||||
"code": "GIFT_XXXX"
|
||||
}`}
|
||||
response={`{
|
||||
"success": true,
|
||||
"data": { "amount": 10.00, "balance": 120.50 }
|
||||
}`}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<p className="text-gray-500 text-xs mt-6">
|
||||
管理端仅使用 /api/admin/*、/api/db/*;小程序使用 /api/miniprogram/*。完整实现见 soul-api 源码。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
TrendingUp,
|
||||
Clock,
|
||||
Wallet,
|
||||
Gift,
|
||||
Search,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
@@ -108,6 +109,7 @@ interface Order {
|
||||
bookName?: string
|
||||
chapterTitle?: string
|
||||
sectionTitle?: string
|
||||
description?: string
|
||||
amount: number
|
||||
status: string
|
||||
paymentMethod?: string
|
||||
@@ -122,7 +124,7 @@ interface Order {
|
||||
}
|
||||
|
||||
export function DistributionPage() {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'orders' | 'bindings' | 'withdrawals' | 'settings'>(
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'orders' | 'bindings' | 'withdrawals' | 'giftPay' | 'settings'>(
|
||||
'overview',
|
||||
)
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
@@ -144,6 +146,25 @@ export function DistributionPage() {
|
||||
const [rejectWithdrawalId, setRejectWithdrawalId] = useState<string | null>(null)
|
||||
const [rejectReason, setRejectReason] = useState('')
|
||||
const [rejectLoading, setRejectLoading] = useState(false)
|
||||
const [giftPayRequests, setGiftPayRequests] = useState<Array<{
|
||||
id: string
|
||||
requestSn: string
|
||||
initiatorUserId: string
|
||||
initiatorNick?: string
|
||||
productType: string
|
||||
productId: string
|
||||
amount: number
|
||||
description: string
|
||||
status: string
|
||||
payerUserId?: string
|
||||
payerNick?: string
|
||||
orderId?: string
|
||||
expireAt: string
|
||||
createdAt: string
|
||||
}>>([])
|
||||
const [giftPayPage, setGiftPayPage] = useState(1)
|
||||
const [giftPayTotal, setGiftPayTotal] = useState(0)
|
||||
const [giftPayStatusFilter, setGiftPayStatusFilter] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadInitialData()
|
||||
@@ -161,7 +182,10 @@ export function DistributionPage() {
|
||||
if (['orders', 'bindings', 'withdrawals'].includes(activeTab)) {
|
||||
loadTabData(activeTab, true)
|
||||
}
|
||||
}, [page, pageSize, statusFilter, searchTerm])
|
||||
if (activeTab === 'giftPay') {
|
||||
loadTabData('giftPay', true)
|
||||
}
|
||||
}, [page, pageSize, statusFilter, searchTerm, giftPayPage, giftPayStatusFilter])
|
||||
|
||||
async function loadInitialData() {
|
||||
setError(null)
|
||||
@@ -284,6 +308,30 @@ export function DistributionPage() {
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'giftPay': {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: String(giftPayPage),
|
||||
pageSize: '20',
|
||||
...(giftPayStatusFilter && { status: giftPayStatusFilter }),
|
||||
})
|
||||
const res = await get<{ success?: boolean; data?: typeof giftPayRequests; total?: number }>(
|
||||
`/api/admin/gift-pay-requests?${params}`,
|
||||
)
|
||||
if (res?.success && res.data) {
|
||||
setGiftPayRequests(res.data)
|
||||
setGiftPayTotal(res.total ?? res.data.length)
|
||||
} else {
|
||||
setGiftPayRequests([])
|
||||
setGiftPayTotal(0)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError('加载代付请求失败')
|
||||
setGiftPayRequests([])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
setLoadedTabs((prev) => new Set(prev).add(tab))
|
||||
} catch (e) {
|
||||
@@ -470,6 +518,7 @@ export function DistributionPage() {
|
||||
{ key: 'orders', label: '订单管理', icon: DollarSign },
|
||||
{ key: 'bindings', label: '绑定管理', icon: Link2 },
|
||||
{ key: 'withdrawals', label: '提现审核', icon: Wallet },
|
||||
{ key: 'giftPay', label: '代付请求', icon: Gift },
|
||||
{ key: 'settings', label: '推广设置', icon: Settings },
|
||||
].map((tab) => (
|
||||
<button
|
||||
@@ -479,6 +528,10 @@ export function DistributionPage() {
|
||||
setActiveTab(tab.key as typeof activeTab)
|
||||
setStatusFilter('all')
|
||||
setSearchTerm('')
|
||||
if (tab.key === 'giftPay') {
|
||||
setGiftPayStatusFilter('')
|
||||
setGiftPayPage(1)
|
||||
}
|
||||
}}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === tab.key
|
||||
@@ -1213,6 +1266,97 @@ export function DistributionPage() {
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'giftPay' && (
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-[#0f2137] border-gray-700/50">
|
||||
<CardHeader>
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<CardTitle className="text-white">代付请求列表</CardTitle>
|
||||
<div className="flex gap-2 items-center">
|
||||
<select
|
||||
className="bg-[#0a1628] border border-gray-700 text-white rounded px-3 py-1.5 text-sm"
|
||||
value={giftPayStatusFilter}
|
||||
onChange={(e) => { setGiftPayStatusFilter(e.target.value); setGiftPayPage(1) }}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待支付</option>
|
||||
<option value="paid">已支付</option>
|
||||
<option value="cancelled">已取消</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
<Button size="sm" variant="outline" onClick={() => loadTabData('giftPay', true)} className="border-gray-600 text-gray-300">
|
||||
<RefreshCw className="w-4 h-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-700/50">
|
||||
<th className="p-4 text-left font-medium text-gray-400">请求号</th>
|
||||
<th className="p-4 text-left font-medium text-gray-400">发起人</th>
|
||||
<th className="p-4 text-left font-medium text-gray-400">商品/金额</th>
|
||||
<th className="p-4 text-left font-medium text-gray-400">代付人</th>
|
||||
<th className="p-4 text-left font-medium text-gray-400">状态</th>
|
||||
<th className="p-4 text-left font-medium text-gray-400">创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700/50">
|
||||
{giftPayRequests.map((r) => (
|
||||
<tr key={r.id} className="hover:bg-[#0a1628]">
|
||||
<td className="p-4 font-mono text-xs text-gray-400">{r.requestSn}</td>
|
||||
<td className="p-4">
|
||||
<p className="text-white text-sm">{r.initiatorNick || r.initiatorUserId}</p>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<p className="text-white">{r.productType} · ¥{r.amount.toFixed(2)}</p>
|
||||
{r.description && <p className="text-gray-500 text-xs">{r.description}</p>}
|
||||
</td>
|
||||
<td className="p-4 text-gray-400">{r.payerNick || (r.payerUserId ? r.payerUserId : '-')}</td>
|
||||
<td className="p-4">
|
||||
<Badge
|
||||
className={
|
||||
r.status === 'paid'
|
||||
? 'bg-green-500/20 text-green-400 border-0'
|
||||
: r.status === 'pending'
|
||||
? 'bg-amber-500/20 text-amber-400 border-0'
|
||||
: 'bg-gray-500/20 text-gray-400 border-0'
|
||||
}
|
||||
>
|
||||
{r.status === 'paid' ? '已支付' : r.status === 'pending' ? '待支付' : r.status === 'cancelled' ? '已取消' : '已过期'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4 text-gray-400 text-sm">
|
||||
{r.createdAt ? new Date(r.createdAt).toLocaleString('zh-CN') : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{giftPayRequests.length === 0 && !loading && (
|
||||
<p className="text-center py-8 text-gray-500">暂无代付请求</p>
|
||||
)}
|
||||
{giftPayTotal > 20 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
page={giftPayPage}
|
||||
totalPages={Math.ceil(giftPayTotal / 20)}
|
||||
total={giftPayTotal}
|
||||
pageSize={20}
|
||||
onPageChange={setGiftPayPage}
|
||||
onPageSizeChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -34,10 +34,13 @@ import {
|
||||
Smartphone,
|
||||
ShieldCheck,
|
||||
Link2,
|
||||
FileText,
|
||||
Cloud,
|
||||
} from 'lucide-react'
|
||||
import { get, post } from '@/api/client'
|
||||
import { AuthorSettingsPage } from '@/pages/author-settings/AuthorSettingsPage'
|
||||
import { AdminUsersPage } from '@/pages/admin-users/AdminUsersPage'
|
||||
import { ApiDocsPage } from '@/pages/api-docs/ApiDocsPage'
|
||||
|
||||
interface AuthorInfo {
|
||||
name?: string
|
||||
@@ -60,7 +63,6 @@ interface FeatureConfig {
|
||||
matchEnabled: boolean
|
||||
referralEnabled: boolean
|
||||
searchEnabled: boolean
|
||||
aboutEnabled: boolean
|
||||
}
|
||||
|
||||
interface MpConfig {
|
||||
@@ -70,6 +72,14 @@ interface MpConfig {
|
||||
minWithdraw?: number
|
||||
}
|
||||
|
||||
interface OssConfig {
|
||||
endpoint?: string
|
||||
bucket?: string
|
||||
region?: string
|
||||
accessKeyId?: string
|
||||
accessKeySecret?: string
|
||||
}
|
||||
|
||||
const defaultMpConfig: MpConfig = {
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
|
||||
@@ -98,10 +108,9 @@ const defaultFeatures: FeatureConfig = {
|
||||
matchEnabled: true,
|
||||
referralEnabled: true,
|
||||
searchEnabled: true,
|
||||
aboutEnabled: true,
|
||||
}
|
||||
|
||||
const TAB_KEYS = ['system', 'author', 'admin'] as const
|
||||
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
|
||||
type TabKey = (typeof TAB_KEYS)[number]
|
||||
|
||||
export function SettingsPage() {
|
||||
@@ -112,6 +121,7 @@ export function SettingsPage() {
|
||||
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
|
||||
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
|
||||
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMpConfig)
|
||||
const [ossConfig, setOssConfig] = useState<OssConfig>({})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
@@ -135,12 +145,15 @@ export function SettingsPage() {
|
||||
featureConfig?: Partial<FeatureConfig>
|
||||
siteSettings?: { sectionPrice?: number; baseBookPrice?: number; distributorShare?: number; authorInfo?: AuthorInfo; ckbLeadApiKey?: string }
|
||||
mpConfig?: Partial<MpConfig>
|
||||
ossConfig?: Partial<OssConfig>
|
||||
}>('/api/admin/settings')
|
||||
if (!res || (res as { success?: boolean }).success === false) return
|
||||
if (res.featureConfig && Object.keys(res.featureConfig).length)
|
||||
setFeatureConfig((prev) => ({ ...prev, ...res.featureConfig }))
|
||||
if (res.mpConfig && typeof res.mpConfig === 'object')
|
||||
setMpConfig((prev) => ({ ...prev, ...res.mpConfig }))
|
||||
if (res.ossConfig && typeof res.ossConfig === 'object')
|
||||
setOssConfig((prev) => ({ ...prev, ...res.ossConfig }))
|
||||
if (res.siteSettings && typeof res.siteSettings === 'object') {
|
||||
const s = res.siteSettings
|
||||
setLocalSettings((prev) => ({
|
||||
@@ -211,6 +224,15 @@ export function SettingsPage() {
|
||||
mchId: mpConfig.mchId || '',
|
||||
minWithdraw: typeof mpConfig.minWithdraw === 'number' ? mpConfig.minWithdraw : 10,
|
||||
},
|
||||
ossConfig: Object.keys(ossConfig).length
|
||||
? {
|
||||
endpoint: ossConfig.endpoint ?? '',
|
||||
bucket: ossConfig.bucket ?? '',
|
||||
region: ossConfig.region ?? '',
|
||||
accessKeyId: ossConfig.accessKeyId ?? '',
|
||||
accessKeySecret: ossConfig.accessKeySecret ?? '',
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
if (!res || (res as { success?: boolean }).success === false) {
|
||||
showResult('保存失败', (res as { error?: string })?.error ?? '未知错误', true)
|
||||
@@ -273,6 +295,13 @@ export function SettingsPage() {
|
||||
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||
管理员
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="api-docs"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
API 文档
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="system" className="mt-0">
|
||||
@@ -595,7 +624,7 @@ export function SettingsPage() {
|
||||
搜索功能
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制首页搜索栏的显示</p>
|
||||
<p className="text-xs text-gray-400 ml-6">控制首页、目录页搜索栏的显示</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="search-enabled"
|
||||
@@ -604,23 +633,6 @@ export function SettingsPage() {
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('searchEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 rounded-lg bg-[#0a1628] border border-gray-700/50">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="w-4 h-4 text-[#38bdac]" />
|
||||
<Label htmlFor="about-enabled" className="text-white font-medium cursor-pointer">
|
||||
关于页面
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 ml-6">控制关于页面的访问</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="about-enabled"
|
||||
checked={featureConfig.aboutEnabled}
|
||||
disabled={featureSwitchSaving}
|
||||
onCheckedChange={(checked) => handleFeatureSwitch('aboutEnabled', checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/30">
|
||||
<p className="text-xs text-blue-300">
|
||||
@@ -629,6 +641,78 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Cloud className="w-5 h-5 text-[#38bdac]" />
|
||||
OSS 配置(阿里云对象存储)
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
endpoint、bucket、accessKey 等,用于图片/文件上传
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">Endpoint</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="oss-cn-hangzhou.aliyuncs.com"
|
||||
value={ossConfig.endpoint ?? ''}
|
||||
onChange={(e) =>
|
||||
setOssConfig((prev) => ({ ...prev, endpoint: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">Bucket</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="bucket 名称"
|
||||
value={ossConfig.bucket ?? ''}
|
||||
onChange={(e) =>
|
||||
setOssConfig((prev) => ({ ...prev, bucket: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">Region</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="oss-cn-hangzhou"
|
||||
value={ossConfig.region ?? ''}
|
||||
onChange={(e) =>
|
||||
setOssConfig((prev) => ({ ...prev, region: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">AccessKey ID</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="AccessKey ID"
|
||||
value={ossConfig.accessKeyId ?? ''}
|
||||
onChange={(e) =>
|
||||
setOssConfig((prev) => ({ ...prev, accessKeyId: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">AccessKey Secret</Label>
|
||||
<Input
|
||||
type="password"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="AccessKey Secret"
|
||||
value={ossConfig.accessKeySecret ?? ''}
|
||||
onChange={(e) =>
|
||||
setOssConfig((prev) => ({ ...prev, accessKeySecret: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -639,6 +723,10 @@ export function SettingsPage() {
|
||||
<TabsContent value="admin" className="mt-0">
|
||||
<AdminUsersPage />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="api-docs" className="mt-0">
|
||||
<ApiDocsPage />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/ckb.ts","./src/api/client.ts","./src/components/rechargealert.tsx","./src/components/richeditor.tsx","./src/components/modules/user/setvipmodal.tsx","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/hooks/usedebounce.ts","./src/layouts/adminlayout.tsx","./src/lib/utils.ts","./src/pages/admin-users/adminuserspage.tsx","./src/pages/api-doc/apidocpage.tsx","./src/pages/author-settings/authorsettingspage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/chaptertree.tsx","./src/pages/content/contentpage.tsx","./src/pages/content/personaddeditmodal.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/find-partner/findpartnerpage.tsx","./src/pages/find-partner/tabs/ckbconfigpanel.tsx","./src/pages/find-partner/tabs/ckbstatstab.tsx","./src/pages/find-partner/tabs/findpartnertab.tsx","./src/pages/find-partner/tabs/matchpooltab.tsx","./src/pages/find-partner/tabs/matchrecordstab.tsx","./src/pages/find-partner/tabs/mentorbookingtab.tsx","./src/pages/find-partner/tabs/mentortab.tsx","./src/pages/find-partner/tabs/resourcedockingtab.tsx","./src/pages/find-partner/tabs/teamrecruittab.tsx","./src/pages/linked-mp/linkedmppage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.tsx","./src/pages/mentor-consultations/mentorconsultationspage.tsx","./src/pages/mentors/mentorspage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/vip-roles/viprolespage.tsx","./src/pages/withdrawals/withdrawalspage.tsx","./src/utils/toast.ts"],"version":"5.6.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/ckb.ts","./src/api/client.ts","./src/components/rechargealert.tsx","./src/components/richeditor.tsx","./src/components/modules/user/setvipmodal.tsx","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/hooks/usedebounce.ts","./src/layouts/adminlayout.tsx","./src/lib/utils.ts","./src/pages/admin-users/adminuserspage.tsx","./src/pages/api-doc/apidocpage.tsx","./src/pages/api-docs/apidocspage.tsx","./src/pages/author-settings/authorsettingspage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/chaptertree.tsx","./src/pages/content/contentpage.tsx","./src/pages/content/personaddeditmodal.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/find-partner/findpartnerpage.tsx","./src/pages/find-partner/tabs/ckbconfigpanel.tsx","./src/pages/find-partner/tabs/ckbstatstab.tsx","./src/pages/find-partner/tabs/findpartnertab.tsx","./src/pages/find-partner/tabs/matchpooltab.tsx","./src/pages/find-partner/tabs/matchrecordstab.tsx","./src/pages/find-partner/tabs/mentorbookingtab.tsx","./src/pages/find-partner/tabs/mentortab.tsx","./src/pages/find-partner/tabs/resourcedockingtab.tsx","./src/pages/find-partner/tabs/teamrecruittab.tsx","./src/pages/linked-mp/linkedmppage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.tsx","./src/pages/mentor-consultations/mentorconsultationspage.tsx","./src/pages/mentors/mentorspage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/vip-roles/viprolespage.tsx","./src/pages/withdrawals/withdrawalspage.tsx","./src/utils/toast.ts"],"version":"5.6.3"}
|
||||
@@ -341,6 +341,53 @@ func AdminUserBalanceGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": balance, "transactions": transactions}})
|
||||
}
|
||||
|
||||
// AdminUserBalanceAdjust POST /api/admin/users/:id/balance/adjust 管理端-人工调整用户余额
|
||||
func AdminUserBalanceAdjust(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少用户ID"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Amount float64 `json:"amount" binding:"required"` // 正数增加,负数扣减
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"})
|
||||
return
|
||||
}
|
||||
if req.Amount == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "调整金额不能为 0"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var ub model.UserBalance
|
||||
if err := tx.Where("user_id = ?", userID).First(&ub).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
ub = model.UserBalance{UserID: userID, Balance: 0}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
newBalance := ub.Balance + req.Amount
|
||||
if newBalance < 0 {
|
||||
return fmt.Errorf("调整后余额不能为负,当前余额 %.2f", ub.Balance)
|
||||
}
|
||||
tx.Exec("INSERT INTO user_balances (user_id, balance, updated_at) VALUES (?, 0, NOW()) ON DUPLICATE KEY UPDATE balance = ?, updated_at = NOW()", userID, newBalance)
|
||||
txID := fmt.Sprintf("bt_adj_%d", time.Now().UnixNano()%100000000000)
|
||||
return tx.Create(&model.BalanceTransaction{
|
||||
ID: txID, UserID: userID, Type: "admin_adjust", Amount: req.Amount,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "余额已调整"})
|
||||
}
|
||||
|
||||
// ConfirmBalanceRechargeByOrder 支付成功后确认充值(幂等),供 PayNotify 和 activateOrderBenefits 调用
|
||||
func ConfirmBalanceRechargeByOrder(db *gorm.DB, order *model.Order) error {
|
||||
if order == nil || order.ProductType != "balance_recharge" {
|
||||
|
||||
@@ -391,17 +391,58 @@ func checkUserChapterAccess(db *gorm.DB, userID, chapterID string, isPremium boo
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
// previewContent 取内容的前 50%(不少于 100 个字符),并追加省略提示
|
||||
func previewContent(content string) string {
|
||||
// getUnpaidPreviewPercent 从 system_config 读取 unpaid_preview_percent,默认 20
|
||||
func getUnpaidPreviewPercent(db *gorm.DB) int {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "unpaid_preview_percent").First(&row).Error; err != nil || len(row.ConfigValue) == 0 {
|
||||
return 20
|
||||
}
|
||||
var val interface{}
|
||||
if err := json.Unmarshal(row.ConfigValue, &val); err != nil {
|
||||
return 20
|
||||
}
|
||||
switch v := val.(type) {
|
||||
case float64:
|
||||
p := int(v)
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if p > 100 {
|
||||
p = 100
|
||||
}
|
||||
return p
|
||||
case int:
|
||||
if v < 1 {
|
||||
return 1
|
||||
}
|
||||
if v > 100 {
|
||||
return 100
|
||||
}
|
||||
return v
|
||||
}
|
||||
return 20
|
||||
}
|
||||
|
||||
// previewContent 取内容的前 percent%(不少于 100 字,上限 500 字),并追加省略提示
|
||||
func previewContent(content string, percent int) string {
|
||||
total := utf8.RuneCountInString(content)
|
||||
if total == 0 {
|
||||
return ""
|
||||
}
|
||||
// 截取前 50% 的内容,保证有足够的预览长度
|
||||
limit := total / 2
|
||||
if percent < 1 {
|
||||
percent = 1
|
||||
}
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
limit := total * percent / 100
|
||||
if limit < 100 {
|
||||
limit = 100
|
||||
}
|
||||
const maxPreview = 500
|
||||
if limit > maxPreview {
|
||||
limit = maxPreview
|
||||
}
|
||||
if limit > total {
|
||||
limit = total
|
||||
}
|
||||
@@ -442,7 +483,8 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
|
||||
} else if checkUserChapterAccess(db, userID, ch.ID, isPremium) {
|
||||
returnContent = ch.Content
|
||||
} else {
|
||||
returnContent = previewContent(ch.Content)
|
||||
percent := getUnpaidPreviewPercent(db)
|
||||
returnContent = previewContent(ch.Content, percent)
|
||||
}
|
||||
|
||||
// data 中的 content 必须与外层 content 一致,避免泄露完整内容给未授权用户
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// 从 system_config 读取 chapter_config、feature_config、mp_config,合并后返回(免费以章节 is_free/price 为准)
|
||||
func GetPublicDBConfig(c *gin.Context) {
|
||||
defaultPrices := gin.H{"section": float64(1), "fullbook": 9.9}
|
||||
defaultFeatures := gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true}
|
||||
defaultFeatures := gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true}
|
||||
apiDomain := "https://soulapi.quwanzhi.com"
|
||||
if cfg := config.Get(); cfg != nil && cfg.BaseURL != "" {
|
||||
apiDomain = cfg.BaseURL
|
||||
@@ -174,11 +174,12 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true},
|
||||
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
|
||||
"mpConfig": defaultMp,
|
||||
"ossConfig": gin.H{},
|
||||
}
|
||||
keys := []string{"feature_config", "site_settings", "mp_config"}
|
||||
keys := []string{"feature_config", "site_settings", "mp_config", "oss_config"}
|
||||
for _, k := range keys {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
|
||||
@@ -208,6 +209,10 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
}
|
||||
out["mpConfig"] = merged
|
||||
}
|
||||
case "oss_config":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["ossConfig"] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
@@ -219,6 +224,7 @@ func AdminSettingsPost(c *gin.Context) {
|
||||
FeatureConfig map[string]interface{} `json:"featureConfig"`
|
||||
SiteSettings map[string]interface{} `json:"siteSettings"`
|
||||
MpConfig map[string]interface{} `json:"mpConfig"`
|
||||
OssConfig map[string]interface{} `json:"ossConfig"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
@@ -260,6 +266,12 @@ func AdminSettingsPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.OssConfig != nil {
|
||||
if err := saveKey("oss_config", "阿里云 OSS 配置", body.OssConfig); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存 OSS 配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "设置已保存"})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -417,3 +418,76 @@ func GiftPayMyPayments(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "list": out})
|
||||
}
|
||||
|
||||
// AdminGiftPayRequestsList GET /api/admin/gift-pay-requests 管理端-代付请求列表
|
||||
func AdminGiftPayRequestsList(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
status := strings.TrimSpace(c.Query("status"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
db := database.DB()
|
||||
q := db.Model(&model.GiftPayRequest{})
|
||||
if status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
var list []model.GiftPayRequest
|
||||
q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list)
|
||||
userIDs := make(map[string]bool)
|
||||
for _, r := range list {
|
||||
userIDs[r.InitiatorUserID] = true
|
||||
if r.PayerUserID != nil && *r.PayerUserID != "" {
|
||||
userIDs[*r.PayerUserID] = true
|
||||
}
|
||||
}
|
||||
nicknames := make(map[string]string)
|
||||
if len(userIDs) > 0 {
|
||||
ids := make([]string, 0, len(userIDs))
|
||||
for id := range userIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var users []model.User
|
||||
db.Select("id, nickname").Where("id IN ?", ids).Find(&users)
|
||||
for _, u := range users {
|
||||
if u.Nickname != nil {
|
||||
nicknames[u.ID] = *u.Nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for _, r := range list {
|
||||
initiatorNick := nicknames[r.InitiatorUserID]
|
||||
payerNick := ""
|
||||
if r.PayerUserID != nil {
|
||||
payerNick = nicknames[*r.PayerUserID]
|
||||
}
|
||||
orderID := ""
|
||||
if r.OrderID != nil {
|
||||
orderID = *r.OrderID
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": r.ID,
|
||||
"requestSn": r.RequestSN,
|
||||
"initiatorUserId": r.InitiatorUserID,
|
||||
"initiatorNick": initiatorNick,
|
||||
"productType": r.ProductType,
|
||||
"productId": r.ProductID,
|
||||
"amount": r.Amount,
|
||||
"description": r.Description,
|
||||
"status": r.Status,
|
||||
"payerUserId": r.PayerUserID,
|
||||
"payerNick": payerNick,
|
||||
"orderId": orderID,
|
||||
"expireAt": r.ExpireAt,
|
||||
"createdAt": r.CreatedAt,
|
||||
"updatedAt": r.UpdatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out, "total": total})
|
||||
}
|
||||
|
||||
@@ -83,13 +83,18 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
admin.GET("/ckb/devices", handler.AdminCKBDevices)
|
||||
admin.GET("/author-settings", handler.AdminAuthorSettingsGet)
|
||||
admin.POST("/author-settings", handler.AdminAuthorSettingsPost)
|
||||
admin.GET("/shensheshou/query", handler.AdminShensheShouQuery)
|
||||
admin.POST("/shensheshou/enrich", handler.AdminShensheShouEnrich)
|
||||
admin.POST("/shensheshou/ingest", handler.AdminShensheShouIngest)
|
||||
admin.PUT("/orders/refund", handler.AdminOrderRefund)
|
||||
admin.GET("/users/:id/balance", handler.AdminUserBalanceGet)
|
||||
admin.POST("/users/:id/balance/adjust", handler.AdminUserBalanceAdjust)
|
||||
admin.GET("/users", handler.AdminUsersList)
|
||||
admin.POST("/users", handler.AdminUsersAction)
|
||||
admin.PUT("/users", handler.AdminUsersAction)
|
||||
admin.DELETE("/users", handler.AdminUsersAction)
|
||||
admin.GET("/orders", handler.OrdersList)
|
||||
admin.GET("/gift-pay-requests", handler.AdminGiftPayRequestsList)
|
||||
}
|
||||
|
||||
// ----- 鉴权 -----
|
||||
@@ -159,6 +164,8 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
db.PUT("/users", handler.DBUsersAction)
|
||||
db.DELETE("/users", handler.DBUsersDelete)
|
||||
db.GET("/users/referrals", handler.DBUsersReferrals)
|
||||
db.GET("/users/rfm", handler.DBUsersRFM)
|
||||
db.GET("/users/journey-stats", handler.DBUsersJourneyStats)
|
||||
db.GET("/vip-roles", handler.DBVipRolesList)
|
||||
db.POST("/vip-roles", handler.DBVipRolesAction)
|
||||
db.PUT("/vip-roles", handler.DBVipRolesAction)
|
||||
|
||||
@@ -387,3 +387,26 @@ Mycontent-temp/miniprogram 为样式预览分支,miniprogram 为线上主线
|
||||
## 文档同步
|
||||
|
||||
- `开发文档/迁移完成度与待办清单.md`:搁置项更新为「稳定版已有」,无新增搁置
|
||||
|
||||
---
|
||||
|
||||
# 第十六部分:新版管理端迁移到稳定版 实施方案确认(2026-03-17 会议)
|
||||
|
||||
## 会议决议
|
||||
|
||||
- **新版有的就迁移**:API 文档 Tab、api-docs 独立页、OSS 配置、编辑时手机号禁用、鉴权逻辑优化 → 全部吸纳
|
||||
- **内容管理**:以稳定版为主,不采纳新版
|
||||
- **后端**:迁移前补齐 router(users/rfm、journey-stats、shensheshou 共 5 个)
|
||||
|
||||
## 开发任务指派(乘风)
|
||||
|
||||
| 责任角色 | 任务 |
|
||||
|---------|------|
|
||||
| 后端开发 | soul-api router 注册 users/rfm、users/journey-stats、shensheshou 共 5 个;确认 /api/admin/settings 是否支持 ossConfig |
|
||||
| 管理端开发工程师 | 从 new-soul/soul-admin 迁移 ApiDocsPage、OSS 配置、api-docs 路由、编辑时手机号禁用、鉴权逻辑;以稳定版为基准合并,内容管理不覆盖 |
|
||||
| 测试人员 | 迁移完成后按《需求评估》§七验收,三端联调 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `开发文档/新版管理端迁移到稳定版-需求评估.md`
|
||||
- `.cursor/meeting/2026-03-17_新版管理端迁移到稳定版实施方案确认.md`
|
||||
|
||||
@@ -54,10 +54,10 @@
|
||||
| **/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`、SetVipModal 相关更新 |
|
||||
| **/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` |
|
||||
| **/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` 等 |
|
||||
@@ -125,3 +125,4 @@
|
||||
| 日期 | 变更内容 |
|
||||
|------|----------|
|
||||
| 2026-03-11 | 初版:小程序与管理端界面清单、业务逻辑对齐(VIP 资料以用户资料为准、三端路由、免费章与 VIP、分销提现);与需求汇总、README、运营与变更同步。 |
|
||||
| 2026-03-17 | 管理端清单补充:用户规则、用户余额、订单支付方式;详见《管理端迁移分析-基于小程序功能.md》。 |
|
||||
|
||||
153
开发文档/新版管理端迁移到稳定版-需求评估.md
Normal file
153
开发文档/新版管理端迁移到稳定版-需求评估.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 新版管理端迁移到稳定版 - 需求评估
|
||||
|
||||
> 乘风整理。迁移方向:new-soul/soul-admin → soul-admin(稳定版)。以稳定版真实需求为基准做评估与调整。
|
||||
> 更新日期:2026-03-17
|
||||
|
||||
---
|
||||
|
||||
## 一、评估基准
|
||||
|
||||
| 基准 | 说明 |
|
||||
|------|------|
|
||||
| **稳定版小程序** | 已梳理完毕,定义 C 端能力:余额、代付、VIP、规则引擎、埋点、找伙伴、推广等 |
|
||||
| **稳定版管理端** | 目标端,需支撑上述小程序的运营与管理 |
|
||||
| **以界面定需求** | 管理端界面清单见《以界面定需求》§三;业务逻辑对齐见§四 |
|
||||
|
||||
---
|
||||
|
||||
## 二、稳定版管理端必须保留的能力(不可丢)
|
||||
|
||||
以下能力对应小程序功能,迁移时**必须保留**,新版若缺失则需从稳定版补齐。
|
||||
|
||||
| 能力 | 对应小程序 | 稳定版现状 | 迁移时动作 |
|
||||
|------|------------|------------|------------|
|
||||
| 用户详情 - 当前余额 | wallet、read/vip 余额支付 | ✅ 有 | **保留**,新版无则从稳定版代码带入 |
|
||||
| 订单 - 支付方式(微信/余额/支付宝) | 余额购买、代付 | ✅ 有 | **保留**,新版无则从稳定版代码带入 |
|
||||
| 订单 - 代付标识、代付人 | gift-pay | ✅ 有 | **保留** |
|
||||
| 用户规则 CRUD | ruleEngine | ✅ 有 | **保留** |
|
||||
| 超级个体/VIP 管理 | vip、member-detail | ✅ 有 | **保留** |
|
||||
| 提现审核、分销、找伙伴、导师 | 对应小程序页 | ✅ 有 | **保留** |
|
||||
| **内容管理(文章内容)** | chapters、read | ✅ 有 | **以稳定版为主**,不采纳新版 |
|
||||
| RechargeAlert | 商户号余额不足提示 | ✅ 有 | **保留**,运营需及时感知 |
|
||||
| LinkedMp(关联小程序) | 多小程序场景 | ✅ 有 | **保留**,若业务使用 |
|
||||
|
||||
---
|
||||
|
||||
## 三、稳定版可选能力(依赖后端,当前 404)
|
||||
|
||||
以下能力稳定版 UI 已有,但 soul-api **router 未注册**,会 404。迁移时需决策:**修后端** 或 **暂隐 UI**。
|
||||
|
||||
| 能力 | 接口 | 调用位置 | 建议 |
|
||||
|------|------|----------|------|
|
||||
| 用户列表 RFM 排序 | `GET /api/db/users/rfm` | UsersPage | 若运营需要 RFM 分析 → 补 router;否则迁移时可隐 |
|
||||
| 用户旅程总览 | `GET /api/db/users/journey-stats` | UsersPage | 若运营需要 → 补 router;否则迁移时可隐 |
|
||||
| 神射手(用户资料完善) | `GET/POST /api/admin/shensheshou/*` | UserDetailModal | 若使用神射手 → 补 router;否则迁移时可隐 |
|
||||
|
||||
---
|
||||
|
||||
## 四、新版独有、稳定版可吸纳的能力
|
||||
|
||||
以下为新版有、稳定版无,迁移时**可选择性吸纳**到稳定版。
|
||||
|
||||
| 能力 | 说明 | 需求评估 | 建议 |
|
||||
|------|------|----------|------|
|
||||
| **OSS 配置** | 阿里云对象存储 endpoint、bucket、accessKey 等 | 新版有 → 迁移 | **吸纳**(2026-03-17 会议决议) |
|
||||
| **API 文档 Tab** | 系统设置内嵌 ApiDocsPage | 新版有 → 迁移 | **吸纳** |
|
||||
| **api-docs 独立页** | `/api-docs` 路由 | 新版有 → 迁移 | **吸纳** |
|
||||
| **编辑时手机号禁用** | 编辑用户时手机号不可改 | 新版有 → 迁移 | **吸纳** |
|
||||
| **鉴权逻辑优化** | 先检查 token 再请求 | 新版有 → 迁移 | **吸纳** |
|
||||
|
||||
---
|
||||
|
||||
## 五、迁移策略(按模块)
|
||||
|
||||
### 5.1 内容管理(文章内容)— 以稳定版为主
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| **ContentPage** | 完全以稳定版为准,不采纳新版 |
|
||||
| **章节树、内容编辑、LinkTag、Person、LinkedMp** | 稳定版实现为准 |
|
||||
| **富文本、排序、预览、免费章节配置** | 稳定版为准 |
|
||||
|
||||
迁移时:内容管理相关代码**不覆盖**,保持稳定版实现。
|
||||
|
||||
---
|
||||
|
||||
### 5.2 其他模块迁移策略
|
||||
|
||||
| 模块 | 策略 | 说明 |
|
||||
|------|------|------|
|
||||
| **用户管理** | 以稳定版为主 | 用户详情余额、RFM、旅程、规则、超级个体均保留稳定版;可吸纳新版「编辑时手机号禁用」 |
|
||||
| **订单** | 以稳定版为主 | 支付方式、代付、导出格式不覆盖 |
|
||||
| **推广中心** | 以稳定版为主 | 保持稳定版实现 |
|
||||
| **提现** | 以稳定版为主 | 保持稳定版实现 |
|
||||
| **找伙伴** | 以稳定版为主 | CKB、匹配池、导师、资源对接等保持稳定版 |
|
||||
| **系统设置** | 稳定版为主 + 吸纳 | 保持稳定版结构;可吸纳新版 API 文档 Tab、OSS 配置 |
|
||||
| **Layout** | 以稳定版为主 | RechargeAlert、菜单结构保留;可吸纳新版鉴权逻辑 |
|
||||
| **Dashboard** | 以稳定版为主 | 保持稳定版 |
|
||||
| **支付/站点/小程序码/导师/API 文档** | 以稳定版为主 | 保持稳定版;API 文档可吸纳新版 ApiDocsPage |
|
||||
|
||||
---
|
||||
|
||||
### 5.3 需调整的项
|
||||
|
||||
| 项 | 调整说明 |
|
||||
|----|----------|
|
||||
| **后端 router** | 若保留 RFM、journey-stats、神射手,需 soul-api 补 5 个路由 |
|
||||
| **OSS** | 若吸纳,需 soul-api 支持 ossConfig(/api/admin/settings 需确认) |
|
||||
| **ApiDocsPage** | 新版有独立实现,可复制到稳定版并接入设置 Tab 或独立路由 |
|
||||
|
||||
---
|
||||
|
||||
### 5.4 不采纳的项
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 新版 ContentPage | 内容管理以稳定版为准 |
|
||||
| 新版 Layout 结构差异 | 系统设置分区等保持稳定版 |
|
||||
| 新版部分 UI 细节 | grid-cols、间距等以稳定版为准 |
|
||||
|
||||
---
|
||||
|
||||
## 六、需求优先级与实施顺序
|
||||
|
||||
| 阶段 | 内容 | 优先级 |
|
||||
|------|------|:------:|
|
||||
| **1. 必须保留** | 确保迁移后稳定版不丢失:用户详情余额、订单支付方式/代付、RechargeAlert、LinkedMp | P0 |
|
||||
| **2. 后端补齐** | 若保留 RFM/journey/神射手:soul-api router 注册 5 个路由 | P1 |
|
||||
| **3. 吸纳优化** | 编辑时手机号禁用、鉴权逻辑、API 文档 Tab | P2 |
|
||||
| **4. 按需吸纳** | OSS 配置、api-docs 独立页 | P3 |
|
||||
|
||||
---
|
||||
|
||||
## 七、验收标准(迁移完成后)
|
||||
|
||||
| 验收项 | 标准 |
|
||||
|--------|------|
|
||||
| 用户详情 | 基础信息 Tab 有「当前余额」卡,数据来自 `/api/admin/users/:id/balance` |
|
||||
| 订单页 | 支付方式列含微信/余额/支付宝,代付订单展示代付人 |
|
||||
| 用户规则 | 规则配置 Tab 可 CRUD,与小程序 ruleEngine 联动 |
|
||||
| RechargeAlert | 余额不足等错误时顶部红条可展示 |
|
||||
| 内容管理 | 章节树、文章编辑、LinkTag、Person、LinkedMp 等以稳定版为准 |
|
||||
| 其他 | 提现、分销、找伙伴、设置等页面功能正常 |
|
||||
|
||||
---
|
||||
|
||||
## 八、迁移策略汇总
|
||||
|
||||
| 原则 | 模块 |
|
||||
|------|------|
|
||||
| **以稳定版为主(不覆盖)** | 内容管理、用户管理、订单、推广、提现、找伙伴、Layout、Dashboard |
|
||||
| **吸纳新版** | 编辑时手机号禁用、鉴权逻辑、API 文档 Tab、OSS 配置(按需) |
|
||||
| **内容管理** | **明确以稳定版为主**,章节树、文章编辑、LinkTag、Person、LinkedMp 等均不采纳新版 |
|
||||
|
||||
---
|
||||
|
||||
## 九、总结
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| **必须保留** | 余额、代付、规则、VIP、RechargeAlert、LinkedMp、**内容管理** |
|
||||
| **可选(依赖后端)** | RFM、journey-stats、神射手 |
|
||||
| **可吸纳** | OSS、API 文档、编辑禁用、鉴权优化 |
|
||||
| **结论** | 内容管理以稳定版为主;其他模块以稳定版为主,选择性吸纳新版优化;后端按需补 router。 |
|
||||
175
开发文档/稳定版-源码质量分析报告.md
Normal file
175
开发文档/稳定版-源码质量分析报告.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Soul 创业派对稳定版 - 源码质量分析报告
|
||||
|
||||
> 分析时间:2026-03-17
|
||||
> 分析人:乘风(老板分身)
|
||||
|
||||
---
|
||||
|
||||
## 一、高优先级(建议优先处理)
|
||||
|
||||
### 1. soul-api:敏感配置默认值泄露
|
||||
|
||||
**位置**:`soul-api/internal/config/config.go` 第 174–237 行
|
||||
|
||||
**问题**:AppSecret、MchKey、APIv3Key、adminPassword、adminSessionSecret、证书序列号等敏感信息有硬编码默认值。若 `.env` 未配置,将使用这些默认值,存在泄露与撞库风险。
|
||||
|
||||
**建议**:
|
||||
- 生产环境强制从环境变量读取,缺则 `log.Fatal` 启动失败
|
||||
- 或使用占位符如 `"MUST_SET_IN_ENV"`,启动时校验并拒绝
|
||||
|
||||
---
|
||||
|
||||
### 2. soul-api:`/api/user/track` 无鉴权
|
||||
|
||||
**位置**:`soul-api/internal/router/router.go` 第 260–261 行;`soul-api/internal/handler/user.go` 第 535 行
|
||||
|
||||
**问题**:`GET /api/user/track?userId=xxx` 无 AdminAuth,任何人可查任意用户行为轨迹。管理端 `UserDetailModal` 调用此接口展示用户行为。
|
||||
|
||||
**建议**:
|
||||
- 将接口迁至 `/api/admin/user/track` 或 `/api/db/user/track`,并加 `AdminAuth()` 中间件
|
||||
- 或保留路径,在 handler 内校验管理员 token
|
||||
|
||||
---
|
||||
|
||||
### 3. miniprogram:`payment.js` 使用未定义变量
|
||||
|
||||
**位置**:`miniprogram/utils/payment.js` 第 25 行
|
||||
|
||||
**问题**:使用 `app.globalData.apiBase`,而 app 中仅有 `baseUrl`,会导致 `undefined + '/payment/create'`,请求失败。
|
||||
|
||||
**说明**:当前支付流程在 `read.js` 中直接调用 `/api/miniprogram/pay`,未使用 `payment.js`,该文件疑似废弃。若确认废弃,建议删除;若保留,需改为 `baseUrl` 并统一走 miniprogram 支付接口。
|
||||
|
||||
---
|
||||
|
||||
## 二、中优先级
|
||||
|
||||
### 4. miniprogram:`goToMatch()` 重复定义
|
||||
|
||||
**位置**:`miniprogram/pages/my/my.js` 第 801、816 行
|
||||
|
||||
**问题**:两个完全相同的 `goToMatch()`,后者覆盖前者,易造成维护困惑。
|
||||
|
||||
**建议**:删除其中一个。
|
||||
|
||||
---
|
||||
|
||||
### 5. miniprogram:敏感信息硬编码
|
||||
|
||||
**位置**:`miniprogram/app.js` 第 18–25 行
|
||||
|
||||
**问题**:appId、mchId、withdrawSubscribeTmplId 写死在代码中,与后端 config 重复。
|
||||
|
||||
**建议**:优先从 `/api/miniprogram/config` 的 `mpConfig` 读取,代码中仅保留开发兜底。
|
||||
|
||||
---
|
||||
|
||||
### 6. miniprogram:开发接口暴露
|
||||
|
||||
**位置**:`miniprogram/pages/settings/settings.js` 第 416 行
|
||||
|
||||
**问题**:`dev/login-as` 在开发/体验版可见,生产需确保后端关闭或鉴权。
|
||||
|
||||
**建议**:后端对该接口做环境或 IP 白名单限制。
|
||||
|
||||
---
|
||||
|
||||
### 7. soul-admin:RichEditor innerHTML 潜在 XSS
|
||||
|
||||
**位置**:`soul-admin/src/components/RichEditor.tsx` 第 177 行
|
||||
|
||||
**问题**:`popup.innerHTML = items.map(...)` 直接拼接 `item.name`、`item.label`,若含 HTML 可能 XSS。
|
||||
|
||||
**建议**:对 `name`、`label` 做 HTML 转义,或使用 `textContent` 渲染。
|
||||
|
||||
---
|
||||
|
||||
### 8. miniprogram:`totalSections: 62` 多处硬编码
|
||||
|
||||
**位置**:`app.js`、`index.js`、`my.js`、`chapters.js` 等
|
||||
|
||||
**问题**:章节总数写死为 62,与真实数据可能不一致。
|
||||
|
||||
**建议**:从 `book/stats` 或 `all-chapters` 动态获取,或统一从 config 读取。
|
||||
|
||||
---
|
||||
|
||||
### 9. miniprogram:废弃/备份文件
|
||||
|
||||
**位置**:
|
||||
- `miniprogram/pages/read/read.js.backup`
|
||||
- `miniprogram/pages/referral/referral.wxss.backup`
|
||||
|
||||
**问题**:备份文件混入源码,增加噪音。
|
||||
|
||||
**建议**:若无需保留,可删除或移出版本库。
|
||||
|
||||
---
|
||||
|
||||
### 10. soul-admin:`/api/user/track` 三端边界
|
||||
|
||||
**位置**:`soul-admin/src/components/modules/user/UserDetailModal.tsx` 第 192 行
|
||||
|
||||
**问题**:管理端调用 `/api/user/track`(非 admin/db 路径),与「管理端只调 admin/db」约定不符。
|
||||
|
||||
**建议**:后端将 track 查询迁至 `/api/admin/user/track` 或 `/api/db/user/track`,管理端同步改路径。
|
||||
|
||||
---
|
||||
|
||||
## 三、低优先级
|
||||
|
||||
### 11. soul-api:`DBUsersList` 函数过长
|
||||
|
||||
**位置**:`soul-api/internal/handler/db.go`
|
||||
|
||||
**建议**:拆分为统计、过滤、分页等子函数,提升可读性。
|
||||
|
||||
---
|
||||
|
||||
### 12. soul-api:`AdminWithdrawTest` 默认值
|
||||
|
||||
**位置**:`soul-api/internal/handler/withdraw.go` 第 137–138 行
|
||||
|
||||
**问题**:测试接口默认 userId、amount 写死,易误用。
|
||||
|
||||
**建议**:仅开发环境启用,或强制从请求体传入。
|
||||
|
||||
---
|
||||
|
||||
### 13. miniprogram:错误处理不统一
|
||||
|
||||
**问题**:部分 `app.request` 用 `try/catch` 并提示用户,部分 `catch (e) {}` 静默吞错。
|
||||
|
||||
**建议**:统一约定:用户可见操作需提示,静默请求可吞错但需 `console.warn`。
|
||||
|
||||
---
|
||||
|
||||
### 14. soul-admin:上传逻辑重复
|
||||
|
||||
**位置**:`ContentPage.tsx`、`MentorsPage.tsx` 等
|
||||
|
||||
**建议**:抽取公共 `uploadFile(apiUrl, file)` 方法复用。
|
||||
|
||||
---
|
||||
|
||||
### 15. 性能:config / book 请求缓存
|
||||
|
||||
**问题**:`/api/miniprogram/config`、`book/all-chapters` 等频繁请求,无本地缓存。
|
||||
|
||||
**建议**:对 config 做短期缓存(如 5 分钟),减少重复请求。
|
||||
|
||||
---
|
||||
|
||||
## 四、已确认无问题
|
||||
|
||||
- **三端路由边界**:小程序统一走 `/api/miniprogram/*`,管理端走 `/api/admin/*`、`/api/db/*`,主体符合约定
|
||||
- **支付流程**:`read.js` 正确使用 `/api/miniprogram/pay`,未依赖有问题的 `payment.js`
|
||||
- **read.js.backup**:未在业务中引用,可安全清理
|
||||
|
||||
---
|
||||
|
||||
## 五、建议处理顺序
|
||||
|
||||
1. **立即**:soul-api 敏感默认值、`/api/user/track` 鉴权
|
||||
2. **短期**:`payment.js` 废弃或修复、`goToMatch` 去重、`totalSections` 动态化
|
||||
3. **中期**:管理端 track 路径迁移、RichEditor XSS 修复、配置与缓存优化
|
||||
4. **长期**:代码结构优化、备份文件清理、错误处理统一
|
||||
123
开发文档/稳定版-管理端与小程序对接分析.md
Normal file
123
开发文档/稳定版-管理端与小程序对接分析.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 稳定版:管理端与小程序功能对接分析
|
||||
|
||||
> 分析时间:2026-03-17
|
||||
> 分析人:乘风(老板分身)
|
||||
> 更新:2026-03-17 已完成 referralEnabled、searchEnabled 对接,删除 aboutEnabled 及关于作者页
|
||||
|
||||
## 一、结论概览
|
||||
|
||||
| 类型 | 数量 | 说明 |
|
||||
|-----|------|------|
|
||||
| ✅ 已对接 | 3 | matchEnabled、referralEnabled、searchEnabled |
|
||||
| 🗑️ 已删除 | 1 | aboutEnabled、关于作者页 |
|
||||
| ⚠️ 需确认 | 1 | site_settings 与 chapter_config 价格同步 |
|
||||
|
||||
---
|
||||
|
||||
## 二、功能开关(feature_config)对接情况
|
||||
|
||||
### 管理端配置项(SettingsPage 系统设置)
|
||||
|
||||
| 开关 | 管理端 | 后端 config 返回 | 小程序使用 |
|
||||
|-----|--------|------------------|------------|
|
||||
| **matchEnabled** | ✅ 有 | ✅ features.matchEnabled | ✅ **已用**:custom-tab-bar 控制找伙伴 tab;my 页根据此隐藏/显示找伙伴入口 |
|
||||
| **referralEnabled** | ✅ 有 | ✅ features.referralEnabled | ✅ **已用**:my 页推荐好友/我的收益入口 `wx:if`;goToReferral 二次校验 |
|
||||
| **searchEnabled** | ✅ 有 | ✅ features.searchEnabled | ✅ **已用**:首页搜索栏、目录页搜索按钮 `wx:if` |
|
||||
| ~~aboutEnabled~~ | 🗑️ 已删除 | - | 关于作者页已移除 |
|
||||
|
||||
### 小程序实际使用(对接后)
|
||||
|
||||
- **custom-tab-bar**:读取 `matchEnabled`,控制找伙伴 tab
|
||||
- **my.js**:`loadFeatureConfig()` 取 matchEnabled、referralEnabled、searchEnabled,存 globalData.features
|
||||
- **my.wxml**:推荐好友/我的收益 `wx:if="{{referralEnabled}}"`;已删除关于作者菜单
|
||||
- **index**:`loadFeatureConfig()` 取 searchEnabled;搜索栏 `wx:if="{{searchEnabled}}"`
|
||||
- **chapters**:`loadFeatureConfig()` 取 searchEnabled;搜索按钮 `wx:if="{{searchEnabled}}"`
|
||||
|
||||
---
|
||||
|
||||
## 三、其他配置对接情况
|
||||
|
||||
### 已对接
|
||||
|
||||
| 配置项 | 来源 | 小程序使用位置 |
|
||||
|--------|------|----------------|
|
||||
| prices (section/fullbook) | chapter_config | read.js 展示价格、chapterAccessManager |
|
||||
| userDiscount | referral_config | referral 页展示好友优惠 |
|
||||
| linkTags | link_tag 表 | read.js onLinkTagTap |
|
||||
| linkedMiniprograms | system_config | read.js 跳转关联小程序 |
|
||||
| mpConfig | mp_config | 支付、提现等 |
|
||||
|
||||
### 需确认
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| site_settings (sectionPrice, baseBookPrice) | 管理端保存到 `site_settings`,小程序 config 的 prices 来自 `chapter_config`。需确认两处是否同步,或是否有单独维护 chapter_config 的流程。 |
|
||||
|
||||
---
|
||||
|
||||
## 四、建议改造(补齐未用开关)
|
||||
|
||||
### 1. 推广开关 referralEnabled
|
||||
|
||||
**影响入口**:我的页「推荐好友」「我的收益」统计、goToReferral 跳转
|
||||
|
||||
**改造建议**:
|
||||
- my.js:`loadFeatureConfig()` 增加 `referralEnabled`
|
||||
- my.wxml:`profile-stat`(推荐好友、我的收益)加 `wx:if="{{referralEnabled}}"`
|
||||
- 若有单独推广入口(如菜单项),也加 `wx:if="{{referralEnabled}}"`
|
||||
|
||||
### 2. 搜索开关 searchEnabled
|
||||
|
||||
**影响入口**:首页搜索栏、目录页搜索按钮
|
||||
|
||||
**改造建议**:
|
||||
- app.js 或各页:从 config 拉取 `searchEnabled` 存 globalData
|
||||
- index.wxml:搜索栏加 `wx:if="{{searchEnabled}}"`
|
||||
- chapters.wxml:搜索按钮加 `wx:if="{{searchEnabled}}"`
|
||||
|
||||
### 3. 关于作者开关 aboutEnabled
|
||||
|
||||
**影响入口**:我的页「关于作者」菜单项
|
||||
|
||||
**改造建议**:
|
||||
- my.js:`loadFeatureConfig()` 增加 `aboutEnabled`
|
||||
- my.wxml:关于作者 menu-item 加 `wx:if="{{aboutEnabled}}"`
|
||||
|
||||
---
|
||||
|
||||
## 五、数据流说明
|
||||
|
||||
```
|
||||
管理端 SettingsPage
|
||||
→ POST /api/admin/settings { featureConfig }
|
||||
→ 写入 system_config.feature_config
|
||||
|
||||
小程序
|
||||
→ GET /api/miniprogram/config
|
||||
→ GetPublicDBConfig 读取 feature_config,合并到 features
|
||||
→ 返回 { features: { matchEnabled, referralEnabled, searchEnabled, aboutEnabled } }
|
||||
|
||||
当前:仅 matchEnabled 被小程序使用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、附录:config 接口返回结构(GetPublicDBConfig)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"prices": { "section": 1, "fullbook": 9.9 },
|
||||
"features": {
|
||||
"matchEnabled": true,
|
||||
"referralEnabled": true,
|
||||
"searchEnabled": true,
|
||||
"aboutEnabled": true
|
||||
},
|
||||
"mpConfig": { "appId", "apiDomain", "buyerDiscount", ... },
|
||||
"userDiscount": 5,
|
||||
"linkTags": [...],
|
||||
"linkedMiniprograms": [...],
|
||||
"configs": { "chapter_config", "feature_config", "mp_config" }
|
||||
}
|
||||
```
|
||||
36
开发文档/管理端两版界面差异-新需求参考.md
Normal file
36
开发文档/管理端两版界面差异-新需求参考.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 管理端两版界面差异(新需求参考)
|
||||
|
||||
> 迁移方向:新版 → 稳定版。界面差异用于需求评估与迁移取舍。
|
||||
> 更新日期:2026-03-17
|
||||
|
||||
---
|
||||
|
||||
## 一、稳定版有、新版无(迁移时必须保留)
|
||||
|
||||
迁移时以稳定版为主,以下能力**不可丢失**。
|
||||
|
||||
| 页面/组件 | 界面差异 | 说明 |
|
||||
|-----------|----------|------|
|
||||
| **用户详情** | 基础信息 Tab 有「当前余额」卡片 | 对应小程序 wallet、余额支付 |
|
||||
| **订单页** | 支付方式(微信/余额/支付宝)、代付标识、代付人 | 对应小程序 gift-pay、余额购买 |
|
||||
| **用户列表** | RFM 排序模式、RFM 分值列 | 运营分析用(依赖后端 users/rfm) |
|
||||
| **用户列表** | 用户旅程总览 Tab | 依赖后端 users/journey-stats |
|
||||
| **内容管理** | 关联小程序管理(LinkedMpPage) | 多小程序场景 |
|
||||
| **全局** | RechargeAlert 充值告警条 | 商户号余额不足提示 |
|
||||
|
||||
---
|
||||
|
||||
## 二、新版有、稳定版无(迁移时可吸纳)
|
||||
|
||||
| 页面/组件 | 界面差异 | 建议 |
|
||||
|-----------|----------|------|
|
||||
| **系统设置** | API 文档 Tab、OSS 配置 | 按需吸纳 |
|
||||
| **路由** | `/api-docs` 独立页 | 按需吸纳 |
|
||||
| **用户编辑** | 编辑时手机号禁用 | 建议吸纳 |
|
||||
| **Layout** | 鉴权逻辑优化 | 建议吸纳 |
|
||||
|
||||
---
|
||||
|
||||
## 三、与需求评估的关系
|
||||
|
||||
详见《新版管理端迁移到稳定版-需求评估.md》。
|
||||
89
开发文档/管理端迁移分析-基于小程序功能.md
Normal file
89
开发文档/管理端迁移分析-基于小程序功能.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 管理端迁移分析:基于现有小程序功能
|
||||
|
||||
> 乘风整理。以 miniprogram 当前功能为基准,分析稳定版与新版管理端的差异及迁移建议。
|
||||
> 更新日期:2026-03-17
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与范围
|
||||
|
||||
| 项目 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| **稳定版管理端** | `soul-admin/` | 根目录,当前主用 |
|
||||
| **新版管理端** | `new-soul/soul-admin/` | 新版,与 new-soul 配套 |
|
||||
| **分析基准** | miniprogram 当前页面与接口(app.json、以界面定需求.md) |
|
||||
|
||||
**迁移方向**:新版 → 稳定版。分析目标:**以稳定版真实需求为基准,评估新版迁移时的保留项与吸纳项**。详见《新版管理端迁移到稳定版-需求评估.md》。
|
||||
|
||||
---
|
||||
|
||||
## 二、稳定版 vs 新版 差异对比
|
||||
|
||||
| 能力 | 稳定版 soul-admin | 新版 new-soul/soul-admin |
|
||||
|------|-------------------|---------------------------|
|
||||
| **用户详情-余额** | ✅ 有 balanceData、余额 Tab、`GET /api/admin/users/:id/balance` | ❌ 无余额展示 |
|
||||
| **订单-支付方式** | ✅ 展示 wechat/balance/alipay、代付人 payerNickname | ❌ 仅 wechat/alipay,无 balance、无代付 |
|
||||
| **用户规则** | ✅ 规则配置 Tab、user-rules CRUD | ✅ 已有 |
|
||||
| **超级个体/VIP** | ✅ 有 | ✅ 有 |
|
||||
| **用户详情-VIP 表单** | ✅ 含 isVip、vipExpireDate、vipRole 等完整字段 | ⚠️ 有 vipForm,但 payload 不含 isVip/vipExpireDate 等(handleSave 仅保存基础字段,handleSaveVip 单独保存 VIP) |
|
||||
| **LinkedMp 页** | 稳定版有 `/linked-mp` | 新版无(需确认是否需迁移) |
|
||||
| **ApiDocs 页** | 单页 api-doc | 新版多 api-docs 页 |
|
||||
|
||||
---
|
||||
|
||||
## 三、小程序功能 → 管理端能力对照
|
||||
|
||||
以下按 miniprogram 核心能力,逐项对照管理端是否已覆盖(以稳定版为基准)。
|
||||
|
||||
| 小程序能力 | 小程序页面/接口 | 管理端需支持 | 稳定版 | 新版 |
|
||||
|------------|-----------------|--------------|:------:|:----:|
|
||||
| **余额体系** | wallet、read/vip 余额支付 | 用户余额查看、交易记录 | ✅ | ❌ |
|
||||
| **代付(美团式)** | gift-pay/detail、list | 订单页展示代付信息 | ✅ | ❌ |
|
||||
| **规则引擎** | ruleEngine | 用户规则 CRUD | ✅ | ✅ |
|
||||
| **VIP** | vip、member-detail | VIP 状态、超级个体 | ✅ | ✅ |
|
||||
| **订单** | purchases、read/vip 支付 | 订单列表、支付方式、退款 | ✅ | ⚠️ 缺余额/代付 |
|
||||
| **提现、分销、找伙伴、导师、内容** | 对应页面 | 已有 | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 四、迁移时稳定版必须保留(新版缺失则从稳定版带入)
|
||||
|
||||
### 4.1 用户详情 - 余额展示
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| **目标** | 迁移后稳定版保留「当前余额」卡 |
|
||||
| **接口** | `GET /api/admin/users/:id/balance` |
|
||||
| **实现** | 稳定版 UserDetailModal 已有,合并时不可覆盖 |
|
||||
|
||||
### 4.2 订单页 - 支付方式与代付
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| **目标** | 迁移后稳定版保留 balance、代付人展示 |
|
||||
| **接口** | `GET /api/admin/orders` 已返回 |
|
||||
| **实现** | 稳定版 OrdersPage 已有,合并时不可覆盖 |
|
||||
|
||||
### 4.3 LinkedMp、RechargeAlert
|
||||
|
||||
稳定版 ContentPage 嵌入 LinkedMpPage、Layout 有 RechargeAlert,合并时保留。
|
||||
|
||||
---
|
||||
|
||||
## 五、稳定版待补充(两版共用)
|
||||
|
||||
| 优先级 | 项 | 说明 |
|
||||
|:------:|----|------|
|
||||
| P2 | 用户余额人工调整 | 用户详情增加「调整余额」入口,需 soul-api 新增 `POST /api/admin/users/:id/balance/adjust` |
|
||||
| P2 | 代付列表页 | 新增代付请求维度管理,需 soul-api 新增 `GET /api/admin/gift-pay-requests` |
|
||||
| P3 | 埋点看板 | 可选,低优先级 |
|
||||
|
||||
---
|
||||
|
||||
## 六、总结
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| **新版迁移** | 用户详情余额、订单页支付方式(余额/代付)——2 项,参考稳定版实现 |
|
||||
| **稳定版待补充** | 用户余额人工调整、代付列表页——2 项,两版共用后端接口 |
|
||||
| **结论** | 新版管理端需从稳定版迁移余额与代付相关展示能力,接口已就绪,以 UI 与数据绑定为主。
|
||||
Reference in New Issue
Block a user