更新个人资料页和文章类型需求分析,增加增值版与普通版的计价逻辑及相关字段展示。优化小程序页面,确保用户在选择导师顾问时的跳转逻辑一致性,提升用户体验。调整文档以反映最新开发进展,增强功能可用性与团队协作效率。
This commit is contained in:
@@ -10,3 +10,10 @@
|
||||
|
||||
- **展示/编辑页一致性**:展示页(enhanced_professional_profile)与编辑页(comprehensive_profile_editor_v1_1)需字段一一对应、配色统一。
|
||||
- **skills「我擅长」**:展示页已有,编辑页必须补充;验收时确认两页数据一致。
|
||||
|
||||
## 文章类型(普通版/增值版)需求分析会议
|
||||
|
||||
- **普通版**:9.9 元买断,与现有 fullbook 一致。
|
||||
- **增值版**:基础价 + 后 N 章(N 可配置)按章额外付费;每购一章价格累加该章单价。
|
||||
- **已确认**:普通版与增值版**分开、互斥**,用户购买其一,非叠加。
|
||||
- **待输出**:增值版基础价是否固定 9.9;N 默认值。
|
||||
|
||||
@@ -15,3 +15,9 @@
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **profile API**:`GET/POST /api/miniprogram/user/profile` 已覆盖 skills 等全部扩展字段;无需新增接口。
|
||||
|
||||
## 文章类型(普通版/增值版)需求分析会议
|
||||
|
||||
- **premium_config**:system_config 新增 premium_base_price、premium_chapter_count(后 N 个 section)。
|
||||
- **增值章节购买**:沿用 product_type=section、product_id=section_id,金额为 chapters.price。
|
||||
- **purchase-status 扩展**:需返回 editionType、premiumPurchasedSections 等。
|
||||
|
||||
@@ -13,3 +13,9 @@
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **展示/编辑页协同**:profile-show 与 profile-edit 共用同一 API,skills 等扩展字段需双向同步;配色统一为 enhanced(#5EEAD4)强化品牌一致。
|
||||
|
||||
## 文章类型(普通版/增值版)需求分析会议
|
||||
|
||||
- **增值版计价**:基础价 + Σ(增值章节单价),按章购买、按章累加。
|
||||
- **后 N 章**:指全书最后 N 个 section;N 可配置。
|
||||
- **普通版与增值版**:分开、互斥的两套产品,用户购买其一。
|
||||
|
||||
@@ -23,3 +23,9 @@
|
||||
- **问题**:弹窗内 input 文字贴边,padding 直接写在 input 上导致布局异常。
|
||||
- **正确做法**:按 Skill §6,用 view 包裹,padding 写 view,内部 input 设 `width: 100%`。
|
||||
- **修改**:match 页资源对接两个输入框 + 联系方式输入框,统一改为 `form-input-wrap` + `form-input-inner` / `input-field-wrap` + `input-field-inner` 结构。
|
||||
|
||||
## 文章类型(普通版/增值版)需求分析会议
|
||||
|
||||
- **选购**:目录/书籍页区分普通版 9.9 与增值版(基础价 + 增值章按章付费)。
|
||||
- **解锁**:进入增值章节未购时展示「该章需额外 ¥X.X 解锁」,点击发起支付。
|
||||
- **依赖**:purchase-status 需返回 editionType、premiumPurchasedSections。
|
||||
|
||||
@@ -9,3 +9,8 @@
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **无新增任务**:个人资料展示/编辑为 C 端能力,管理端沿用现有能力即可。
|
||||
|
||||
## 文章类型(普通版/增值版)需求分析会议
|
||||
|
||||
- **书籍版本配置**:支持选择普通版/增值版;配置「后 N 章」的 N。
|
||||
- **章节标注**:章节列表需标注是否为增值章节;单价取自 chapters.price。
|
||||
|
||||
@@ -9,3 +9,8 @@
|
||||
## 个人资料页实现评估会议
|
||||
|
||||
- **验证点**:profile-show 与 profile-edit 字段一一对应;保存后两页及「我的」数据一致;手机/微信号脱敏与复制;头像上传、昵称、MBTI 选择。
|
||||
|
||||
## 文章类型(普通版/增值版)需求分析会议
|
||||
|
||||
- **用例**:普通版 9.9 买断全书;增值版基础价+逐章购买、价格累加正确。
|
||||
- **边界**:N=0、N=全书、章节无单价时的降级逻辑。
|
||||
|
||||
153
.cursor/meeting/2026-02-28_文章类型普通版增值版需求分析.md
Normal file
153
.cursor/meeting/2026-02-28_文章类型普通版增值版需求分析.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# 会议纪要 - 2026-02-28 | 文章增加类型(普通版 / 增值版)需求分析
|
||||
|
||||
> 本文件由**助理橙子**在会议结束后自动生成。
|
||||
|
||||
---
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **时间**:2026-02-28
|
||||
- **议题**:文章增加类型(普通版 / 增值版),增值版后 N 章额外付费、按章累加计价
|
||||
- **触发方式**:开个会议分析需求
|
||||
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员
|
||||
|
||||
---
|
||||
|
||||
## 各角色发言
|
||||
|
||||
### 【产品经理】
|
||||
|
||||
**需求理解**:
|
||||
1. **普通版**:全书 9.9 元,一次性买断,与现有 fullbook 逻辑一致。
|
||||
2. **增值版**:基础价 + 「后 N 章」(N 可配置,如 10)需额外付费;每多看一章,价格 = 已付金额 + 该章单价,按章累加。
|
||||
3. **两者关系**:普通版与增值版是**分开的、互斥的两套产品**,用户购买其一,非叠加关系。
|
||||
|
||||
**待澄清**:
|
||||
- 「后 N 章」是指全书最后 N 个 section,还是最后 N 个 chapter?
|
||||
- 增值版基础价是否仍为 9.9,还是单独定价?
|
||||
|
||||
**用户价值**:用阶梯付费降低首购门槛,提升付费转化;增值版满足深度阅读用户需求。
|
||||
|
||||
---
|
||||
|
||||
### 【后端开发】
|
||||
|
||||
**现状**:
|
||||
- `chapters` 表:`id`、`part_id`、`chapter_id`、`section_title`、`price` 等;现有按 section 购买、fullbook 9.9。
|
||||
- `orders` 表:`product_type` 含 `section`、`fullbook`、`vip`;`product_id` 存 section id 或 `fullbook`。
|
||||
|
||||
**技术方案建议**:
|
||||
1. **书/产品类型**:增加 `book_edition` 或 `product_edition` 概念:
|
||||
- `standard`:普通版,9.9 买断
|
||||
- `premium`:增值版,基础价 + 增值章节按章付费
|
||||
2. **配置**:`system_config` 增加 `premium_config`:
|
||||
- `premium_base_price`:增值版基础价
|
||||
- `premium_chapter_count`:后 N 章(section 数量或 chapter 数量需与产品约定)
|
||||
- `premium_section_ids`:或直接配置增值章节 id 列表(灵活)
|
||||
3. **接口**:
|
||||
- `GET /api/miniprogram/user/purchase-status`:需区分普通版 / 增值版购买态,返回 `editionType`、`premiumPurchasedSections` 等
|
||||
- 支付:`product_type` 扩展 `section_premium` 或沿用 `section`,`product_id` 为 section id,金额按章节 price 累加
|
||||
4. **权限**:普通版权限独立;增值版权限 = 增值版基础价已购 + 该 section 已单独付费;两者互斥。
|
||||
|
||||
---
|
||||
|
||||
### 【管理端开发工程师】
|
||||
|
||||
**管理端需求**:
|
||||
1. **书籍/版本配置**:支持选择「普通版 / 增值版」或为同一本书配置两种版本。
|
||||
2. **增值章节配置**:配置「后 N 章」的 N,或勾选具体 section 作为增值章节。
|
||||
3. **章节单价**:增值章节的单价在章节编辑中维护(现有 `chapters.price`)。
|
||||
4. **价格展示**:在书籍/章节管理列表中区分普通版、增值版及增值章节。
|
||||
|
||||
**接口依赖**:需 `GET/POST /api/db/chapters` 支持 `is_premium` 或类似标记;`/api/db/config` 或 `/api/admin/settings` 支持 premium_config。
|
||||
|
||||
---
|
||||
|
||||
### 【小程序开发工程师】
|
||||
|
||||
**C 端体验**:
|
||||
1. **选购**:目录/书籍页区分「普通版 9.9」与「增值版 基础价 + 增值章节按章付费」。
|
||||
2. **阅读**:进入增值章节时,未购则展示「该章需额外 ¥X.X 解锁」或类似提示,点击发起支付。
|
||||
3. **支付流程**:与现有一致,`product_type`、`product_id`、`amount` 由后端计算并返回。
|
||||
4. **权限**:依赖 `purchase-status` 返回的 `editionType`、`premiumPurchasedSections` 等判断是否可读。
|
||||
|
||||
**接口**:需 `miniprogram` 组下的 `purchase-status`、`pay` 支持增值版逻辑。
|
||||
|
||||
---
|
||||
|
||||
### 【测试人员】
|
||||
|
||||
**测试重点**:
|
||||
1. 普通版 9.9 买断,全书可读。
|
||||
2. 增值版:基础价购买后,后 N 章仍锁定;逐章购买,价格累加正确。
|
||||
3. 边界:N=0、N=全书、章节无单价时的降级逻辑。
|
||||
4. 三端:管理端配置 → API 返回 → 小程序展示、支付、阅读权限。
|
||||
|
||||
---
|
||||
|
||||
## 讨论过程
|
||||
|
||||
**产品经理**:建议「后 N 章」先按 section 数量实现,便于与现有 `chapters` 结构对齐;后续可扩展为按 chapter。
|
||||
|
||||
**后端开发**:同意;建议 `premium_chapter_count` 表示「最后 N 个 section」,section 顺序按 `sort_order` 或 `id` 排序。
|
||||
|
||||
**管理端开发工程师**:需在章节列表中标注「是否增值章节」,并在书籍级配置中设置 N。
|
||||
|
||||
---
|
||||
|
||||
## 会议决议
|
||||
|
||||
1. **版本类型**:支持「普通版」(9.9 买断)与「增值版」(基础价 + 后 N 章按章付费);**两者分开、互斥**,用户只能购买其一。
|
||||
2. **增值章节**:「后 N 章」指全书最后 N 个 **section**(与 chapters 表结构一致);N 为可配置参数。
|
||||
3. **计价规则**:增值版基础价可配置(默认建议 9.9);增值章节单价取自 `chapters.price`;每购一章,实付 = 该章 price。
|
||||
4. **订单**:`product_type` 保留 `section`、`fullbook`;普通版用 `fullbook`;增值版用 `fullbook_premium`(基础价)和 `section`(增值章);增值章节购买用 `product_type=section`、`product_id=section_id`。
|
||||
5. **待确认项**:增值版基础价是否固定 9.9;N 默认值。
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
| 责任角色 | 任务 | 优先级 | 截止建议 |
|
||||
|---------|------|--------|---------|
|
||||
| 产品经理 | 输出增值版 MRD:基础价、N 默认值、与普通版关系 | 高 | 需求定稿前 |
|
||||
| 后端开发 | 设计 premium_config、扩展 purchase-status / pay | 高 | 方案评审后 |
|
||||
| 管理端开发工程师 | 增值章节配置 UI、书籍版本选择 | 中 | 接口就绪后 |
|
||||
| 小程序开发工程师 | 增值版选购与章节解锁流程、支付衔接 | 中 | 接口就绪后 |
|
||||
| 测试人员 | 编写增值版测试用例、边界场景 | 中 | 开发完成前 |
|
||||
|
||||
---
|
||||
|
||||
## 问题与作答区
|
||||
|
||||
| # | 问题 | 责任角色 | 作答 |
|
||||
|---|------|---------|------|
|
||||
| 1 | 「后 N 章」按 section 还是 chapter 计数? | 产品经理 | 决议:按 section |
|
||||
| 2 | 增值版基础价是否固定 9.9? | 产品经理 | (待补充) |
|
||||
| 3 | 普通版与增值版是否互斥?用户能否同时拥有? | 产品经理 | **已确认:分开、互斥**,用户购买其一 |
|
||||
| 4 | N 的默认值建议?(如 10) | 产品经理 | (待补充) |
|
||||
|
||||
---
|
||||
|
||||
## 各角色经验与业务理解更新
|
||||
|
||||
### 产品经理
|
||||
- 文章/书籍可区分为普通版与增值版,增值版采用「基础价 + 增值章节按章付费」模式。
|
||||
|
||||
### 后端开发
|
||||
- 增值版需新增 `premium_config`,含 `premium_chapter_count`(后 N 个 section)、`premium_base_price`;增值章节购买沿用 `section` 订单。
|
||||
|
||||
### 管理端开发工程师
|
||||
- 需支持「增值版配置」与「增值章节」的 N、单价维护。
|
||||
|
||||
### 小程序开发工程师
|
||||
- 增值版需在目录与阅读页区分普通/增值,未购增值章节时展示解锁与支付入口。
|
||||
|
||||
### 测试人员
|
||||
- 增值版需覆盖:基础价购买、逐章购买、价格累加、权限边界、配置 N=0/全书的异常场景。
|
||||
|
||||
### 团队共享
|
||||
- 增值版计价规则:基础价 + Σ(增值章节单价),按章购买、按章累加。
|
||||
|
||||
---
|
||||
|
||||
*会议纪要由助理橙子生成 | 各角色经验已同步至 `agent/{角色}/evolution/2026-02-28.md`*
|
||||
@@ -61,3 +61,4 @@ YYYY-MM-DD_会议主题.md
|
||||
| 2026-02-27 | 开发进度同步会议 | 产品、后端、管理端、小程序 | [2026-02-27_开发进度同步会议.md](2026-02-27_开发进度同步会议.md) |
|
||||
| 2026-02-28 | 临时需求池 stitch_soul 需求评审(含页面重构专项·10 张图全覆盖) | 产品、后端、管理端、小程序、测试 | [2026-02-28_临时需求池stitch_soul需求评审.md](2026-02-28_临时需求池stitch_soul需求评审.md) |
|
||||
| 2026-02-28 | 个人资料页实现评估(profile-show / profile-edit) | 产品、后端、管理端、小程序、测试 | [2026-02-28_个人资料页实现评估.md](2026-02-28_个人资料页实现评估.md) |
|
||||
| 2026-02-28 | 文章类型(普通版/增值版)需求分析 | 产品、后端、管理端、小程序、测试 | [2026-02-28_文章类型普通版增值版需求分析.md](2026-02-28_文章类型普通版增值版需求分析.md) |
|
||||
|
||||
@@ -201,12 +201,7 @@ Page({
|
||||
async handleMatchClick() {
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
// stitch_soul:导师顾问 → 跳转选择导师页
|
||||
if (currentType && currentType.id === 'mentor') {
|
||||
wx.navigateTo({ url: '/pages/mentors/mentors' })
|
||||
return
|
||||
}
|
||||
|
||||
// 导师顾问:先播匹配动画,动画完成后再跳转(不在此处直接跳)
|
||||
// 找伙伴/资源对接:需先完善联系方式
|
||||
if (this.data.isLoggedIn && currentType?.matchFromDB) {
|
||||
await this.ensureContactInfo(() => this._handleMatchClickInner(currentType))
|
||||
@@ -346,7 +341,7 @@ Page({
|
||||
this.startMatch()
|
||||
},
|
||||
|
||||
// 匹配动画后弹出加入确认
|
||||
// 匹配动画后弹出加入确认(导师顾问:动画完成后直接跳转导师页)
|
||||
startMatchingAnimation(currentType) {
|
||||
// 显示匹配中状态
|
||||
this.setData({
|
||||
@@ -360,19 +355,23 @@ Page({
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 500)
|
||||
|
||||
// 1-3秒随机延迟后显示弹窗
|
||||
const delay = Math.random() * 2000 + 1000
|
||||
// 1.5-3秒后:导师顾问→跳转;其他类型→弹窗
|
||||
const delay = Math.random() * 1500 + 1500
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
this.setData({
|
||||
isMatching: false,
|
||||
showJoinModal: true,
|
||||
joinType: currentType.id,
|
||||
joinTypeLabel: currentType.matchLabel || currentType.label,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: false
|
||||
})
|
||||
this.setData({ isMatching: false })
|
||||
if (currentType && currentType.id === 'mentor') {
|
||||
wx.navigateTo({ url: '/pages/mentors/mentors' })
|
||||
} else {
|
||||
this.setData({
|
||||
showJoinModal: true,
|
||||
joinType: currentType.id,
|
||||
joinTypeLabel: currentType.matchLabel || currentType.label,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: false
|
||||
})
|
||||
}
|
||||
}, delay)
|
||||
},
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const app = getApp()
|
||||
|
||||
const MOCK_ENRICHMENT = [
|
||||
{ mbti: 'ENTJ', region: '深圳', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', bestMonth: '做跨境电商独立站,单月GMV突破200万,净利润35万', achievement: '从0到1搭建了30人的电商团队,年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商,放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' },
|
||||
{ mbti: 'INFP', region: '杭州', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', bestMonth: '旅游账号30天涨粉10万,带货佣金收入12万', achievement: '帮助3个素人打造个人IP,每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵,全网粉丝50万+' },
|
||||
{ mbti: 'INTP', region: '厦门', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', bestMonth: 'AI客服系统外包项目,单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' },
|
||||
{ mbti: 'ESTP', region: '成都', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', bestMonth: '撮合景区合作,居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群,活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人,打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' }
|
||||
{ mbti: 'ENTJ', region: '深圳', industry: '跨境电商', position: '创始人', businessScale: '年GMV 3000万+', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', wechatRaw: 'wxid_entj001', bestMonth: '做跨境电商独立站,单月GMV突破200万,净利润35万', achievement: '从0到1搭建了30人的电商团队,年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商,放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' },
|
||||
{ mbti: 'INFP', region: '杭州', industry: '新媒体/电商', position: '创始人', businessScale: '年GMV 5000万+', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', wechatRaw: 'wxid_****abc', bestMonth: '旅游账号30天涨粉10万,带货佣金收入12万', achievement: '帮助3个素人打造个人IP,每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵,全网粉丝50万+' },
|
||||
{ mbti: 'INTP', region: '厦门', industry: 'SaaS/技术', position: '技术合伙人', businessScale: '天使轮200万', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', wechatRaw: 'wxid_intp003', bestMonth: 'AI客服系统外包项目,单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' },
|
||||
{ mbti: 'ESTP', region: '成都', industry: '资源对接', position: '联合创始人', businessScale: '50+城市', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', wechatRaw: 'wxid_estp004', bestMonth: '撮合景区合作,居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群,活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人,打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' }
|
||||
]
|
||||
|
||||
Page({
|
||||
@@ -33,8 +33,16 @@ Page({
|
||||
this.setData({ member: this.enrichAndFormat({
|
||||
id: u.id, name: u.vipName || u.vip_name || u.nickname || '创业者',
|
||||
avatar: u.vipAvatar || u.vip_avatar || u.avatar || '', isVip: u.is_vip === 1,
|
||||
contactRaw: u.vipContact || u.vip_contact || u.phone || '', project: u.vipProject || u.vip_project || '',
|
||||
bio: u.vipBio || u.vip_bio || '', mbti: '', region: '', skills: '',
|
||||
contactRaw: u.vipContact || u.vip_contact || u.phone || '',
|
||||
wechatId: u.wechatId || u.wechat_id,
|
||||
project: u.vipProject || u.vip_project || u.projectIntro || u.project_intro || '',
|
||||
industry: u.industry, position: u.position, businessScale: u.businessScale || u.business_scale,
|
||||
skills: u.skills, mbti: u.mbti, region: u.region,
|
||||
storyBestMonth: u.storyBestMonth || u.story_best_month,
|
||||
storyAchievement: u.storyAchievement || u.story_achievement,
|
||||
storyTurning: u.storyTurning || u.story_turning,
|
||||
helpOffer: u.helpOffer || u.help_offer,
|
||||
helpNeed: u.helpNeed || u.help_need,
|
||||
}), loading: false })
|
||||
return
|
||||
}
|
||||
@@ -54,21 +62,29 @@ Page({
|
||||
isVip: raw.isVip || raw.is_vip === 1,
|
||||
mbti: raw.mbti || mock.mbti,
|
||||
region: raw.region || mock.region,
|
||||
industry: raw.industry || mock.industry,
|
||||
position: raw.position || mock.position,
|
||||
businessScale: raw.businessScale || raw.business_scale || mock.businessScale,
|
||||
skills: raw.skills || mock.skills,
|
||||
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || mock.contactRaw,
|
||||
bestMonth: raw.bestMonth || mock.bestMonth,
|
||||
achievement: raw.achievement || mock.achievement,
|
||||
turningPoint: raw.turningPoint || mock.turningPoint,
|
||||
canHelp: raw.canHelp || mock.canHelp,
|
||||
needHelp: raw.needHelp || mock.needHelp,
|
||||
project: raw.project || raw.vipProject || raw.vip_project || mock.project
|
||||
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || raw.phone || mock.contactRaw,
|
||||
wechatRaw: raw.wechatRaw || raw.wechatId || raw.wechat_id || mock.wechatRaw,
|
||||
bestMonth: raw.bestMonth || raw.storyBestMonth || raw.story_best_month || mock.bestMonth,
|
||||
achievement: raw.achievement || raw.storyAchievement || raw.story_achievement || mock.achievement,
|
||||
turningPoint: raw.turningPoint || raw.storyTurning || raw.story_turning || mock.turningPoint,
|
||||
canHelp: raw.canHelp || raw.helpOffer || raw.help_offer || mock.canHelp,
|
||||
needHelp: raw.needHelp || raw.helpNeed || raw.help_need || mock.needHelp,
|
||||
project: raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro || mock.project
|
||||
}
|
||||
|
||||
const contact = merged.contactRaw || ''
|
||||
const wechat = merged.wechatRaw || ''
|
||||
const isMatched = (app.globalData.matchedUsers || []).includes(merged.id)
|
||||
merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : ''
|
||||
merged.contactUnlocked = isMatched
|
||||
merged.contactFull = contact
|
||||
merged.wechatDisplay = wechat ? (wechat.slice(0, 4) + '****' + (wechat.length > 8 ? wechat.slice(-3) : '')) : ''
|
||||
merged.wechatUnlocked = isMatched
|
||||
merged.wechatFull = wechat
|
||||
return merged
|
||||
},
|
||||
|
||||
@@ -86,6 +102,12 @@ Page({
|
||||
wx.setClipboardData({ data: c, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
copyWechat() {
|
||||
const w = this.data.member?.wechatFull
|
||||
if (!w) return
|
||||
wx.setClipboardData({ data: w, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
|
||||
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
|
||||
goBack() { wx.navigateBack() },
|
||||
|
||||
@@ -1,101 +1,147 @@
|
||||
<!-- Soul创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><text class="back-arrow">‹</text></view>
|
||||
<text class="nav-title">超级个体</text>
|
||||
<view class="nav-ph"></view>
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<text class="nav-icon">‹</text>
|
||||
</view>
|
||||
<text class="nav-title">个人资料</text>
|
||||
<view class="nav-right">
|
||||
<view class="nav-icon-wrap"><text class="nav-icon">⋯</text></view>
|
||||
<view class="nav-icon-wrap"><text class="nav-icon-dot">●</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view scroll-y class="scroll-wrap" wx:if="{{member}}">
|
||||
<!-- ===== 顶部名片 ===== -->
|
||||
<view class="card-hero">
|
||||
<view class="hero-deco"></view>
|
||||
<view class="hero-deco2"></view>
|
||||
<view class="hero-body">
|
||||
<view class="avatar-ring {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<!-- 顶部 profile 卡片 -->
|
||||
<view class="card-profile">
|
||||
<view class="profile-deco"></view>
|
||||
<view class="profile-body">
|
||||
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{member.name[0] || '创'}}</text></view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
<text class="hero-name">{{member.name}}</text>
|
||||
<view class="hero-tags">
|
||||
<text class="tag-item tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<text class="tag-item tag-region" wx:if="{{member.region}}">📍{{member.region}}</text>
|
||||
<text class="profile-name">{{member.name}}</text>
|
||||
<view class="profile-tags">
|
||||
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<text class="tag tag-region" wx:if="{{member.region}}"><text class="pin-icon">📍</text>{{member.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 基本信息 ===== -->
|
||||
<!-- 基本信息 -->
|
||||
<view class="card">
|
||||
<view class="card-head"><text class="card-icon">👤</text><text class="card-label">基本信息</text></view>
|
||||
<view class="field" wx:if="{{member.skills}}">
|
||||
<text class="f-key">我擅长</text>
|
||||
<text class="f-val">{{member.skills}}</text>
|
||||
<view class="card-head">
|
||||
<text class="card-icon">👤</text>
|
||||
<text class="card-label">基本信息</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.contactDisplay}}">
|
||||
<text class="f-key">联系方式</text>
|
||||
<view class="f-contact">
|
||||
<text class="f-val masked">{{member.contactDisplay}}</text>
|
||||
<view class="lock-chip" wx:if="{{!member.contactUnlocked}}" bindtap="unlockContact">
|
||||
<text class="lock-icon">🔒</text><text>匹配解锁</text>
|
||||
<view class="card-body">
|
||||
<view class="field" wx:if="{{member.industry}}">
|
||||
<text class="f-key">行业</text>
|
||||
<text class="f-val">{{member.industry}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.position}}">
|
||||
<text class="f-key">职位</text>
|
||||
<text class="f-val">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.businessScale}}">
|
||||
<text class="f-key">业务体量</text>
|
||||
<text class="f-val">{{member.businessScale}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.industry || member.position || member.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{member.skills}}">
|
||||
<text class="f-key">我擅长</text>
|
||||
<text class="f-val">{{member.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.contactDisplay || member.contactRaw}}">
|
||||
<text class="f-key">联系方式</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.contactDisplay || '未填写'}}</text>
|
||||
<view class="icon-copy" wx:if="{{!member.contactUnlocked}}" bindtap="unlockContact">🔒</view>
|
||||
<view class="icon-copy" wx:else bindtap="copyContact">📋</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.wechatDisplay || member.wechatRaw}}">
|
||||
<text class="f-key">微信号</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.wechatDisplay || '未填写'}}</text>
|
||||
<view class="icon-copy" wx:if="{{!member.wechatUnlocked}}" bindtap="unlockContact">🔒</view>
|
||||
<view class="icon-copy" wx:else bindtap="copyWechat">📋</view>
|
||||
</view>
|
||||
<view class="copy-chip" wx:if="{{member.contactUnlocked}}" bindtap="copyContact">复制</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 个人故事 ===== -->
|
||||
<!-- 个人故事 -->
|
||||
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="card-head"><text class="card-icon">💡</text><text class="card-label">个人故事</text></view>
|
||||
<view class="story" wx:if="{{member.bestMonth}}">
|
||||
<text class="story-q">🏆 最赚钱的一个月做的是什么</text>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
<view class="card-head">
|
||||
<text class="card-icon bulb">💡</text>
|
||||
<text class="card-label">个人故事</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="story" wx:if="{{member.achievement}}">
|
||||
<text class="story-q">⭐ 最有成就感的一件事</text>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="story" wx:if="{{member.turningPoint}}">
|
||||
<text class="story-q">🔄 人生的转折点</text>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
<view class="card-body">
|
||||
<view class="story" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><text class="story-icon">🏆</text><text class="story-q">最赚钱的一个月做的是什么</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><text class="story-icon">⭐</text><text class="story-q">最有成就感的一件事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><text class="story-icon turn">🔄</text><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 互助需求 ===== -->
|
||||
<!-- 互助需求 -->
|
||||
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="card-head"><text class="card-icon">🤝</text><text class="card-label">互助需求</text></view>
|
||||
<view class="help-box help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tag">我能帮到你</text>
|
||||
<text class="help-txt">{{member.canHelp}}</text>
|
||||
<view class="card-head">
|
||||
<text class="card-icon">🤝</text>
|
||||
<text class="card-label">互助需求</text>
|
||||
</view>
|
||||
<view class="help-box help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tag need">我需要帮助</text>
|
||||
<text class="help-txt">{{member.needHelp}}</text>
|
||||
<view class="card-body">
|
||||
<view class="help-box help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tag">我能帮你</text>
|
||||
<text class="help-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-box help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tag need">我需要帮助</text>
|
||||
<text class="help-txt">{{member.needHelp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 项目介绍 ===== -->
|
||||
<!-- 项目介绍 -->
|
||||
<view class="card" wx:if="{{member.project}}">
|
||||
<view class="card-head"><text class="card-icon">🚀</text><text class="card-label">项目介绍</text></view>
|
||||
<view class="card-head">
|
||||
<text class="card-icon rocket">🚀</text>
|
||||
<text class="card-label">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-txt">{{member.project}}</text>
|
||||
</view>
|
||||
|
||||
<!-- ===== 底部操作 ===== -->
|
||||
<view class="bottom-actions">
|
||||
<view class="btn-match" bindtap="goToMatch">开始匹配 · 解锁联系方式</view>
|
||||
<view class="btn-vip" bindtap="goToVip" wx:if="{{!member.isVip}}">成为超级个体 →</view>
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-wrap">
|
||||
<view class="btn-super" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height:120rpx;"></view>
|
||||
<view style="height:160rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 加载和空状态 -->
|
||||
<view class="state-wrap" wx:if="{{loading}}">
|
||||
<view class="loading-dot"></view><text class="state-txt">加载中...</text>
|
||||
<view class="loading-dot"></view>
|
||||
<text class="state-txt">加载中...</text>
|
||||
</view>
|
||||
<view class="state-wrap" wx:if="{{!loading && !member}}">
|
||||
<text class="state-emoji">👤</text><text class="state-txt">暂无该超级个体信息</text>
|
||||
<text class="state-emoji">👤</text>
|
||||
<text class="state-txt">暂无该超级个体信息</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,75 +1,164 @@
|
||||
.page { background: #050a10; min-height: 100vh; color: #fff; }
|
||||
/* Soul创业派对 - 个人资料页(enhanced_professional_profile 1:1 还原) */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
|
||||
/* 导航 */
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; height: 44px; background: rgba(5,10,16,.92); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-bottom: 1rpx solid rgba(56,189,172,.08); }
|
||||
.back-arrow { font-size: 48rpx; color: #38bdac; font-weight: 300; }
|
||||
.nav-title { font-size: 32rpx; font-weight: 600; letter-spacing: 2rpx; }
|
||||
.nav-ph { width: 48rpx; }
|
||||
.nav-back { width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 999;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 32rpx; height: 44px;
|
||||
background: rgba(5, 11, 20, 0.9);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.nav-back { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: flex-start; }
|
||||
.nav-icon { font-size: 44rpx; color: #5EEAD4; font-weight: 300; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
|
||||
.nav-right { display: flex; align-items: center; gap: 16rpx; }
|
||||
.nav-icon-wrap { padding: 8rpx; }
|
||||
.nav-icon-dot { font-size: 28rpx; color: rgba(255,255,255,0.8); }
|
||||
|
||||
.scroll-wrap { height: calc(100vh - 88px); }
|
||||
|
||||
/* ===== 顶部名片 ===== */
|
||||
.card-hero { position: relative; margin: 24rpx 24rpx 0; padding: 56rpx 0 40rpx; border-radius: 28rpx; overflow: hidden; background: linear-gradient(160deg, #0d1f2d 0%, #0a1620 40%, #101828 100%); border: 1rpx solid rgba(56,189,172,.15); }
|
||||
.hero-deco { position: absolute; top: -60rpx; right: -60rpx; width: 240rpx; height: 240rpx; border-radius: 50%; background: radial-gradient(circle, rgba(56,189,172,.12) 0%, transparent 70%); }
|
||||
.hero-deco2 { position: absolute; bottom: -40rpx; left: -40rpx; width: 180rpx; height: 180rpx; border-radius: 50%; background: radial-gradient(circle, rgba(245,166,35,.06) 0%, transparent 70%); }
|
||||
.hero-body { position: relative; display: flex; flex-direction: column; align-items: center; z-index: 1; }
|
||||
|
||||
.avatar-ring { position: relative; width: 168rpx; height: 168rpx; border-radius: 50%; padding: 6rpx; background: linear-gradient(135deg, #1a3a3a, #0d1f2d); margin-bottom: 20rpx; }
|
||||
.avatar-ring.vip-ring { background: linear-gradient(135deg, #f5a623, #38bdac, #f5a623); background-size: 300% 300%; animation: vipGlow 4s ease infinite; }
|
||||
@keyframes vipGlow { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
|
||||
.avatar-img { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; }
|
||||
.avatar-ph { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; background: #152030; display: flex; align-items: center; justify-content: center; font-size: 52rpx; color: #38bdac; font-weight: 700; }
|
||||
.vip-tag { position: absolute; bottom: 4rpx; right: 4rpx; background: linear-gradient(135deg, #f5a623, #e8920d); color: #000; font-size: 18rpx; font-weight: 800; padding: 4rpx 14rpx; border-radius: 16rpx; letter-spacing: 1rpx; box-shadow: 0 4rpx 12rpx rgba(245,166,35,.4); }
|
||||
|
||||
.hero-name { font-size: 38rpx; font-weight: 700; letter-spacing: 2rpx; margin-bottom: 12rpx; text-shadow: 0 2rpx 8rpx rgba(0,0,0,.5); }
|
||||
.hero-tags { display: flex; gap: 12rpx; flex-wrap: wrap; justify-content: center; }
|
||||
.tag-item { font-size: 22rpx; padding: 6rpx 18rpx; border-radius: 24rpx; }
|
||||
.tag-mbti { color: #38bdac; background: rgba(56,189,172,.12); border: 1rpx solid rgba(56,189,172,.2); }
|
||||
.tag-region { color: #ccc; background: rgba(255,255,255,.06); border: 1rpx solid rgba(255,255,255,.08); }
|
||||
/* ===== 顶部 Profile 卡片 ===== */
|
||||
.card-profile {
|
||||
position: relative; margin: 32rpx 32rpx 0;
|
||||
padding: 64rpx 40rpx 48rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-deco {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(180deg, rgba(30, 58, 69, 0.3) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-body { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 176rpx; height: 176rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.avatar-wrap.vip-ring {
|
||||
border: 4rpx solid transparent;
|
||||
background: linear-gradient(135deg, #F59E0B, #5EEAD4, #F59E0B);
|
||||
background-size: 200% 200%;
|
||||
animation: vipGlow 4s ease infinite;
|
||||
}
|
||||
@keyframes vipGlow {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.avatar-ph {
|
||||
width: 100%; height: 100%;
|
||||
background: #17212F;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 56rpx; color: #5EEAD4; font-weight: 700;
|
||||
}
|
||||
.vip-tag {
|
||||
position: absolute; bottom: 4rpx; right: 4rpx;
|
||||
background: linear-gradient(135deg, #F59E0B, #e8920d);
|
||||
color: #000; font-size: 20rpx; font-weight: 800;
|
||||
padding: 6rpx 16rpx; border-radius: 20rpx;
|
||||
}
|
||||
.profile-name { font-size: 40rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; letter-spacing: 2rpx; }
|
||||
.profile-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; flex-wrap: wrap; }
|
||||
.tag { font-size: 24rpx; font-weight: 500; padding: 8rpx 24rpx; border-radius: 999rpx; }
|
||||
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94, 234, 212, 0.2); }
|
||||
.tag-region { background: #1F2937; color: #D1D5DB; border: 1rpx solid rgba(255, 255, 255, 0.1); display: flex; align-items: center; gap: 8rpx; }
|
||||
.pin-icon { color: #EF4444; font-size: 22rpx; }
|
||||
|
||||
/* ===== 通用卡片 ===== */
|
||||
.card { margin: 20rpx 24rpx; padding: 28rpx 28rpx; border-radius: 24rpx; background: rgba(15,25,40,.8); border: 1rpx solid rgba(255,255,255,.06); backdrop-filter: blur(10px); }
|
||||
.card-head { display: flex; align-items: center; gap: 10rpx; margin-bottom: 24rpx; }
|
||||
.card-icon { font-size: 28rpx; }
|
||||
.card-label { font-size: 28rpx; font-weight: 600; color: #fff; letter-spacing: 1rpx; }
|
||||
.card {
|
||||
margin: 32rpx;
|
||||
padding: 40rpx 40rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.card-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.card-icon { font-size: 40rpx; }
|
||||
.card-icon.bulb { filter: sepia(1) saturate(3) hue-rotate(15deg); }
|
||||
.card-icon.rocket { opacity: 0.9; }
|
||||
.card-label { font-size: 30rpx; font-weight: 700; color: #fff; letter-spacing: 1rpx; }
|
||||
|
||||
.field { margin-bottom: 20rpx; }
|
||||
.card-body { }
|
||||
.field { margin-bottom: 32rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.f-key { display: block; font-size: 22rpx; color: #6b7b8e; margin-bottom: 8rpx; text-transform: uppercase; letter-spacing: 2rpx; }
|
||||
.f-val { font-size: 28rpx; color: #e0e8f0; line-height: 1.7; }
|
||||
.f-contact { display: flex; align-items: center; gap: 16rpx; }
|
||||
.masked { letter-spacing: 3rpx; font-family: 'Courier New', monospace; }
|
||||
.lock-chip { display: flex; align-items: center; gap: 6rpx; font-size: 20rpx; color: #f5a623; background: rgba(245,166,35,.1); border: 1rpx solid rgba(245,166,35,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; }
|
||||
.lock-icon { font-size: 18rpx; }
|
||||
.copy-chip { font-size: 20rpx; color: #38bdac; background: rgba(56,189,172,.1); border: 1rpx solid rgba(56,189,172,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; }
|
||||
.f-key { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 12rpx; }
|
||||
.f-val { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.6; }
|
||||
.f-val.mono { font-family: ui-monospace, monospace; letter-spacing: 2rpx; }
|
||||
.f-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.icon-copy { font-size: 36rpx; color: #94A3B8; opacity: 0.6; padding: 8rpx; }
|
||||
|
||||
/* ===== 故事 ===== */
|
||||
.story { padding: 4rpx 0; }
|
||||
.story-q { display: block; font-size: 24rpx; color: #7a8fa3; margin-bottom: 10rpx; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #e0e8f0; line-height: 1.8; }
|
||||
.divider { height: 1rpx; background: rgba(255,255,255,.04); margin: 20rpx 0; }
|
||||
.divider { height: 1rpx; background: rgba(255, 255, 255, 0.05); margin: 32rpx 0; }
|
||||
|
||||
/* ===== 互助 ===== */
|
||||
.help-box { padding: 20rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
|
||||
/* ===== 个人故事 ===== */
|
||||
.story { margin-bottom: 32rpx; }
|
||||
.story:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.story-icon { font-size: 32rpx; }
|
||||
.story-icon.turn { opacity: 0.9; }
|
||||
.story-q { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 互助需求 ===== */
|
||||
.help-box {
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: #17212F;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.help-box:last-child { margin-bottom: 0; }
|
||||
.help-give { background: rgba(56,189,172,.06); border: 1rpx solid rgba(56,189,172,.1); }
|
||||
.help-need { background: rgba(245,166,35,.06); border: 1rpx solid rgba(245,166,35,.1); }
|
||||
.help-tag { display: inline-block; font-size: 22rpx; font-weight: 600; color: #38bdac; margin-bottom: 10rpx; padding: 4rpx 14rpx; border-radius: 12rpx; background: rgba(56,189,172,.12); }
|
||||
.help-tag.need { color: #f5a623; background: rgba(245,166,35,.12); }
|
||||
.help-txt { display: block; font-size: 26rpx; color: #ccd4de; line-height: 1.7; }
|
||||
.help-tag {
|
||||
display: inline-block;
|
||||
font-size: 22rpx; font-weight: 600;
|
||||
padding: 6rpx 16rpx; border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.help-give .help-tag { color: #5EEAD4; background: #112D2A; }
|
||||
.help-need .help-tag { color: #F59E0B; background: #2D1F0D; }
|
||||
.help-txt { display: block; font-size: 26rpx; color: #fff; line-height: 1.6; letter-spacing: 1rpx; }
|
||||
|
||||
/* ===== 项目 ===== */
|
||||
.proj-txt { font-size: 26rpx; color: #ccd4de; line-height: 1.8; }
|
||||
/* ===== 项目介绍 ===== */
|
||||
.proj-txt { font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 底部按钮 ===== */
|
||||
.bottom-actions { padding: 32rpx 24rpx 0; display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.btn-match { text-align: center; padding: 26rpx 0; border-radius: 48rpx; font-size: 30rpx; font-weight: 700; letter-spacing: 2rpx; background: linear-gradient(135deg, #38bdac 0%, #2ca898 50%, #249e8c 100%); color: #fff; box-shadow: 0 8rpx 24rpx rgba(56,189,172,.3); }
|
||||
.btn-vip { text-align: center; padding: 22rpx 0; border-radius: 48rpx; font-size: 26rpx; color: #f5a623; background: transparent; border: 1rpx solid rgba(245,166,35,.3); }
|
||||
.bottom-wrap {
|
||||
padding: 48rpx 32rpx 0;
|
||||
}
|
||||
.btn-super {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
width: 100%;
|
||||
padding: 32rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: transparent;
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.3);
|
||||
color: #F59E0B;
|
||||
font-size: 30rpx; font-weight: 500;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.btn-arrow { font-size: 36rpx; font-weight: 300; }
|
||||
|
||||
/* ===== 状态 ===== */
|
||||
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 16rpx; }
|
||||
.state-txt { font-size: 28rpx; color: #4a5a6e; }
|
||||
.state-emoji { font-size: 80rpx; }
|
||||
.loading-dot { width: 48rpx; height: 48rpx; border-radius: 50%; border: 4rpx solid rgba(56,189,172,.2); border-top-color: #38bdac; animation: spin 1s linear infinite; }
|
||||
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 24rpx; }
|
||||
.state-txt { font-size: 28rpx; color: #64748B; }
|
||||
.state-emoji { font-size: 96rpx; }
|
||||
.loading-dot {
|
||||
width: 56rpx; height: 56rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(94, 234, 212, 0.2);
|
||||
border-top-color: #5EEAD4;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
460
soul-admin/dist/assets/index-C0uzGclW.js
vendored
460
soul-admin/dist/assets/index-C0uzGclW.js
vendored
File diff suppressed because one or more lines are too long
465
soul-admin/dist/assets/index-CKRjkwdJ.js
vendored
Normal file
465
soul-admin/dist/assets/index-CKRjkwdJ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
soul-admin/dist/index.html
vendored
2
soul-admin/dist/index.html
vendored
@@ -4,7 +4,7 @@
|
||||
<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-C0uzGclW.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CKRjkwdJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D0nEGm4H.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/client.ts","./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/api-doc/apidocpage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/contentpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.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"],"version":"5.6.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/client.ts","./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/api-doc/apidocpage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/contentpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.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"],"version":"5.6.3"}
|
||||
@@ -1,6 +1,8 @@
|
||||
# Air 热重载配置:改 .go 后自动重新编译并重启
|
||||
# 默认使用开发/测试环境:env_files 加载 .env.development
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
env_files = [".env", ".env.development"]
|
||||
|
||||
# Windows 下用 .exe 避免系统弹出「选择应用打开 main」
|
||||
[build]
|
||||
|
||||
@@ -64,6 +64,7 @@ var defaultCORSOrigins = []string{
|
||||
"http://127.0.0.1:5174",
|
||||
"https://soul.quwanzhi.com",
|
||||
"http://soul.quwanzhi.com",
|
||||
"https://souladmin.quwanzhi.com",
|
||||
"http://souladmin.quwanzhi.com",
|
||||
}
|
||||
|
||||
@@ -95,12 +96,49 @@ func parseCORSOrigins() []string {
|
||||
return origins
|
||||
}
|
||||
|
||||
// Load 加载配置,端口等从 .env 读取。优先从可执行文件同目录加载 .env,再试当前目录
|
||||
// Load 加载配置,端口等从 .env 读取。
|
||||
// 环境区分:APP_ENV=development 加载 .env.development,APP_ENV=production 加载 .env.production;
|
||||
// air 运行时通过 env_files 或 full_bin 设置 APP_ENV,开发用 .env.development,部署用 .env.production。
|
||||
func Load() (*Config, error) {
|
||||
workDir, _ := os.Getwd()
|
||||
execDir := ""
|
||||
if execPath, err := os.Executable(); err == nil {
|
||||
_ = godotenv.Load(filepath.Join(filepath.Dir(execPath), ".env"))
|
||||
execDir = filepath.Dir(execPath)
|
||||
}
|
||||
loadEnv := func(name string) {
|
||||
for _, dir := range []string{execDir, workDir, "."} {
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
_ = godotenv.Load(p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
overloadEnv := func(name string) {
|
||||
for _, dir := range []string{execDir, workDir, "."} {
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
_ = godotenv.Overload(p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 加载 .env 作为基础
|
||||
loadEnv(".env")
|
||||
// 2. 按 APP_ENV 覆盖(优先读已设置的 APP_ENV,如 air 的 env_files 已注入)
|
||||
appEnv := strings.ToLower(strings.TrimSpace(os.Getenv("APP_ENV")))
|
||||
if appEnv == "development" {
|
||||
overloadEnv(".env.development")
|
||||
} else if appEnv == "production" {
|
||||
overloadEnv(".env.production")
|
||||
}
|
||||
_ = godotenv.Load(".env")
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
|
||||
@@ -17,11 +17,13 @@ func AdminChaptersList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
type section struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Price float64 `json:"price"`
|
||||
IsFree bool `json:"isFree"`
|
||||
Status string `json:"status"`
|
||||
ID string `json:"id"`
|
||||
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"`
|
||||
@@ -60,7 +62,10 @@ func AdminChaptersList(c *gin.Context) {
|
||||
if row.Status != nil {
|
||||
st = *row.Status
|
||||
}
|
||||
ch.Sections = append(ch.Sections, section{ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st})
|
||||
ch.Sections = append(ch.Sections, section{
|
||||
ID: row.ID, 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 {
|
||||
@@ -77,11 +82,13 @@ func AdminChaptersList(c *gin.Context) {
|
||||
// AdminChaptersAction POST/PUT/DELETE /api/admin/chapters
|
||||
func AdminChaptersAction(c *gin.Context) {
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
ID string `json:"id"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
Status *string `json:"status"`
|
||||
Action string `json:"action"`
|
||||
ID string `json:"id"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
Status *string `json:"status"`
|
||||
EditionStandard *bool `json:"editionStandard"`
|
||||
EditionPremium *bool `json:"editionPremium"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
@@ -97,5 +104,17 @@ func AdminChaptersAction(c *gin.Context) {
|
||||
if body.Action == "updateStatus" && body.ID != "" && body.Status != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("status", *body.Status)
|
||||
}
|
||||
if body.Action == "updateEdition" && body.ID != "" {
|
||||
updates := make(map[string]interface{})
|
||||
if body.EditionStandard != nil {
|
||||
updates["edition_standard"] = *body.EditionStandard
|
||||
}
|
||||
if body.EditionPremium != nil {
|
||||
updates["edition_premium"] = *body.EditionPremium
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
@@ -151,11 +151,18 @@ func BookChapters(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
|
||||
updates := map[string]interface{}{
|
||||
"part_title": body.PartTitle, "chapter_title": body.ChapterTitle, "section_title": body.SectionTitle,
|
||||
"content": body.Content, "word_count": body.WordCount, "is_free": body.IsFree, "price": body.Price,
|
||||
"sort_order": body.SortOrder, "status": body.Status,
|
||||
}).Error; err != nil {
|
||||
}
|
||||
if body.EditionStandard != nil {
|
||||
updates["edition_standard"] = body.EditionStandard
|
||||
}
|
||||
if body.EditionPremium != nil {
|
||||
updates["edition_premium"] = body.EditionPremium
|
||||
}
|
||||
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,12 +179,14 @@ func DBBookAction(c *gin.Context) {
|
||||
}
|
||||
case http.MethodPut:
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
IsNew *bool `json:"isNew"` // stitch_soul:标记最新新增
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
IsNew *bool `json:"isNew"` // stitch_soul:标记最新新增
|
||||
EditionStandard *bool `json:"editionStandard"` // 是否属于普通版
|
||||
EditionPremium *bool `json:"editionPremium"` // 是否属于增值版
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||
@@ -209,6 +211,12 @@ func DBBookAction(c *gin.Context) {
|
||||
if body.IsNew != nil {
|
||||
updates["is_new"] = *body.IsNew
|
||||
}
|
||||
if body.EditionStandard != nil {
|
||||
updates["edition_standard"] = *body.EditionStandard
|
||||
}
|
||||
if body.EditionPremium != nil {
|
||||
updates["edition_premium"] = *body.EditionPremium
|
||||
}
|
||||
err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
|
||||
@@ -757,17 +757,31 @@ func MiniprogramUsers(c *gin.Context) {
|
||||
var cnt int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
|
||||
id, "paid", "fullbook", "vip").Count(&cnt)
|
||||
// 用户信息(nickname/avatar)与会员资料(vip*)区分,字段与 model 一致(camelCase)
|
||||
// 用户信息与会员资料(vip*)、P3 资料扩展,供会员详情页完整展示
|
||||
item := gin.H{
|
||||
"id": user.ID,
|
||||
"nickname": getStringValue(user.Nickname),
|
||||
"avatar": getStringValue(user.Avatar),
|
||||
"vipName": getStringValue(user.VipName),
|
||||
"vipAvatar": getStringValue(user.VipAvatar),
|
||||
"vipContact": getStringValue(user.VipContact),
|
||||
"vipProject": getStringValue(user.VipProject),
|
||||
"vipBio": getStringValue(user.VipBio),
|
||||
"is_vip": cnt > 0,
|
||||
"id": user.ID,
|
||||
"nickname": getStringValue(user.Nickname),
|
||||
"avatar": getStringValue(user.Avatar),
|
||||
"phone": getStringValue(user.Phone),
|
||||
"wechatId": getStringValue(user.WechatID),
|
||||
"vipName": getStringValue(user.VipName),
|
||||
"vipAvatar": getStringValue(user.VipAvatar),
|
||||
"vipContact": getStringValue(user.VipContact),
|
||||
"vipProject": getStringValue(user.VipProject),
|
||||
"vipBio": getStringValue(user.VipBio),
|
||||
"mbti": getStringValue(user.Mbti),
|
||||
"region": getStringValue(user.Region),
|
||||
"industry": getStringValue(user.Industry),
|
||||
"position": getStringValue(user.Position),
|
||||
"businessScale": getStringValue(user.BusinessScale),
|
||||
"skills": getStringValue(user.Skills),
|
||||
"storyBestMonth": getStringValue(user.StoryBestMonth),
|
||||
"storyAchievement": getStringValue(user.StoryAchievement),
|
||||
"storyTurning": getStringValue(user.StoryTurning),
|
||||
"helpOffer": getStringValue(user.HelpOffer),
|
||||
"helpNeed": getStringValue(user.HelpNeed),
|
||||
"projectIntro": getStringValue(user.ProjectIntro),
|
||||
"is_vip": cnt > 0,
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": item})
|
||||
return
|
||||
|
||||
@@ -18,8 +18,11 @@ type Chapter struct {
|
||||
SortOrder *int `gorm:"column:sort_order" json:"sortOrder,omitempty"`
|
||||
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
|
||||
IsNew *bool `gorm:"column:is_new" json:"isNew,omitempty"` // stitch_soul:目录/首页「最新新增」标记
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
// 普通版/增值版:两者分开互斥,添加文章时勾选归属
|
||||
EditionStandard *bool `gorm:"column:edition_standard" json:"editionStandard,omitempty"` // 是否属于普通版
|
||||
EditionPremium *bool `gorm:"column:edition_premium" json:"editionPremium,omitempty"` // 是否属于增值版
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Chapter) TableName() string { return "chapters" }
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user