chore: 新增 .gitignore 排除开发文档,同步代码与构建产物

Made-with: Cursor
This commit is contained in:
卡若
2026-03-16 09:21:39 +08:00
parent fa9903d235
commit 85ce2422d1
40 changed files with 2315 additions and 947 deletions

View File

@@ -0,0 +1,44 @@
# 2026-03-15 Soul 创业派对全站深度测试
## 问题背景
对 Soul 创业派对项目进行首次全站深度测试(只检测不修改),覆盖管理端 20+ 页面、35 个 API 端点、25 个小程序页面、25 张数据库表。
## 解决过程
### 测试方法
1. 环境检查:确认后端/前端/数据库运行状态
2. 管理端浏览器测试:逐页逐按钮截图检查
3. API 端点测试curl 逐个测试 35 个端点,含安全边界测试
4. 小程序代码审查25 个页面 + 8 个工具文件全量代码阅读
5. 数据库一致性检查:交叉验证各 API 数据
### 关键发现42 个问题)
- 严重 11 个OSS 密钥泄露、登录守卫缺失、小程序模块混用、废弃 API
- 高 13 个硬编码、API 失败伪装成功、分页缺失
- 中 12 个:调试日志残留、模拟数据未清理
- 低 6 个:版本号未设置等
## 可提炼规则
### 安全测试
1. **密钥脱敏是硬性规则**:任何返回配置的 API密钥类字段必须脱敏
2. **SPA 路由守卫必查**:直接访问后台路径测试
3. **上传接口安全测试**:非图片文件、超大文件、空文件
### API 测试
4. **响应字段名不能假设**:先 print keys() 再解析,不同端点可能用 data/results/orders/records
5. **分页必须翻页验证**:测第一页也测 page=2
6. **交叉验证**stats 的总数 vs list API 的实际条数
### 小程序测试
7. **废弃 API 年检制度**:每年核对微信基础库废弃列表
8. **模块语法统一检查**grep -rn "export default" 快速排查
9. **死代码扫描**utils/ 每个文件是否被 pages/ 引用
### 数据库测试
10. **空表不代表无问题**:空表可能是同步逻辑失效
## 适用角色
- target_roles: ["软件测试", "团队"]

View File

@@ -2,6 +2,7 @@
| 日期 | 摘要 | 文件 |
|------|------|------|
| 2026-03-15 | **全站深度测试42个问题**管理端20+页面截图测试、35个API端点、25个小程序页面代码审查、25张数据库表严重11/高13/中12/低6沉淀安全测试/API测试/小程序测试/数据库测试方法论 | [2026-03-15-全站深度测试42问题.md](./2026-03-15-全站深度测试42问题.md) |
| 2026-03-10 | 管理端迁移 Mycontent-temp菜单一致性、隐藏路由可达性、鉴权与跳转回归 | [2026-03-10.md](./2026-03-10.md) |
| 2026-03-05 | 分支合并后回归清单制定;三端联调验证 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-05 | 文章详情@某人@ 展示与添加好友用例、联调与回归点 | [2026-03-05.md](./2026-03-05.md) |
| 2026-03-10 | 管理端迁移 Mycontent-temp菜单一致性、隐藏路由可达性、鉴权与跳转回归 | [2026-03-10.md](./2026-03-10.md) |

View File

@@ -1,72 +1,225 @@
---
name: soul-tester
description: Soul 创业派对测试人员。功能测试、回归测试、三端小程序、管理端、API联调验证。Use when 测试, 测试用例, 回归测试, 功能测试, QA.
description: Soul 创业派对测试人员。全站深度测试、功能测试、回归测试、三端联调验证、安全审计。Use when 测试, 测试用例, 回归测试, 功能测试, QA, 全站测试, 深度测试.
version: "2.0"
updated: "2026-03-15"
---
# Soul 创业派对 - 测试人员 Skill
# Soul 创业派对 - 测试 Skill v2.0
当你在**功能测试、回归测试、三端联调验证**时,使用本 Skill。测试人员负责小程序、管理端、API 的功能与集成测试,不参与源码编写
> **定位**Soul 创业派对项目的专业测试规范。基于 2026-03-15 全站深度测试经验沉淀覆盖管理端、小程序、API、数据库四个维度
>
> **核心原则**:逐页逐按钮逐功能,三端隔离验证,数据交叉校验,安全必查。
---
## 1. 职责范围
## 1. 测试范围
| 职责 | 说明 | 产出 |
|------|------|------|
| 功能测试 | 按需求验证功能正确性 | 测试用例、通过/失败记录 |
| 回归测试 | 变更后验证原有功能未破坏 | 回归清单、测试报告 |
| 三端联调 | 小程序↔API、管理端↔API 数据流验证 | 联调记录 |
| Bug 反馈 | 复现步骤、环境、期望 vs 实际 | Bug 列表、复现说明 |
| 端 | 目录 | API 路径 | 数据表 |
|----|------|----------|--------|
| 管理端 | soul-admin/ | /api/admin/*, /api/db/* | 25 张表 |
| 小程序 | miniprogram/ | /api/miniprogram/* | 同上 |
| API 后端 | soul-api/ | 全部 | 同上 |
| 数据库 | MySQL | — | users, chapters, orders 等 25 张 |
---
## 2. 测试范围
## 2. 测试执行检查清单(每次必做)
| 端 | 目录 | API 路径 | 重点 |
|----|------|----------|------|
| 小程序 | miniprogram/ | /api/miniprogram/* | 登录、支付、推荐码、VIP、阅读、分享 |
| 管理端 | soul-admin/ | /api/admin/*、/api/db/* | 内容管理、用户、订单、提现、VIP 角色、推广设置 |
| API 后端 | soul-api/ | 全部 | 接口契约、鉴权、分润、支付回调 |
### 2.1 环境检查Step 0
- [ ] 后端 API 运行(`curl localhost:8080/health`
- [ ] 管理端前端运行(`curl localhost:5174`
- [ ] 数据库连接正常(通过 /api/book/stats 间接验证)
- [ ] OSS 配置状态(/api/admin/settings → ossConfig 有值)
### 2.2 管理端页面检查24 个页面/子Tab
**登录**
- [ ] 登录页 UI 完整(表单、按钮、品牌标识)
- [ ] 错误凭据有明确错误提示
- [ ] 正确凭据登录成功跳转
- [ ] **路由守卫**:未登录直接访问 /dashboard 应跳转 /login
**仪表盘**
- [ ] 4 个统计卡片数据正确(用户数、收入、订单数、转化率)
- [ ] 最近订单列表有数据
- [ ] 新注册用户列表**用户名不应显示为"-"**
- [ ] 分类标签点击统计有数据或合理空状态提示
**内容管理**5 个 Tab
- [ ] 章节管理:树结构完整,篇/章/节层级
- [ ] 内容排行榜:排名、浏览量、付费数、热度完整
- [ ] 内容搜索:搜索框可用
- [ ] 链接人与事AI 列表展示
- [ ] 链接标签:标签 CRUD
**用户管理**2 个 Tab
- [ ] 用户列表:昵称、手机号、付费状态、分销、分页
- [ ] 用户旅程8 阶段漏斗数据
**找伙伴**5 个 Tab
- [ ] 数据统计匹配次数、用户数、AI 获客
- [ ] 匹配记录:列表展示
- [ ] 匹配池设置:来源池、基础设置
- [ ] 导师管理:导师列表和价格
- [ ] 团队招募:招募记录
**推广中心**5 个 Tab
- [ ] 数据概览:今日/本月/累计统计
- [ ] 订单管理:订单列表、搜索、分页
- [ ] 绑定管理:绑定关系列表
- [ ] 提现审核:提现记录和审核功能
- [ ] 推广设置:收益率、提现规则
**系统设置**4 个 Tab
- [ ] 系统设置功能开关、价格、OSS、小程序配置
- [ ] 作者详情:基本信息、统计、亮点
- [ ] 管理员:管理员列表
- [ ] API 文档:接口文档完整
### 2.3 API 端点检查35+ 端点)
**公开 API无需 Token**
- [ ] GET /health → 200
- [ ] GET /api/config → 200配置完整
- [ ] GET /api/book/all-chapters → 200章节数与 DB 一致
- [ ] GET /api/book/hot → 200热度排行有数据
- [ ] GET /api/book/recommended → 200
- [ ] GET /api/book/search?q=创业 → 200有结果注意返回字段是 `results` 不是 `data`
- [ ] GET /api/book/search?q= → 200空结果
- [ ] GET /api/book/stats → 200交叉验证章节数
**小程序 API无需 Token**
- [ ] GET /api/miniprogram/config → 200
- [ ] GET /api/miniprogram/book/hot → 200
- [ ] GET /api/miniprogram/book/stats → 200
- [ ] GET /api/miniprogram/mentors → 200
- [ ] GET /api/miniprogram/vip/members → 200
**管理端 API需 Token**
- [ ] POST /api/admin → 登录获取 token
- [ ] GET /api/admin/dashboard/stats → 200
- [ ] GET /api/admin/chapters → 200
- [ ] GET /api/admin/users → 200
- [ ] GET /api/admin/orders → 200注意返回字段 `orders` 不是 `data`
- [ ] GET /api/admin/track/stats → 200
- [ ] GET /api/admin/settings → 200**检查 OSS 密钥是否脱敏**
- [ ] GET /api/admin/referral-settings → 200
- [ ] GET /api/admin/author-settings → 200
**DB API需 Token**
- [ ] GET /api/db/book?action=list → 200
- [ ] GET /api/db/ckb-leads → 200
- [ ] GET /api/db/ckb-plan-stats → 200
- [ ] GET /api/db/vip-roles → 200
- [ ] GET /api/db/mentors → 200
- [ ] GET /api/db/persons → 200
**安全测试**
- [ ] 无 Token → /api/admin/settings → 401
- [ ] 错误 Token → /api/admin/settings → 401
- [ ] POST /api/admin 空 body → 错误提示
- [ ] POST /api/upload 非图片 → 拒绝
### 2.4 数据库检查
- [ ] 25 张表结构完整AutoMigrate 无报错)
- [ ] 关键数据量核对chapters/users/orders 与 API 一致
- [ ] 无重复订单号
- [ ] stats vs all-chapters 章节数交叉验证
- [ ] orders vs dashboard 收入交叉验证
### 2.5 小程序代码审查25 个页面)
- [ ] 所有页面 API 路径遵循 `/api/miniprogram/*`(不混调 admin/db
- [ ] 无废弃 API 使用wx.getUserProfile / wx.createCanvasContext
- [ ] 模块导入方式统一(不混用 import/require 和 export default/module.exports
- [ ] 无未使用工具文件(检查 utils/ 下每个文件是否被引用)
- [ ] 无硬编码baseUrl/appId/mchId/微信号/日期)
- [ ] 无 mock/test/debug 代码残留
- [ ] 无过多 console.log 调试日志
- [ ] 核心流程完整登录→阅读→购买→VIP→分销→提现→匹配
---
## 3. 测试原则
## 3. 安全必查项(每次必做)
- **路径隔离**:小程序只调 miniprogram管理端只调 admin/db不得混用。
- **鉴权**:需登录接口需带 token401 时正确跳转登录。
- **数据流**下单→支付→回调→分润推荐码绑定→访问记录VIP 资料保存→排行展示。
- **变更检查**:开发完成变更后,可参考 soul-change-checklist 做关联检查,避免遗漏。
| 检查项 | 方法 | 标准 |
|--------|------|------|
| OSS 密钥脱敏 | GET /api/admin/settings 查 ossConfig | accessKeySecret 不应返回明文 |
| 管理端登录守卫 | 直接访问 /dashboard | 应跳转 /login |
| Token 有效性 | 过期/错误 Token 访问管理 API | 返回 401 |
| 上传文件类型限制 | POST /api/upload 上传 .txt | 应拒绝 |
| 支付参数来源 | 审查小程序支付代码 | 参数必须从后端获取 |
| 小程序敏感信息 | 审查 app.js | appId/mchId 不应硬编码 |
---
## 4. 常用测试场景
## 4. 经验库(持续沉淀)
| 场景 | 验证点 |
|------|--------|
| 小程序登录 | 微信登录、手机号、token 持久化 |
| 购买与支付 | 下单、微信支付、回调更新、购买状态 |
| 推荐与分润 | 扫码/分享带 ref、绑定、分润计算 |
| VIP 功能 | 开通、资料填写、头像上传、保存、排行展示 |
| 管理端 CRUD | 列表、搜索、分页、新增、编辑、删除 |
| 提现 | 申请、审核、状态流转、到账确认 |
### 4.1 API 响应字段陷阱
不同端点返回字段名不一致,测试脚本解析前先确认结构:
| 端点 | 数据字段 | 总数字段 |
|------|----------|----------|
| /api/book/search | `results` | `total` |
| /api/admin/orders | `orders` | `total` |
| /api/admin/users | `records` | `total` |
| /api/admin/settings | 直接在根层 | — |
| /api/book/hot | `data` | — |
| /api/book/stats | `data` | — |
### 4.2 OSS 上传测试四步法
1. **配置验证**GET /api/admin/settings → ossConfig 有值
2. **上传测试**POST /api/upload -F file=@test.png → 返回 storage=oss
3. **URL 可访问**curl 返回的 URL → HTTP 200
4. **ACL 策略**:阿里云新账号默认禁止公共访问,需用签名 URL
### 4.3 小程序代码审查 grep 命令
```bash
# 废弃 API 检查
grep -rn "getUserProfile\|createCanvasContext" miniprogram/
# 模块语法混用
grep -rn "export default" miniprogram/utils/
# 硬编码检查
grep -rn "apiBase\|hardcode\|28533368\|2025-01-01" miniprogram/
# mock/test 残留
grep -rn "mock\|Mock\|测试模式\|test mode" miniprogram/pages/
# console.log 统计
grep -rc "console.log" miniprogram/pages/ | grep -v ":0$"
```
### 4.4 通用经验2026-03-15 沉淀)
| 经验 | 详情 |
|------|------|
| 密钥返回前端必须脱敏 | 后端 settings API 返回 ossConfig/apiKey 等时secret 类字段只返回 `****` |
| SPA 管理端必须有路由守卫 | 未登录用户访问任何管理页面必须跳转 /login |
| API 失败绝不伪装成功 | catch 中不可设置 success=true必须真实反馈 |
| 上线前清理 mock/test 代码 | `grep -r "mock\|test mode\|测试模式"` |
| 上线前清理 console.log | `grep -rc "console.log"` |
| 微信 API 每年检查废弃 | wx.getUserProfile(2022)、wx.createCanvasContext(即将) |
| 三端路径隔离是底线 | 小程序只调 miniprogram管理端只调 admin/db |
| 聚合统计必须交叉验证 | stats 的数字要与 list API 返回的实际条数对比 |
| 分页必须实际翻页验证 | 不只测第一页,要测 page=2 和超出范围的 page |
| 签名 URL 有有效期 | OSS 私有 bucket 用签名 URL注意设置足够长的过期时间 |
---
## 5. 产出与协同
| 产出 | 说明 |
| 产出 | 路径 |
|------|------|
| 测试用例 | 场景、步骤、期望结果 |
| 测试报告 | 通过率、失败用例、环境信息 |
| Bug 列表 | 复现步骤、关联端、严重程度 |
| 测试报告 | `开发文档/全站测试报告_YYYYMMDD.md` |
| 截图存档 | 浏览器测试自动截图 |
| 飞书通知 | 测试完成后发飞书开发群 |
| 经验沉淀 | 本文件 §4 + `.cursor/agent/软件测试/evolution/` |
**协同**:发现 Bug 时与对应开发角色(小程序/管理端/后端)对接;验收前完成测试并输出报告。
---
## 6. 何时使用本 Skill
- 编写或执行测试用例时
- 做回归测试、功能验证时
- 三端联调、接口契约验证时
- 说「测试」「测试用例」「回归测试」「功能测试」「QA」时
**协同**:发现 Bug 时与对应角色对接(小程序/管理端/后端)验收前完成测试并输出报告。

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# 开发文档不上传 GitHub
开发文档/
# 常见忽略
node_modules/
.DS_Store
*.log
.env
.env.local

View File

@@ -5,22 +5,43 @@
const { parseScene } = require('./utils/scene.js')
const DEFAULT_BASE_URL = 'https://soulapi.quwanzhi.com'
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
function getRuntimeBootstrapConfig() {
try {
const extCfg = wx.getExtConfigSync ? (wx.getExtConfigSync() || {}) : {}
return {
baseUrl: extCfg.apiBaseUrl || wx.getStorageSync('apiBaseUrl') || DEFAULT_BASE_URL,
appId: extCfg.appId || DEFAULT_APP_ID,
mchId: extCfg.mchId || DEFAULT_MCH_ID,
withdrawSubscribeTmplId: extCfg.withdrawSubscribeTmplId || DEFAULT_WITHDRAW_TMPL_ID
}
} catch (_) {
return {
baseUrl: DEFAULT_BASE_URL,
appId: DEFAULT_APP_ID,
mchId: DEFAULT_MCH_ID,
withdrawSubscribeTmplId: DEFAULT_WITHDRAW_TMPL_ID
}
}
}
const bootstrapConfig = getRuntimeBootstrapConfig()
App({
globalData: {
// API基础地址 - 连接真实后端
baseUrl: 'https://soulapi.quwanzhi.com',
// baseUrl: 'https://souldev.quwanzhi.com',
// baseUrl: 'http://localhost:8080',
// 小程序配置 - 真实AppID
appId: 'wxb8bbb2b10dec74aa',
// 运行配置:优先外部配置/缓存,其次默认值
baseUrl: bootstrapConfig.baseUrl,
appId: bootstrapConfig.appId,
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
withdrawSubscribeTmplId: bootstrapConfig.withdrawSubscribeTmplId,
// 微信支付配置
mchId: '1318592501', // 商户号
mchId: bootstrapConfig.mchId,
// 用户信息
userInfo: null,
@@ -29,7 +50,8 @@ App({
// 书籍数据
bookData: null,
totalSections: 62,
totalSections: 0,
supportWechat: '',
// 购买记录
purchasedSections: [],
@@ -86,6 +108,7 @@ App({
// 检查登录状态
this.checkLoginStatus()
this.loadRuntimeConfig()
// 加载书籍数据
this.loadBookData()
@@ -328,6 +351,23 @@ App({
}
},
async loadRuntimeConfig() {
try {
const res = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 })
const mpConfig = res?.mpConfig || {}
this.globalData.baseUrl = mpConfig.apiDomain || this.globalData.baseUrl
this.globalData.appId = mpConfig.appId || this.globalData.appId
this.globalData.mchId = mpConfig.mchId || this.globalData.mchId
this.globalData.withdrawSubscribeTmplId = mpConfig.withdrawSubscribeTmplId || this.globalData.withdrawSubscribeTmplId
this.globalData.supportWechat = mpConfig.supportWechat || mpConfig.customerWechat || mpConfig.serviceWechat || ''
try {
wx.setStorageSync('apiBaseUrl', this.globalData.baseUrl)
} catch (_) {}
} catch (e) {
console.warn('[App] 加载运行配置失败,继续使用默认配置:', e)
}
},
// 加载书籍数据
async loadBookData() {
try {
@@ -342,6 +382,7 @@ App({
if (res && (res.data || res.chapters)) {
const chapters = res.data || res.chapters || []
this.globalData.bookData = chapters
this.globalData.totalSections = res.total || chapters.length || 0
wx.setStorageSync('bookData', chapters)
}
} catch (e) {
@@ -595,13 +636,6 @@ App({
return null
},
// 模拟登录已废弃 - 不再使用
// 现在必须使用真实的微信登录获取openId作为唯一标识
mockLogin() {
console.warn('[App] mockLogin已废弃请使用真实登录')
return null
},
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
async loginWithPhone(phoneCode) {
if (!this.ensureFullAppForAuth()) {

View File

@@ -28,11 +28,7 @@ Page({
expandedPart: null,
// 附录
appendixList: [
{ id: 'appendix-1', title: '附录1Soul派对房精选对话' },
{ id: 'appendix-2', title: '附录2创业者自检清单' },
{ id: 'appendix-3', title: '附录3本书提到的工具和资源' }
],
appendixList: [],
// 每日新增章节
dailyChapters: []
@@ -77,6 +73,7 @@ Page({
const totalSections = res.total ?? rows.length
app.globalData.bookData = rows
app.globalData.totalSections = totalSections
wx.setStorageSync('bookData', rows)
// bookData过滤序言/尾声/附录,按 part 聚合,篇章顺序按 sort_order 与后台一致含「2026每日派对干货」等
@@ -134,6 +131,16 @@ Page({
}))
const baseSort = 62
const appendixList = rows
.filter(r => {
const partTitle = String(r.partTitle || r.part_title || '')
return partTitle.includes('附录')
})
.sort((a, b) => (a.sort_order ?? a.sectionOrder ?? 999999) - (b.sort_order ?? b.sectionOrder ?? 999999))
.map(c => ({
id: c.id,
title: c.section_title || c.sectionTitle || c.title || c.chapterTitle || '附录'
}))
const daily = rows
.filter(r => (r.sectionOrder ?? r.sort_order ?? 0) > baseSort)
.sort((a, b) => new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0))
@@ -152,6 +159,7 @@ Page({
this.setData({
bookData,
totalSections,
appendixList,
dailyChapters: daily,
expandedPart: this.data.expandedPart
})

View File

@@ -4,8 +4,6 @@
* 技术支持: 存客宝
*/
console.log('[Index] ===== 首页文件开始加载 =====')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
@@ -22,7 +20,7 @@ Page({
readCount: 0,
// 书籍数据
totalSections: 62,
totalSections: 0,
bookData: [],
// 精选推荐按热度排行默认显示3篇可展开更多
@@ -64,8 +62,6 @@ Page({
},
onLoad(options) {
console.log('[Index] ===== onLoad 触发 =====')
// 获取系统信息
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
@@ -74,7 +70,6 @@ Page({
// 处理分享参数(推荐码绑定)
if (options && options.ref) {
console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
}
@@ -83,16 +78,11 @@ Page({
},
onShow() {
console.log('[Index] onShow 触发')
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
// 主动触发配置加载
if (tabBar && tabBar.loadFeatureConfig) {
console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
tabBar.loadFeatureConfig()
}
@@ -102,8 +92,6 @@ Page({
} else if (tabBar) {
tabBar.setData({ selected: 0 })
}
} else {
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
}
// 更新用户状态
@@ -139,13 +127,8 @@ Page({
avatar: u.avatar || '',
isVip: true
}))
if (members.length > 0) {
console.log('[Index] 超级个体加载成功:', members.length, '人')
}
}
} catch (e) {
console.log('[Index] vip/members 请求失败:', e)
}
} catch (e) {}
// 不足 4 个则用有头像的普通用户补充
if (members.length < 4) {
try {
@@ -162,7 +145,6 @@ Page({
}
this.setData({ superMembers: members, superMembersLoading: false })
} catch (e) {
console.log('[Index] 加载超级个体失败:', e)
this.setData({ superMembersLoading: false })
}
},
@@ -192,7 +174,7 @@ Page({
featuredExpanded: false,
})
}
} catch (e) { console.log('[Index] book/hot 失败:', e) }
} catch (e) {}
// 2. 最新更新:用 book/latest-chapters 取第1条排除「序言」「尾声」「附录」
try {
@@ -233,9 +215,7 @@ Page({
})
}
}
} catch (e) {
console.log('[Index] 从服务端加载推荐失败:', e)
}
} catch (e) {}
},
async loadBookData() {
@@ -246,7 +226,7 @@ Page({
const partIds = new Set(chapters.map(c => c.partId || c.part_id || '').filter(Boolean))
this.setData({
bookData: chapters,
totalSections: res.total || chapters.length || 62,
totalSections: res.total || chapters.length || app.globalData.totalSections || 0,
partCount: partIds.size || 5
})
}
@@ -258,7 +238,7 @@ Page({
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
const readCount = Math.min(app.getReadCount(), this.data.totalSections || app.globalData.totalSections || 0)
this.setData({
isLoggedIn,
hasFullBook,
@@ -317,12 +297,6 @@ Page({
return
}
const userId = app.globalData.userInfo.id
// 2 分钟内只能点一次(与后端限频一致)
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
let avatar = (app.globalData.userInfo.avatar || app.globalData.userInfo.avatarUrl || '').trim()
@@ -363,8 +337,12 @@ Page({
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
wx.showModal({
title: '提交成功',
content: '卡若会主动添加你微信,请注意你的微信消息',
showCancel: false,
confirmText: '好的'
})
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
@@ -427,11 +405,6 @@ Page({
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
const app = getApp()
const userId = app.globalData.userInfo?.id
wx.showLoading({ title: '提交中...', mask: true })
@@ -448,7 +421,6 @@ Page({
wx.hideLoading()
this.setData({ showLeadModal: false, leadPhone: '' })
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
// 同步手机号到用户资料
try {
if (userId) {
@@ -492,8 +464,11 @@ Page({
async loadLatestChapters() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const chapters = (res && res.data) || (res && res.chapters) || []
let chapters = app.globalData.bookData || []
if (!Array.isArray(chapters) || chapters.length === 0) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
chapters = (res && res.data) || (res && res.chapters) || []
}
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
@@ -538,7 +513,7 @@ Page({
latestChapters: latestAll.slice(0, 5),
latestChaptersExpanded: false,
})
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
} catch (e) {}
},
toggleLatestExpand() {

View File

@@ -309,7 +309,7 @@ Page({
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/catalog/catalog' })
wx.switchTab({ url: '/pages/chapters/chapters' })
}
}
})
@@ -466,35 +466,6 @@ Page({
}, delay)
},
// 生成模拟匹配数据
generateMockMatch() {
const nicknames = ['创业先锋', '资源整合者', '私域专家', '导师顾问', '连续创业者']
const concepts = [
'专注私域流量运营5年帮助100+品牌实现从0到1的增长。',
'连续创业者,擅长商业模式设计和资源整合。',
'在Soul分享真实创业故事希望找到志同道合的合作伙伴。'
]
const wechats = ['soul_partner_1', 'soul_business_2024', 'soul_startup_fan']
const index = Math.floor(Math.random() * nicknames.length)
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
return {
id: `user_${Date.now()}`,
nickname: nicknames[index],
avatar: `https://picsum.photos/200/200?random=${Date.now()}`,
tags: ['创业者', '私域运营', currentType?.label || '创业合伙'],
matchScore: Math.floor(Math.random() * 20) + 80,
concept: concepts[index % concepts.length],
wechat: wechats[index % wechats.length],
commonInterests: [
{ icon: '📚', text: '都在读《创业派对》' },
{ icon: '💼', text: '对私域运营感兴趣' },
{ icon: '🎯', text: '相似的创业方向' }
]
}
},
// 上报匹配行为
async reportMatch(matchedUser) {
try {
@@ -648,18 +619,16 @@ Page({
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
} else {
// 即使API返回失败也模拟成功因为已保存本地
this.setData({ joinSuccess: true })
setTimeout(() => {
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
this.setData({
joinSuccess: false,
joinError: res.error || '提交失败,请稍后重试'
})
}
} catch (e) {
// 网络错误时也模拟成功
this.setData({ joinSuccess: true })
setTimeout(() => {
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
this.setData({
joinSuccess: false,
joinError: e.message || '网络异常,请稍后重试'
})
} finally {
this.setData({ isJoining: false })
}
@@ -737,19 +706,7 @@ Page({
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消', icon: 'none' })
} else {
// 测试模式
wx.showModal({
title: '支付服务暂不可用',
content: '是否使用测试模式购买?',
success: (res) => {
if (res.confirm) {
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
wx.showToast({ title: '测试购买成功', icon: 'success' })
this.initUserStatus()
}
}
})
wx.showToast({ title: e.message || '支付失败,请稍后重试', icon: 'none' })
}
}
},

View File

@@ -19,7 +19,7 @@ Page({
userInfo: null,
// 统计数据
totalSections: 62,
totalSections: 0,
readCount: 0,
referralCount: 0,
earnings: '-',
@@ -74,6 +74,9 @@ Page({
// 我的余额wallet 页入口展示)
walletBalance: 0,
// 我的代付链接
giftList: [],
},
onLoad() {
@@ -142,6 +145,7 @@ Page({
this.loadPendingConfirm()
this.loadVipStatus()
this.loadWalletBalance()
this.loadGiftList()
} else {
this.setData({
isLoggedIn: false,
@@ -797,12 +801,6 @@ Page({
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 跳转到找伙伴
goToMatch() {
trackClick('my', 'nav_click', '匹配')
wx.switchTab({ url: '/pages/match/match' })
},
// 退出登录
handleLogout() {
wx.showModal({
@@ -829,6 +827,35 @@ Page({
} catch (e) {}
},
async loadGiftList() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance/gifts?userId=${userId}`, silent: true })
if (res?.success && res.data?.gifts) {
this.setData({ giftList: res.data.gifts })
}
} catch (e) {}
},
onGiftShareTap(e) {
const giftCode = e.currentTarget.dataset.code
const title = e.currentTarget.dataset.title || '精选文章'
const sectionId = e.currentTarget.dataset.sectionId
this._pendingGiftShare = { giftCode, title, sectionId }
wx.showModal({
title: '分享代付链接',
content: `将「${title}」的免费阅读链接分享给好友`,
confirmText: '立即分享',
cancelText: '取消',
success: (r) => {
if (r.confirm) {
wx.shareAppMessage()
}
}
})
},
// VIP状态查询注意hasFullBook=9.9 买断,不等同 VIP
async loadVipStatus() {
const userId = app.globalData.userInfo?.id
@@ -1020,6 +1047,17 @@ Page({
stopPropagation() {},
onShareAppMessage() {
if (this._pendingGiftShare) {
const { giftCode, title, sectionId } = this._pendingGiftShare
this._pendingGiftShare = null
const ref = app.getMyReferralCode()
let path = `/pages/read/read?id=${sectionId}&gift=${giftCode}`
if (ref) path += `&ref=${ref}`
return {
title: `🎁 好友已为你解锁:${title}`,
path
}
}
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 我的',

View File

@@ -142,6 +142,26 @@
</view>
</view>
<!-- 我的代付链接 -->
<view class="card gift-card" wx:if="{{giftList.length > 0}}">
<view class="card-header">
<image class="card-icon-img" src="/assets/icons/wallet.svg" mode="aspectFit"/>
<text class="card-title">我的代付链接</text>
</view>
<view class="gift-list">
<view class="gift-item" wx:for="{{giftList}}" wx:key="giftCode">
<view class="gift-left">
<text class="gift-title">{{item.sectionTitle}}</text>
<text class="gift-meta">¥{{item.amount}} · {{item.status === 'pending' ? '待领取' : '已领取'}} · {{item.createdAt}}</text>
</view>
<view class="gift-action" wx:if="{{item.status === 'pending'}}" bindtap="onGiftShareTap" data-code="{{item.giftCode}}" data-title="{{item.sectionTitle}}" data-section-id="{{item.sectionId}}">
<text class="gift-share-btn">分享</text>
</view>
<text class="gift-done" wx:else>已送出</text>
</view>
</view>
</view>
<!-- 我的订单 + 关于作者 + 设置 -->
<view class="card menu-card">
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">

View File

@@ -251,5 +251,14 @@
.modal-btn-cancel { background: rgba(255,255,255,0.1); color: #fff; }
.modal-btn-confirm { background: #4FD1C5; color: #000; font-weight: 600; }
/* 代付链接卡片 */
.gift-list { display: flex; flex-direction: column; gap: 16rpx; }
.gift-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; }
.gift-left { flex: 1; min-width: 0; }
.gift-title { display: block; font-size: 28rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gift-meta { display: block; font-size: 22rpx; color: #9CA3AF; margin-top: 6rpx; }
.gift-share-btn { display: inline-block; padding: 8rpx 28rpx; background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: 600; border-radius: 20rpx; }
.gift-done { font-size: 24rpx; color: #6B7280; }
/* 底部留白:配合 page padding-bottom避免内容被 TabBar 遮挡 */
.bottom-space { height: calc(80rpx + env(safe-area-inset-bottom, 0px)); }

View File

@@ -13,8 +13,8 @@
* - contentSegments 解析每行mention 高亮可点;点击→确认→登录/资料校验→POST /api/miniprogram/ckb/lead
*/
import accessManager from '../../utils/chapterAccessManager'
import readingTracker from '../../utils/readingTracker'
const accessManager = require('../../utils/chapterAccessManager')
const readingTracker = require('../../utils/readingTracker')
const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
@@ -63,7 +63,7 @@ Page({
// 价格
sectionPrice: 1,
fullBookPrice: 9.9,
totalSections: 62,
totalSections: 0,
// 弹窗
showShareModal: false,
@@ -321,49 +321,31 @@ Page({
// 获取章节信息
getSectionInfo(id) {
// 特殊章节
if (id === 'preface') {
return { id: 'preface', title: '为什么我每天早上6点在Soul开播?', isFree: true, price: 0 }
}
if (id === 'epilogue') {
return { id: 'epilogue', title: '这本书的真实目的', isFree: true, price: 0 }
}
if (id.startsWith('appendix')) {
const appendixTitles = {
'appendix-1': 'Soul派对房精选对话',
'appendix-2': '创业者自检清单',
'appendix-3': '本书提到的工具和资源'
const cachedSection = (app.globalData.bookData || []).find((item) => item.id === id)
if (cachedSection) {
return {
id,
title: cachedSection.sectionTitle || cachedSection.section_title || cachedSection.title || cachedSection.chapterTitle || `章节 ${id}`,
isFree: cachedSection.isFree === true || cachedSection.is_free === true || cachedSection.price === 0,
price: cachedSection.price ?? 1
}
return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
}
// 普通章节
return {
id: id,
id,
title: this.getSectionTitle(id),
isFree: id === '1.1',
isFree: false,
price: 1
}
},
// 获取章节标题
getSectionTitle(id) {
const titles = {
'1.1': '荷包:电动车出租的被动收入模式',
'1.2': '老墨:资源整合高手的社交方法',
'1.3': '笑声背后的MBTI',
'1.4': '人性的三角结构:利益、情感、价值观',
'1.5': '沟通差的问题:为什么你说的别人听不懂',
'2.1': '相亲故事:你以为找的是人,实际是在找模式',
'2.2': '找工作迷茫者:为什么简历解决不了人生',
'2.3': '撸运费险:小钱困住大脑的真实心理',
'2.4': '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力',
'2.5': '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒',
'3.1': '3000万流水如何跑出来(退税模式解析)',
'8.1': '流量杠杆:抖音、Soul、飞书',
'9.14': '大健康私域一个月150万的70后'
const cachedSection = (app.globalData.bookData || []).find((item) => item.id === id)
if (cachedSection) {
return cachedSection.sectionTitle || cachedSection.section_title || cachedSection.title || cachedSection.chapterTitle || `章节 ${id}`
}
return titles[id] || `章节 ${id}`
return `章节 ${id}`
},
// 根据 id/mid 构造章节接口路径(优先使用 mid。必须带 userId 才能让后端正确判断付费用户并返回完整内容
@@ -679,12 +661,6 @@ Page({
})
return
}
// 2 分钟内只能点一次(与后端限频一致,与首页链接卡若共用)
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
@@ -702,8 +678,13 @@ Page({
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
const who = targetNickname || '对方'
wx.showModal({
title: '提交成功',
content: `${who} 会主动添加你微信,请注意你的微信消息`,
showCancel: false,
confirmText: '好的'
})
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
@@ -728,11 +709,6 @@ Page({
return
}
const userId = app.globalData.userInfo.id
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
wx.showToast({ title: '操作太频繁请2分钟后再试', icon: 'none' })
return
}
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
let avatar = (app.globalData.userInfo.avatar || app.globalData.userInfo.avatarUrl || '').trim()
@@ -774,8 +750,12 @@ Page({
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
wx.showModal({
title: '提交成功',
content: '卡若会主动添加你微信,请注意你的微信消息',
showCancel: false,
confirmText: '好的'
})
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
@@ -811,16 +791,15 @@ Page({
// 复制分享文案(朋友圈风格)
copyShareText() {
const { section } = this.data
const shareText = `🔥 刚看完这篇《${section?.title || 'Soul创业派对'}》,太上头了!
62个真实商业案例每个都是从0到1的实战经验。私域运营、资源整合、商业变现干货满满。
推荐给正在创业或想创业的朋友,搜"Soul创业派对"小程序就能看!
#创业派对 #私域运营 #商业案例`
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
const raw = (this.data.content || '')
.replace(/<[^>]+>/g, '\n')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/&quot;/g, '"')
.replace(/[#@]\S+/g, '')
const sentences = raw.split(/[。!?\n]+/).map(s => s.trim()).filter(s => s.length > 4)
const picked = sentences.slice(0, 5)
const shareText = title + '\n\n' + picked.join('\n\n')
wx.setClipboardData({
data: shareText,
success: () => {
@@ -864,13 +843,23 @@ Page({
shareToMoments() {
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
const raw = (this.data.content || '').replace(/[#@]\S+/g, '').replace(/\s+/g, ' ').trim()
const excerpt = raw.length > 200 ? raw.slice(0, 200) + '……' : raw.length > 100 ? raw + '……' : raw
const copyText = `${title}\n\n${excerpt}\n\n👉 来自「Soul创业派对」`
const raw = (this.data.content || '')
.replace(/<[^>]+>/g, '\n')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/&quot;/g, '"')
.replace(/[#@]\S+/g, '')
const sentences = raw.split(/[。!?\n]+/).map(s => s.trim()).filter(s => s.length > 4)
const picked = sentences.slice(0, 5)
const copyText = title + '\n\n' + picked.join('\n\n')
wx.setClipboardData({
data: copyText,
success: () => {
wx.showToast({ title: '文案已复制,去朋友圈粘贴发布', icon: 'none', duration: 2500 })
wx.showModal({
title: '文案已复制',
content: '请点击右上角「···」菜单,选择「分享到朋友圈」即可发布',
showCancel: false,
confirmText: '知道了'
})
},
fail: () => {
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
@@ -1154,15 +1143,18 @@ Page({
console.error('[Pay] API创建订单失败:', apiError)
wx.hideLoading()
// 支付接口失败时,显示客服联系方式
const supportWechat = app.globalData.supportWechat || ''
wx.showModal({
title: '支付通道维护中',
content: '微信支付正在审核中请添加客服微信28533368手动购买感谢理解',
confirmText: '复制微信号',
content: supportWechat
? `微信支付正在审核中,请添加客服微信(${supportWechat})手动购买,感谢理解!`
: '微信支付正在审核中,请联系管理员手动购买,感谢理解!',
confirmText: supportWechat ? '复制微信号' : '我知道了',
cancelText: '稍后再说',
success: (res) => {
if (res.confirm) {
if (res.confirm && supportWechat) {
wx.setClipboardData({
data: '28533368',
data: supportWechat,
success: () => {
wx.showToast({ title: '微信号已复制', icon: 'success' })
}
@@ -1202,15 +1194,18 @@ Page({
wx.showToast({ title: '已取消支付', icon: 'none' })
} else if (payErr.errMsg && payErr.errMsg.includes('requestPayment:fail')) {
// 支付失败,可能是参数错误或权限问题
const supportWechat = app.globalData.supportWechat || ''
wx.showModal({
title: '支付失败',
content: '微信支付暂不可用请添加客服微信28533368手动购买',
confirmText: '复制微信号',
content: supportWechat
? `微信支付暂不可用,请添加客服微信(${supportWechat})手动购买`
: '微信支付暂不可用,请稍后重试或联系管理员',
confirmText: supportWechat ? '复制微信号' : '我知道了',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
if (res.confirm && supportWechat) {
wx.setClipboardData({
data: '28533368',
data: supportWechat,
success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
})
}

View File

@@ -63,9 +63,8 @@ Page({
posterReferralLink: '',
posterNickname: '',
posterNicknameInitial: '',
posterCaseCount: 62,
},
posterCaseCount: 62
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
@@ -94,28 +93,17 @@ Page({
// 生成邀请码
const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
console.log('[Referral] 开始加载分销数据userId:', userInfo.id)
// 从API获取真实数据
let realData = null
try {
// app.request 第一个参数是 URL 字符串(会自动拼接 baseUrl
const res = await app.request('/api/miniprogram/referral/data?userId=' + userInfo.id)
console.log('[Referral] API返回:', JSON.stringify(res).substring(0, 200))
if (res && res.success && res.data) {
realData = res.data
console.log('[Referral] ✅ 获取推广数据成功')
console.log('[Referral] - bindingCount:', realData.bindingCount)
console.log('[Referral] - paidCount:', realData.paidCount)
console.log('[Referral] - earnings:', realData.earnings)
console.log('[Referral] - expiringCount:', realData.stats?.expiringCount)
} else {
console.log('[Referral] ❌ API返回格式错误:', res?.error || 'unknown')
}
} catch (e) {
console.log('[Referral] ❌ API调用失败:', e.message || e)
console.log('[Referral] 错误详情:', e)
console.warn('[Referral] 加载分销数据失败:', e && e.message ? e.message : e)
}
// 使用真实数据或默认值
@@ -123,15 +111,9 @@ Page({
let convertedBindings = realData?.convertedUsers || []
let expiredBindings = realData?.expiredUsers || []
console.log('[Referral] activeBindings:', activeBindings.length)
console.log('[Referral] convertedBindings:', convertedBindings.length)
console.log('[Referral] expiredBindings:', expiredBindings.length)
// 计算即将过期的数量7天内
const expiringCount = realData?.stats?.expiringCount || activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length
console.log('[Referral] expiringCount:', expiringCount)
// 计算各类统计
const bindingCount = realData?.bindingCount || activeBindings.length
const paidCount = realData?.paidCount || convertedBindings.length
@@ -153,7 +135,6 @@ Page({
purchaseCount: user.purchaseCount || 0,
conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
}
console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
return formatted
}
@@ -169,15 +150,6 @@ Page({
const availableEarningsNum = Math.max(0, totalCommissionNum - withdrawnNum - pendingWithdrawNum)
const minWithdrawAmount = realData?.minWithdrawAmount || 10
console.log('=== [Referral] 收益计算(完整版)===')
console.log('累计佣金 (totalCommission):', totalCommissionNum)
console.log('已提现金额 (withdrawnEarnings):', withdrawnNum)
console.log('待审核金额 (pendingWithdrawAmount):', pendingWithdrawNum)
console.log('可提现金额 = 累计 - 已提现 - 待审核 =', totalCommissionNum, '-', withdrawnNum, '-', pendingWithdrawNum, '=', availableEarningsNum)
console.log('最低提现金额 (minWithdrawAmount):', minWithdrawAmount)
console.log('按钮判断:', availableEarningsNum, '>=', minWithdrawAmount, '=', availableEarningsNum >= minWithdrawAmount)
console.log('✅ 按钮应该:', availableEarningsNum >= minWithdrawAmount ? '🟢 启用(绿色)' : '⚫ 禁用(灰色)')
const hasWechatId = !!(userInfo?.wechat || userInfo?.wechatId || wx.getStorageSync('user_wechat'))
this.setData({
isLoggedIn: true,
@@ -233,21 +205,6 @@ Page({
})
})
console.log('[Referral] ✅ 数据设置完成')
console.log('[Referral] - 绑定中:', this.data.bindingCount)
console.log('[Referral] - 即将过期:', this.data.expiringCount)
console.log('[Referral] - 收益:', this.data.earnings)
console.log('=== [Referral] 按钮状态验证 ===')
console.log('累计佣金 (totalCommission):', this.data.totalCommission)
console.log('待审核金额 (pendingWithdrawAmount):', this.data.pendingWithdrawAmount)
console.log('可提现金额 (availableEarnings 显示):', this.data.availableEarnings)
console.log('可提现金额 (availableEarningsNum 判断):', this.data.availableEarningsNum, typeof this.data.availableEarningsNum)
console.log('最低提现金额 (minWithdrawAmount):', this.data.minWithdrawAmount, typeof this.data.minWithdrawAmount)
console.log('按钮启用条件:', this.data.availableEarningsNum, '>=', this.data.minWithdrawAmount, '=', this.data.availableEarningsNum >= this.data.minWithdrawAmount)
console.log('✅ 最终结果: 按钮应该', this.data.availableEarningsNum >= this.data.minWithdrawAmount ? '🟢 启用' : '⚫ 禁用')
// 隐藏加载提示
wx.hideLoading()
} else {

View File

@@ -13,8 +13,8 @@ Page({
loading: false,
searched: false,
total: 0,
// 热门搜索关键词
hotKeywords: ['私域', '电商', '流量', '赚钱', '创业', 'Soul', '抖音', '变现'],
// 热门搜索关键词(运行时根据热门章节/目录动态生成)
hotKeywords: [],
// 热门章节推荐
hotChapters: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', part: '真实的人' },
@@ -47,13 +47,36 @@ Page({
part: c.part_title || c.partTitle || c.part || '',
tag: ['免费', '热门', '推荐', '最新'][i % 4] || '热门'
}))
this.setData({ hotChapters })
this.setData({
hotChapters,
hotKeywords: this.buildHotKeywords(hotChapters)
})
} else {
this.setData({ hotKeywords: this.buildHotKeywords(app.globalData.bookData || []) })
}
} catch (e) {
console.log('加载热门章节失败,使用默认数据')
this.setData({ hotKeywords: this.buildHotKeywords(app.globalData.bookData || []) })
}
},
buildHotKeywords(sourceList) {
const words = []
const pushWord = (word) => {
const w = (word || '').trim()
if (!w || w.length < 2 || words.includes(w)) return
words.push(w)
}
;(sourceList || []).forEach((item) => {
const title = String(item.title || '').replace(/[|:,.,。!?]/g, ' ')
const part = String(item.part || '').replace(/[|:,.,。!?]/g, ' ')
title.split(/\s+/).forEach(pushWord)
part.split(/\s+/).forEach(pushWord)
})
return words.slice(0, 8)
},
// 输入关键词
onInput(e) {
this.setData({ keyword: e.detail.value })
@@ -99,7 +122,6 @@ Page({
this.setData({ results: [], total: 0 })
}
} catch (e) {
console.error('搜索失败:', e)
wx.showToast({ title: '搜索失败', icon: 'none' })
this.setData({ results: [], total: 0 })
} finally {

View File

@@ -245,85 +245,66 @@ Page({
}
},
// 获取微信头像(新版授权)
async getWechatAvatar() {
// 微信原生 chooseAvatar 回调
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail?.avatarUrl
if (!tempAvatarUrl) return
wx.showLoading({ title: '上传中...', mask: true })
try {
const res = await wx.getUserProfile({
desc: '用于完善会员资料'
})
if (res.userInfo) {
const { nickName, avatarUrl: tempAvatarUrl } = res.userInfo
wx.showLoading({ title: '上传中...', mask: true })
// 1. 先上传图片到服务器
console.log('[Settings] 开始上传头像:', tempAvatarUrl)
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempAvatarUrl,
name: 'file',
formData: {
folder: 'avatars'
},
success: (uploadResult) => {
try {
const data = JSON.parse(uploadResult.data)
if (data.success) {
resolve(data)
} else {
reject(new Error(data.error || '上传失败'))
}
} catch (err) {
reject(new Error('解析响应失败'))
}
},
fail: (err) => {
reject(err)
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempAvatarUrl,
name: 'file',
formData: { folder: 'avatars' },
success: (uploadResult) => {
try {
const data = JSON.parse(uploadResult.data)
if (data.success) resolve(data)
else reject(new Error(data.error || '上传失败'))
} catch (err) {
reject(new Error('解析响应失败'))
}
})
},
fail: reject
})
// 2. 获取上传后的完整URL
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
console.log('[Settings] 头像上传成功:', avatarUrl)
// 3. 更新本地
this.setData({
userInfo: {
...this.data.userInfo,
nickname: nickName,
avatar: avatarUrl
}
})
const rawUrl = uploadRes.data.url || ''
const avatarUrl = rawUrl.startsWith('http://') || rawUrl.startsWith('https://')
? rawUrl
: app.globalData.baseUrl + rawUrl
const nickname = this.data.userInfo?.nickname || app.globalData.userInfo?.nickname || ''
this.setData({
userInfo: {
...this.data.userInfo,
nickname,
avatar: avatarUrl
}
})
const userId = app.globalData.userInfo?.id
if (userId) {
await app.request('/api/miniprogram/user/profile', {
method: 'POST',
data: { userId, nickname, avatar: avatarUrl }
})
// 4. 同步到服务器数据库
const userId = app.globalData.userInfo?.id
if (userId) {
await app.request('/api/miniprogram/user/profile', {
method: 'POST',
data: { userId, nickname: nickName, avatar: avatarUrl }
})
}
// 5. 更新全局
if (app.globalData.userInfo) {
app.globalData.userInfo.nickname = nickName
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.hideLoading()
wx.showToast({ title: '头像更新成功', icon: 'success' })
}
if (app.globalData.userInfo) {
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.hideLoading()
wx.showToast({ title: '头像更新成功', icon: 'success' })
} catch (e) {
wx.hideLoading()
console.error('[Settings] 获取头像失败:', e)
wx.showToast({
title: e.message || '获取头像失败',
icon: 'none'
console.error('[Settings] 更新头像失败:', e)
wx.showToast({
title: e.message || '上传失败,请重试',
icon: 'none'
})
}
},

View File

@@ -1,4 +1,4 @@
import accessManager from '../../utils/chapterAccessManager'
const accessManager = require('../../utils/chapterAccessManager')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')

View File

@@ -206,4 +206,4 @@ class ChapterAccessManager {
// 导出单例
const accessManager = new ChapterAccessManager()
export default accessManager
module.exports = accessManager

View File

@@ -1,211 +0,0 @@
// miniprogram/utils/payment.js
// 微信支付工具类
const app = getApp()
/**
* 发起微信支付
* @param {Object} options - 支付选项
* @param {String} options.orderId - 订单ID
* @param {Number} options.amount - 支付金额(元)
* @param {String} options.description - 商品描述
* @param {Function} options.success - 成功回调
* @param {Function} options.fail - 失败回调
*/
function wxPay(options) {
const { orderId, amount, description, success, fail } = options
wx.showLoading({
title: '正在支付...',
mask: true
})
// 1. 调用后端创建支付订单
wx.request({
url: `${app.globalData.apiBase}/payment/create`,
method: 'POST',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: {
orderId,
amount,
description,
paymentMethod: 'wechat'
},
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200) {
const paymentData = res.data
// 2. 调起微信支付
wx.requestPayment({
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType || 'RSA',
paySign: paymentData.paySign,
success: (payRes) => {
console.log('支付成功', payRes)
// 3. 通知后端支付成功
notifyPaymentSuccess(orderId, paymentData.prepayId)
wx.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
success && success(payRes)
},
fail: (payErr) => {
console.error('支付失败', payErr)
if (payErr.errMsg.indexOf('cancel') !== -1) {
wx.showToast({
title: '支付已取消',
icon: 'none'
})
} else {
wx.showToast({
title: '支付失败',
icon: 'none'
})
}
fail && fail(payErr)
}
})
} else {
wx.showToast({
title: res.data.message || '创建订单失败',
icon: 'none'
})
fail && fail(res)
}
},
fail: (err) => {
wx.hideLoading()
console.error('请求失败', err)
wx.showToast({
title: '网络请求失败',
icon: 'none'
})
fail && fail(err)
}
})
}
/**
* 通知后端支付成功
* @param {String} orderId
* @param {String} prepayId
*/
function notifyPaymentSuccess(orderId, prepayId) {
wx.request({
url: `${app.globalData.apiBase}/payment/notify`,
method: 'POST',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: {
orderId,
prepayId,
status: 'success'
},
success: (res) => {
console.log('支付通知成功', res)
},
fail: (err) => {
console.error('支付通知失败', err)
}
})
}
/**
* 查询订单状态
* @param {String} orderId
* @param {Function} callback
*/
function queryOrderStatus(orderId, callback) {
wx.request({
url: `${app.globalData.apiBase}/payment/query`,
method: 'GET',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: { orderId },
success: (res) => {
if (res.statusCode === 200) {
callback && callback(true, res.data)
} else {
callback && callback(false, null)
}
},
fail: () => {
callback && callback(false, null)
}
})
}
/**
* 购买完整电子书
* @param {Function} success
* @param {Function} fail
*/
function purchaseFullBook(success, fail) {
// 计算动态价格9.9 + (天数 * 1元)
const basePrice = 9.9
const startDate = new Date('2025-01-01') // 书籍上架日期
const today = new Date()
const daysPassed = Math.floor((today - startDate) / (1000 * 60 * 60 * 24))
const currentPrice = basePrice + daysPassed
const orderId = `ORDER_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
wxPay({
orderId,
amount: currentPrice,
description: 'Soul派对·创业实验 完整版',
success: (res) => {
// 更新本地购买状态
updatePurchaseStatus(true)
success && success(res)
},
fail
})
}
/**
* 更新购买状态
* @param {Boolean} isPurchased
*/
function updatePurchaseStatus(isPurchased) {
const userInfo = app.getUserInfo()
if (userInfo) {
userInfo.isPurchased = isPurchased
wx.setStorageSync('userInfo', userInfo)
app.globalData.userInfo = userInfo
}
}
/**
* 检查是否已购买
* @returns {Boolean}
*/
function checkPurchaseStatus() {
const userInfo = app.getUserInfo()
return userInfo ? userInfo.isPurchased : false
}
module.exports = {
wxPay,
queryOrderStatus,
purchaseFullBook,
checkPurchaseStatus,
updatePurchaseStatus
}

View File

@@ -246,4 +246,4 @@ class ReadingTracker {
// 导出单例
const readingTracker = new ReadingTracker()
export default readingTracker
module.exports = readingTracker

View File

@@ -42,7 +42,10 @@ function isInCooldown(ruleId) {
const ts = map[ruleId]
if (!ts) return false
return Date.now() - ts < COOLDOWN_MS
} catch { return false }
} catch (e) {
console.warn('[RuleEngine] 读取冷却状态失败:', e)
return false
}
}
function setCooldown(ruleId) {
@@ -50,7 +53,9 @@ function setCooldown(ruleId) {
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
map[ruleId] = Date.now()
wx.setStorageSync(RULE_COOLDOWN_KEY, map)
} catch {}
} catch (e) {
console.warn('[RuleEngine] 写入冷却状态失败:', e)
}
}
function getUserInfo() {
@@ -66,7 +71,9 @@ async function loadRules() {
_cacheTs = Date.now()
return _cachedRules
}
} catch {}
} catch (e) {
console.warn('[RuleEngine] 加载规则失败,继续使用缓存:', e)
}
return _cachedRules || []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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-A95wVqFr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BHf8KXmF.css">
<script type="module" crossorigin src="/assets/index-BI5eNUOf.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DpIZ55qK.css">
</head>
<body>
<div id="root"></div>

View File

@@ -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'
// 主菜单5 项平铺,按 Mycontent-temp 新规范)
const primaryMenuItems = [
@@ -35,22 +35,33 @@ export function AdminLayout() {
if (!mounted) return
setAuthChecked(false)
let cancelled = false
const token = getAdminToken()
if (!token) {
navigate('/login', { replace: true, state: { from: location.pathname } })
return () => {
cancelled = true
}
}
get<{ success?: boolean }>('/api/admin')
.then((data) => {
if (cancelled) return
if (data && (data as { success?: boolean }).success !== false) {
if (data?.success === true) {
setAuthChecked(true)
} else {
clearAdminToken()
navigate('/login', { replace: true })
}
})
.catch(() => {
if (!cancelled) navigate('/login', { replace: true })
if (!cancelled) {
clearAdminToken()
navigate('/login', { replace: true })
}
})
return () => {
cancelled = true
}
}, [mounted, navigate])
}, [location.pathname, mounted, navigate])
const handleLogout = async () => {
clearAdminToken()

View File

@@ -72,7 +72,7 @@ export function AdminUsersPage() {
pageSize: String(pageSize),
})
if (debouncedSearch.trim()) params.set('search', debouncedSearch.trim())
const data = await get<ListRes>(`/api/admin/users?${params}`)
const data = await get<ListRes>(`/api/admin/admin-users?${params}`)
if (data?.success) {
setRecords((data as ListRes).records || [])
setTotal((data as ListRes).total ?? 0)
@@ -130,7 +130,7 @@ export function AdminUsersPage() {
setSaving(true)
try {
if (editingUser) {
const data = await put<{ success?: boolean; error?: string }>('/api/admin/users', {
const data = await put<{ success?: boolean; error?: string }>('/api/admin/admin-users', {
id: editingUser.id,
password: formPassword || undefined,
name: formName.trim(),
@@ -144,7 +144,7 @@ export function AdminUsersPage() {
setError(data?.error || '保存失败')
}
} else {
const data = await post<{ success?: boolean; error?: string }>('/api/admin/users', {
const data = await post<{ success?: boolean; error?: string }>('/api/admin/admin-users', {
username: formUsername.trim(),
password: formPassword,
name: formName.trim(),
@@ -168,7 +168,7 @@ export function AdminUsersPage() {
const handleDelete = async (id: number) => {
if (!confirm('确定删除该管理员?')) return
try {
const data = await del<{ success?: boolean; error?: string }>(`/api/admin/users?id=${id}`)
const data = await del<{ success?: boolean; error?: string }>(`/api/admin/admin-users?id=${id}`)
if (data?.success) loadList()
else setError(data?.error || '删除失败')
} catch (e: unknown) {

View File

@@ -62,6 +62,13 @@ interface OrdersRes {
total?: number
}
function maskPhone(phone?: string) {
if (!phone) return ''
const digits = phone.replace(/\s+/g, '')
if (digits.length < 7) return digits
return `${digits.slice(0, 3)}****${digits.slice(-4)}`
}
export function DashboardPage() {
const navigate = useNavigate()
const [statsLoading, setStatsLoading] = useState(true)
@@ -355,7 +362,7 @@ export function DashboardPage() {
) : (
<RefreshCw className="w-3.5 h-3.5" />
)}
30
</button>
</CardHeader>
<CardContent>
@@ -382,6 +389,7 @@ export function DashboardPage() {
const buyer =
p.userNickname ||
users.find((u) => u.id === p.userId)?.nickname ||
maskPhone(users.find((u) => u.id === p.userId)?.phone) ||
'匿名用户'
return (
@@ -394,7 +402,7 @@ export function DashboardPage() {
<img
src={normalizeImageUrl(p.userAvatar)}
alt={buyer}
className="w-9 h-9 rounded-full object-cover flex-shrink-0 mt-0.5"
className="w-9 h-9 rounded-full object-cover shrink-0 mt-0.5"
onError={(e) => {
e.currentTarget.style.display = 'none'
const next = e.currentTarget.nextElementSibling as HTMLElement
@@ -403,7 +411,7 @@ export function DashboardPage() {
/>
) : null}
<div
className={`w-9 h-9 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac] flex-shrink-0 mt-0.5 ${p.userAvatar ? 'hidden' : ''}`}
className={`w-9 h-9 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac] shrink-0 mt-0.5 ${p.userAvatar ? 'hidden' : ''}`}
>
{buyer.charAt(0)}
</div>
@@ -418,7 +426,7 @@ export function DashboardPage() {
{buyer}
</button>
<span className="text-gray-600">·</span>
<span className="text-sm font-medium text-white truncate">
<span className="text-sm font-medium text-white truncate" title={product.title}>
{product.title}
</span>
</div>
@@ -443,7 +451,7 @@ export function DashboardPage() {
</div>
</div>
<div className="text-right ml-4 flex-shrink-0">
<div className="text-right ml-4 shrink-0">
<p className="text-sm font-bold text-[#38bdac]">
+¥{Number(p.amount).toFixed(2)}
</p>
@@ -482,13 +490,13 @@ export function DashboardPage() {
{users
.slice(0, 5)
.map((u) => (
<div
<div
key={u.id}
className="flex items-center justify-between p-4 bg-[#0a1628] rounded-lg border border-gray-700/30"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[#38bdac]/20 flex items-center justify-center text-sm font-medium text-[#38bdac]">
{u.nickname?.charAt(0) || '?'}
{(u.nickname || maskPhone(u.phone) || '?').charAt(0)}
</div>
<div>
<button
@@ -496,9 +504,9 @@ export function DashboardPage() {
onClick={() => { setDetailUserId(u.id); setShowDetailModal(true) }}
className="text-sm font-medium text-[#38bdac] hover:text-[#2da396] hover:underline text-left"
>
{u.nickname || '匿名用户'}
{u.nickname || maskPhone(u.phone) || '匿名用户'}
</button>
<p className="text-xs text-gray-500">{u.phone || '-'}</p>
<p className="text-xs text-gray-500">{maskPhone(u.phone) || '未填写手机号'}</p>
</div>
</div>
<p className="text-xs text-gray-400">
@@ -580,7 +588,7 @@ export function DashboardPage() {
<span className="text-gray-300 truncate mr-2" title={`${item.action}: ${item.target}`}>
{item.target || item.action}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center gap-2 shrink-0">
<div className="w-16 h-1.5 bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-[#38bdac] rounded-full"

View File

@@ -1,10 +1,10 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Lock, User, ShieldCheck } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { post } from '@/api/client'
import { setAdminToken } from '@/api/auth'
import { getAdminToken, setAdminToken } from '@/api/auth'
export function LoginPage() {
const navigate = useNavigate()
@@ -13,6 +13,12 @@ export function LoginPage() {
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
if (getAdminToken()) {
navigate('/dashboard', { replace: true })
}
}, [navigate])
const handleLogin = async () => {
setError('')
setLoading(true)
@@ -66,7 +72,10 @@ export function LoginPage() {
<Input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
onChange={(e) => {
setUsername(e.target.value)
if (error) setError('')
}}
placeholder="请输入用户名"
className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 focus:border-[#38bdac]"
/>
@@ -80,7 +89,10 @@ export function LoginPage() {
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => {
setPassword(e.target.value)
if (error) setError('')
}}
placeholder="请输入密码"
className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 focus:border-[#38bdac]"
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}

View File

@@ -161,7 +161,7 @@ func Load() (*Config, error) {
version := os.Getenv("APP_VERSION")
if version == "" {
version = "0.0.0"
version = "dev"
}
// 微信配置

View File

@@ -2,6 +2,7 @@ package handler
import (
"net/http"
"strconv"
"soul-api/internal/database"
"soul-api/internal/model"
@@ -11,9 +12,29 @@ import (
// AdminChaptersList GET /api/admin/chapters 从 chapters 表组树part -> chapters -> sections
func AdminChaptersList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
if pageSize < 1 {
pageSize = 20
}
if pageSize > 200 {
pageSize = 200
}
var list []model.Chapter
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"structure": []interface{}{}, "stats": nil}})
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
"structure": []interface{}{},
"records": []interface{}{},
"stats": nil,
"page": page,
"pageSize": pageSize,
"totalPages": 0,
"total": 0,
}})
return
}
type section struct {
@@ -25,6 +46,19 @@ func AdminChaptersList(c *gin.Context) {
EditionStandard *bool `json:"editionStandard,omitempty"`
EditionPremium *bool `json:"editionPremium,omitempty"`
}
type sectionRecord struct {
ID string `json:"id"`
PartID string `json:"partId"`
PartTitle string `json:"partTitle"`
ChapterID string `json:"chapterId"`
ChapterTitle string `json:"chapterTitle"`
Title string `json:"title"`
Price float64 `json:"price"`
IsFree bool `json:"isFree"`
Status string `json:"status"`
EditionStandard *bool `json:"editionStandard,omitempty"`
EditionPremium *bool `json:"editionPremium,omitempty"`
}
type chapter struct {
ID string `json:"id"`
Title string `json:"title"`
@@ -38,6 +72,7 @@ func AdminChaptersList(c *gin.Context) {
}
partMap := make(map[string]*part)
chapterMap := make(map[string]map[string]*chapter)
records := make([]sectionRecord, 0, len(list))
for _, row := range list {
if partMap[row.PartID] == nil {
partMap[row.PartID] = &part{ID: row.PartID, Title: row.PartTitle, Type: "part", Chapters: []chapter{}}
@@ -66,6 +101,19 @@ func AdminChaptersList(c *gin.Context) {
ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st,
EditionStandard: row.EditionStandard, EditionPremium: row.EditionPremium,
})
records = append(records, sectionRecord{
ID: row.ID,
PartID: row.PartID,
PartTitle: row.PartTitle,
ChapterID: row.ChapterID,
ChapterTitle: row.ChapterTitle,
Title: row.SectionTitle,
Price: price,
IsFree: isFree,
Status: st,
EditionStandard: row.EditionStandard,
EditionPremium: row.EditionPremium,
})
}
structure := make([]part, 0, len(partMap))
for _, p := range partMap {
@@ -73,9 +121,29 @@ func AdminChaptersList(c *gin.Context) {
}
var total int64
database.DB().Model(&model.Chapter{}).Count(&total)
totalPages := 0
if pageSize > 0 {
totalPages = (int(total) + pageSize - 1) / pageSize
}
start := (page - 1) * pageSize
if start > len(records) {
start = len(records)
}
end := start + pageSize
if end > len(records) {
end = len(records)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{"structure": structure, "stats": gin.H{"totalSections": total}},
"data": gin.H{
"structure": structure,
"records": records[start:end],
"stats": gin.H{"totalSections": total},
"page": page,
"pageSize": pageSize,
"totalPages": totalPages,
"total": total,
},
})
}

View File

@@ -14,6 +14,11 @@ import (
"gorm.io/gorm"
)
// AdminAppUsersList GET /api/admin/users 普通用户列表兼容入口(转发到 DBUsersList
func AdminAppUsersList(c *gin.Context) {
DBUsersList(c)
}
// AdminUsersList GET /api/admin/users 管理员用户列表(仅 super_admin
func AdminUsersList(c *gin.Context) {
claims := middleware.GetAdminClaims(c)

View File

@@ -125,8 +125,9 @@ func BalanceRechargeConfirm(c *gin.Context) {
// POST /api/miniprogram/balance/gift 小程序-代付解锁(用余额帮他人解锁章节)
func BalanceGift(c *gin.Context) {
var body struct {
GiverID string `json:"giverId" binding:"required"`
SectionID string `json:"sectionId" binding:"required"`
GiverID string `json:"giverId" binding:"required"`
SectionID string `json:"sectionId" binding:"required"`
PaidViaWechat bool `json:"paidViaWechat"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
@@ -150,46 +151,56 @@ func BalanceGift(c *gin.Context) {
}
var giftCode string
err := db.Transaction(func(tx *gorm.DB) error {
var bal model.UserBalance
if err := tx.Where("user_id = ?", body.GiverID).First(&bal).Error; err != nil || bal.Balance < price {
return fmt.Errorf("余额不足,当前 ¥%.2f,需要 ¥%.2f", bal.Balance, price)
}
code := make([]byte, 16)
rand.Read(code)
giftCode = hex.EncodeToString(code)
if err := tx.Model(&bal).Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", price),
"total_gifted": gorm.Expr("total_gifted + ?", price),
}).Error; err != nil {
return err
}
code := make([]byte, 16)
rand.Read(code)
giftCode = hex.EncodeToString(code)
tx.Create(&model.GiftUnlock{
if body.PaidViaWechat {
db.Create(&model.GiftUnlock{
GiftCode: giftCode,
GiverID: body.GiverID,
SectionID: body.SectionID,
Amount: price,
Status: "pending",
})
} else {
err := db.Transaction(func(tx *gorm.DB) error {
var bal model.UserBalance
if err := tx.Where("user_id = ?", body.GiverID).First(&bal).Error; err != nil || bal.Balance < price {
return fmt.Errorf("余额不足,当前 ¥%.2f,需要 ¥%.2f", bal.Balance, price)
}
tx.Create(&model.BalanceTransaction{
UserID: body.GiverID,
Type: "gift",
Amount: -price,
BalanceAfter: bal.Balance - price,
SectionID: &body.SectionID,
Description: fmt.Sprintf("代付章节 %s (¥%.2f)", body.SectionID, price),
if err := tx.Model(&bal).Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", price),
"total_gifted": gorm.Expr("total_gifted + ?", price),
}).Error; err != nil {
return err
}
tx.Create(&model.GiftUnlock{
GiftCode: giftCode,
GiverID: body.GiverID,
SectionID: body.SectionID,
Amount: price,
Status: "pending",
})
tx.Create(&model.BalanceTransaction{
UserID: body.GiverID,
Type: "gift",
Amount: -price,
BalanceAfter: bal.Balance - price,
SectionID: &body.SectionID,
Description: fmt.Sprintf("代付章节 %s (¥%.2f)", body.SectionID, price),
})
return nil
})
return nil
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
return
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
@@ -418,5 +429,47 @@ func BalanceGiftInfo(c *gin.Context) {
"amount": gift.Amount,
"status": gift.Status,
"giverId": gift.GiverID,
"mid": chapter.MID,
}})
}
// GET /api/miniprogram/balance/gifts?userId=xxx 我的代付列表
func BalanceGiftList(c *gin.Context) {
userId := c.Query("userId")
if userId == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"})
return
}
db := database.DB()
var gifts []model.GiftUnlock
db.Where("giver_id = ?", userId).Order("created_at DESC").Limit(50).Find(&gifts)
type giftItem struct {
GiftCode string `json:"giftCode"`
SectionID string `json:"sectionId"`
SectionTitle string `json:"sectionTitle"`
Amount float64 `json:"amount"`
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
}
var result []giftItem
for _, g := range gifts {
var ch model.Chapter
title := g.SectionID
if db.Where("id = ?", g.SectionID).First(&ch).Error == nil && ch.SectionTitle != "" {
title = ch.SectionTitle
}
result = append(result, giftItem{
GiftCode: g.GiftCode,
SectionID: g.SectionID,
SectionTitle: title,
Amount: g.Amount,
Status: g.Status,
CreatedAt: g.CreatedAt.Format("2006-01-02 15:04"),
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"gifts": result}})
}

View File

@@ -36,7 +36,7 @@ func BookAllChapters(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
return
}
freeIDs := getFreeChapterIDs(db)
freeIDs := getEffectiveFreeChapterIDs(db)
for i := range list {
if freeIDs[list[i].ID] {
t := true
@@ -112,6 +112,24 @@ func getFreeChapterIDs(db *gorm.DB) map[string]bool {
return ids
}
func getEffectiveFreeChapterIDs(db *gorm.DB) map[string]bool {
ids := getFreeChapterIDs(db)
var rows []struct {
ID string `gorm:"column:id"`
}
if err := db.Model(&model.Chapter{}).
Select("id").
Where("is_free = ? OR price = 0", true).
Find(&rows).Error; err == nil {
for _, row := range rows {
if row.ID != "" {
ids[row.ID] = true
}
}
}
return ids
}
// checkUserChapterAccess 判断 userId 是否有权读取 chapterIDVIP / 全书购买 / 单章购买)
// isPremium=true 表示增值版fullbook 买断不含增值版
func checkUserChapterAccess(db *gorm.DB, userID, chapterID string, isPremium bool) bool {
@@ -587,8 +605,7 @@ func BookStats(c *gin.Context) {
db := database.DB()
var total int64
db.Model(&model.Chapter{}).Count(&total)
var freeCount int64
db.Model(&model.Chapter{}).Where("is_free = ?", true).Count(&freeCount)
freeCount := len(getEffectiveFreeChapterIDs(db))
var totalWords struct{ S int64 }
db.Model(&model.Chapter{}).Select("COALESCE(SUM(word_count),0) as s").Scan(&totalWords)
var userCount int64

View File

@@ -340,24 +340,13 @@ func CKBIndexLead(c *gin.Context) {
// 首页固定使用全局密钥system_config > .env > 代码内置
leadKey := getCkbLeadApiKey()
// 去重限频2 分钟内同一用户/手机/微信只能提交一次
var cond []string
var args []interface{}
if body.UserID != "" {
cond = append(cond, "user_id = ?")
args = append(args, body.UserID)
}
cond = append(cond, "phone = ?")
args = append(args, phone)
cutoff := time.Now().Add(-2 * time.Minute)
var recentCount int64
if db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Where("created_at > ?", cutoff).Count(&recentCount) == nil && recentCount > 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "您操作太频繁请2分钟后再试"})
return
}
// 去重:同一用户只记录一次(首页链接卡若)
repeatedSubmit := false
var existCount int64
repeatedSubmit = db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Count(&existCount) == nil && existCount > 0
if body.UserID != "" {
var existCount int64
db.Model(&model.CkbLeadRecord{}).Where("user_id = ? AND source = ?", body.UserID, "index_link_button").Count(&existCount)
repeatedSubmit = existCount > 0
}
source := "index_link_button"
paramsJSON, _ := json.Marshal(map[string]interface{}{
@@ -478,33 +467,12 @@ func CKBLead(c *gin.Context) {
}
}
// 去重限频2 分钟内同一用户/手机/微信只能提交一次
var cond []string
var args []interface{}
if body.UserID != "" {
cond = append(cond, "user_id = ?")
args = append(args, body.UserID)
}
if phone != "" {
cond = append(cond, "phone = ?")
args = append(args, phone)
}
if wechatId != "" {
cond = append(cond, "wechat_id = ?")
args = append(args, wechatId)
}
if len(cond) > 0 {
cutoff := time.Now().Add(-2 * time.Minute)
var recentCount int64
if db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Where("created_at > ?", cutoff).Count(&recentCount) == nil && recentCount > 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "您操作太频繁请2分钟后再试"})
return
}
}
// 去重:同一用户对同一目标人物只记录一次(不再限制时间间隔,允许对不同人物立即提交)
repeatedSubmit := false
if len(cond) > 0 {
if body.UserID != "" && body.TargetUserID != "" {
var existCount int64
repeatedSubmit = db.Model(&model.CkbLeadRecord{}).Where(strings.Join(cond, " OR "), args...).Count(&existCount) == nil && existCount > 0
db.Model(&model.CkbLeadRecord{}).Where("user_id = ? AND target_person_id = ?", body.UserID, body.TargetUserID).Count(&existCount)
repeatedSubmit = existCount > 0
}
source := strings.TrimSpace(body.Source)

View File

@@ -323,7 +323,7 @@ func AdminCKBPlans(c *gin.Context) {
if keyword != "" {
values.Set("keyword", keyword)
}
planURL := ckbOpenBaseURL + "/v1/plans?" + values.Encode()
planURL := ckbOpenBaseURL + "/v1/plan/list?" + values.Encode()
req, err := http.NewRequest(http.MethodGet, planURL, nil)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "构造请求失败"})
@@ -344,6 +344,16 @@ func AdminCKBPlans(c *gin.Context) {
return
}
code, _ := parsed["code"].(float64)
if int(code) != 200 {
msg, _ := parsed["msg"].(string)
if msg == "" {
msg = "存客宝返回异常"
}
c.JSON(http.StatusOK, gin.H{"success": false, "error": msg})
return
}
var listAny interface{}
if dataVal, ok := parsed["data"].(map[string]interface{}); ok {
listAny = dataVal["list"]
@@ -359,42 +369,113 @@ func AdminCKBPlans(c *gin.Context) {
plans := make([]map[string]interface{}, 0)
if arr, ok := listAny.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
plans = append(plans, map[string]interface{}{
"id": m["id"],
"name": m["name"],
"apiKey": m["apiKey"],
"sceneId": m["sceneId"],
"scenario": m["scenario"],
"enabled": m["enabled"],
"greeting": m["greeting"],
"tips": m["tips"],
"remarkType": m["remarkType"],
"remarkFormat": m["remarkFormat"],
"addInterval": m["addInterval"],
"startTime": m["startTime"],
"endTime": m["endTime"],
"deviceGroups": m["deviceGroups"],
})
m, ok := item.(map[string]interface{})
if !ok {
continue
}
reqConf, _ := m["reqConf"].(map[string]interface{})
sceneConf, _ := m["sceneConf"].(map[string]interface{})
enabled := false
if sceneConf != nil {
if v, ok := sceneConf["enabled"].(bool); ok {
enabled = v
}
}
if m["status"] != nil {
if s, ok := m["status"].(float64); ok {
enabled = int(s) == 1
}
}
greeting, _ := mapStr(reqConf, "greeting")
tips, _ := mapStr(sceneConf, "tips")
remarkType, _ := mapStr(reqConf, "remarkType")
remarkFormat, _ := mapStr(reqConf, "remarkFormat")
startTime, _ := mapStr(reqConf, "startTime")
endTime, _ := mapStr(reqConf, "endTime")
addInterval := mapFloat(reqConf, "addFriendInterval")
if addInterval == 0 {
addInterval = mapFloat(sceneConf, "addInterval")
}
var deviceGroups interface{}
if reqConf != nil {
deviceGroups = reqConf["device"]
}
if deviceGroups == nil && sceneConf != nil {
deviceGroups = sceneConf["deviceGroups"]
}
plans = append(plans, map[string]interface{}{
"id": m["id"],
"name": m["name"],
"apiKey": m["apiKey"],
"sceneId": m["sceneId"],
"scenario": m["sceneId"],
"enabled": enabled,
"greeting": greeting,
"tips": tips,
"remarkType": remarkType,
"remarkFormat": remarkFormat,
"addInterval": addInterval,
"startTime": startTime,
"endTime": endTime,
"deviceGroups": deviceGroups,
})
}
}
total := 0
switch tv := parsed["total"].(type) {
case float64:
total = int(tv)
case int:
total = tv
case string:
if n, err := strconv.Atoi(tv); err == nil {
total = n
if dataVal, ok := parsed["data"].(map[string]interface{}); ok {
switch tv := dataVal["total"].(type) {
case float64:
total = int(tv)
case int:
total = tv
case string:
if n, err := strconv.Atoi(tv); err == nil {
total = n
}
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "plans": plans, "total": total})
}
func mapStr(m map[string]interface{}, key string) (string, bool) {
if m == nil {
return "", false
}
v, ok := m[key]
if !ok || v == nil {
return "", false
}
s, ok := v.(string)
return s, ok
}
func mapFloat(m map[string]interface{}, key string) float64 {
if m == nil {
return 0
}
v, ok := m[key]
if !ok || v == nil {
return 0
}
switch val := v.(type) {
case float64:
return val
case int:
return float64(val)
case string:
if n, err := strconv.ParseFloat(val, 64); err == nil {
return n
}
}
return 0
}
// AdminCKBPlanDetail GET /api/admin/ckb/plan-detail?planId=xxx 管理端-存客宝获客计划详情
func AdminCKBPlanDetail(c *gin.Context) {
planIDStr := c.Query("planId")

View File

@@ -13,6 +13,7 @@ import (
"soul-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// GetPublicDBConfig GET /api/miniprogram/config 公开接口,供小程序获取完整配置(与 next-project 对齐)
@@ -228,7 +229,7 @@ func AdminSettingsGet(c *gin.Context) {
}
case "oss_config":
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
out["ossConfig"] = m
out["ossConfig"] = sanitizeOSSConfig(m)
}
}
}
@@ -284,6 +285,7 @@ func AdminSettingsPost(c *gin.Context) {
}
}
if body.OssConfig != nil {
body.OssConfig = mergeOSSConfigSecret(db, body.OssConfig)
if err := saveKey("oss_config", "阿里云 OSS 配置", body.OssConfig); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存 OSS 配置失败: " + err.Error()})
return
@@ -1206,6 +1208,49 @@ func DBConfigDelete(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}
func sanitizeOSSConfig(cfg map[string]interface{}) gin.H {
out := gin.H{}
for k, v := range cfg {
out[k] = v
}
if secret, ok := out["accessKeySecret"].(string); ok && secret != "" {
out["accessKeySecret"] = "****"
}
return out
}
func mergeOSSConfigSecret(db *gorm.DB, incoming map[string]interface{}) map[string]interface{} {
if incoming == nil {
return incoming
}
secret, _ := incoming["accessKeySecret"].(string)
if secret != "" && secret != "****" {
return incoming
}
var row model.SystemConfig
if err := db.Where("config_key = ?", "oss_config").First(&row).Error; err != nil {
return incoming
}
var existing map[string]interface{}
if err := json.Unmarshal(row.ConfigValue, &existing); err != nil {
return incoming
}
existingSecret, _ := existing["accessKeySecret"].(string)
if existingSecret == "" {
return incoming
}
merged := map[string]interface{}{}
for k, v := range incoming {
merged[k] = v
}
merged["accessKeySecret"] = existingSecret
return merged
}
// DBInitGet GET /api/db/init
func DBInitGet(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "ok"}})

View File

@@ -74,7 +74,9 @@ func DBPersonSave(c *gin.Context) {
existing.Name = body.Name
existing.Aliases = body.Aliases
existing.Label = body.Label
existing.CkbApiKey = body.CkbApiKey
if strings.TrimSpace(body.CkbApiKey) != "" {
existing.CkbApiKey = body.CkbApiKey
}
existing.Greeting = body.Greeting
existing.Tips = body.Tips
existing.RemarkType = body.RemarkType
@@ -99,27 +101,20 @@ func DBPersonSave(c *gin.Context) {
} else {
existing.DeviceGroups = ""
}
db.Save(&existing)
if err := db.Save(&existing).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "person": existing})
return
}
// 新增:创建本地 Person 记录前,先在存客宝创建获客计划并获取 planId + apiKey
// 新增:创建本地人物记录,存客宝同步失败不阻断 @人物 与内容管理主链路
tok, err := genPersonToken()
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成 token 失败"})
return
}
// 1. 获取开放 API token
openToken, err := ckbOpenGetToken()
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
// 2. 构造创建计划请求体
// 参考 Cunkebao createPlanname, sceneId, scenario, remarkType, greeting, addInterval, startTime, endTime, enabled, tips, deviceGroups
name := fmt.Sprintf("SOUL链接人与事-%s", body.Name)
addInterval := 1
if body.AddFriendInterval != nil && *body.AddFriendInterval > 0 {
@@ -139,52 +134,18 @@ func DBPersonSave(c *gin.Context) {
deviceIDs = append(deviceIDs, id)
}
}
planPayload := map[string]interface{}{
"name": name,
"sceneId": 11,
"scenario": 11,
"remarkType": body.RemarkType,
"greeting": body.Greeting,
"addInterval": addInterval,
"startTime": startTime,
"endTime": endTime,
"enabled": true,
"tips": body.Tips,
"distributionEnabled": false,
}
if len(deviceIDs) > 0 {
planPayload["deviceGroups"] = deviceIDs
}
planID, ckbCreateData, ckbResponse, err := ckbOpenCreatePlan(openToken, planPayload)
if err != nil {
out := gin.H{"success": false, "error": "创建存客宝计划失败: " + err.Error()}
if ckbResponse != nil {
out["ckbResponse"] = ckbResponse
}
c.JSON(http.StatusOK, out)
return
}
// 3. 用 planId 拉计划详情,获取 apiKey
apiKey, err := ckbOpenGetPlanDetail(openToken, planID)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建成功但获取计划密钥失败: " + err.Error()})
return
}
newPerson := model.Person{
PersonID: body.PersonID,
Token: tok,
Name: body.Name,
Aliases: body.Aliases,
Label: body.Label,
CkbApiKey: apiKey,
CkbPlanID: planID,
Greeting: body.Greeting,
Tips: body.Tips,
RemarkType: body.RemarkType,
RemarkFormat: body.RemarkFormat,
PersonID: body.PersonID,
Token: tok,
Name: body.Name,
Aliases: body.Aliases,
Label: body.Label,
CkbApiKey: strings.TrimSpace(body.CkbApiKey),
Greeting: body.Greeting,
Tips: body.Tips,
RemarkType: body.RemarkType,
RemarkFormat: body.RemarkFormat,
AddFriendInterval: addInterval,
StartTime: startTime,
EndTime: endTime,
@@ -201,7 +162,67 @@ func DBPersonSave(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
resp := gin.H{"success": true, "person": newPerson}
planPayload := map[string]interface{}{
"name": name,
"sceneId": 11,
"scenario": 11,
"remarkType": body.RemarkType,
"greeting": body.Greeting,
"addInterval": addInterval,
"startTime": startTime,
"endTime": endTime,
"enabled": true,
"tips": body.Tips,
"distributionEnabled": false,
}
if len(deviceIDs) > 0 {
planPayload["deviceGroups"] = deviceIDs
}
openToken, tokenErr := ckbOpenGetToken()
if tokenErr != nil {
resp["ckbSyncError"] = tokenErr.Error()
resp["message"] = "人物已保存,存客宝同步失败,可稍后补同步"
c.JSON(http.StatusOK, resp)
return
}
planID, ckbCreateData, ckbResponse, planErr := ckbOpenCreatePlan(openToken, planPayload)
if planErr != nil {
resp["ckbSyncError"] = "创建存客宝计划失败: " + planErr.Error()
if ckbResponse != nil {
resp["ckbResponse"] = ckbResponse
}
resp["message"] = "人物已保存,存客宝同步失败,可稍后补同步"
c.JSON(http.StatusOK, resp)
return
}
apiKey, detailErr := ckbOpenGetPlanDetail(openToken, planID)
if detailErr != nil {
db.Model(&model.Person{}).Where("person_id = ?", newPerson.PersonID).Update("ckb_plan_id", planID)
newPerson.CkbPlanID = planID
resp["person"] = newPerson
resp["ckbSyncError"] = "创建成功但获取计划密钥失败: " + detailErr.Error()
resp["message"] = "人物已保存,存客宝部分同步成功"
if len(ckbCreateData) > 0 {
resp["ckbCreateResult"] = ckbCreateData
}
c.JSON(http.StatusOK, resp)
return
}
newPerson.CkbPlanID = planID
newPerson.CkbApiKey = apiKey
if err := db.Model(&model.Person{}).Where("person_id = ?", newPerson.PersonID).Updates(map[string]interface{}{
"ckb_api_key": apiKey,
"ckb_plan_id": planID,
}).Error; err == nil {
resp["person"] = newPerson
}
if len(ckbCreateData) > 0 {
resp["ckbCreateResult"] = ckbCreateData
}

View File

@@ -83,10 +83,11 @@ func Setup(cfg *config.Config) *gin.Engine {
admin.POST("/author-settings", handler.AdminAuthorSettingsPost)
admin.PUT("/orders/refund", handler.AdminOrderRefund)
admin.POST("/content/upload", handler.AdminContentUpload)
admin.GET("/users", handler.AdminUsersList)
admin.POST("/users", handler.AdminUsersAction)
admin.PUT("/users", handler.AdminUsersAction)
admin.DELETE("/users", handler.AdminUsersAction)
admin.GET("/users", handler.AdminAppUsersList)
admin.GET("/admin-users", handler.AdminUsersList)
admin.POST("/admin-users", handler.AdminUsersAction)
admin.PUT("/admin-users", handler.AdminUsersAction)
admin.DELETE("/admin-users", handler.AdminUsersAction)
admin.GET("/orders", handler.OrdersList)
admin.GET("/balance/summary", handler.BalanceSummary)
admin.GET("/shensheshou/query", handler.AdminShensheShouQuery)
@@ -341,6 +342,7 @@ func Setup(cfg *config.Config) *gin.Engine {
miniprogram.POST("/balance/gift", handler.BalanceGift)
miniprogram.POST("/balance/gift/redeem", handler.BalanceGiftRedeem)
miniprogram.GET("/balance/gift/info", handler.BalanceGiftInfo)
miniprogram.GET("/balance/gifts", handler.BalanceGiftList)
miniprogram.POST("/balance/refund", handler.BalanceRefund)
miniprogram.GET("/balance/transactions", handler.BalanceTransactions)
}

Binary file not shown.

File diff suppressed because it is too large Load Diff