Merge branch 'yongxu-dev' into devlop
# Conflicts: # miniprogram/pages/profile-edit/profile-edit.js # miniprogram/pages/profile-edit/profile-edit.wxml # miniprogram/pages/settings/settings.js # miniprogram/utils/ruleEngine.js # soul-admin/src/pages/distribution/DistributionPage.tsx # soul-admin/src/pages/users/UsersPage.tsx # soul-api/.env.production # soul-api/.gitignore # soul-api/internal/handler/db_ckb_leads.go # soul-api/internal/handler/miniprogram.go # soul-api/internal/handler/referral.go # 开发文档/1、需求/archive/链接人与事-存客宝同步-需求规划.md # 开发文档/1、需求/archive/链接人与事-实现方案.md
This commit is contained in:
0
.cursor/agent/安全工程师/evolution/.gitkeep
Normal file
0
.cursor/agent/安全工程师/evolution/.gitkeep
Normal file
32
.cursor/agent/安全工程师/evolution/2026-03-20-挖矿与服务器Skills.md
Normal file
32
.cursor/agent/安全工程师/evolution/2026-03-20-挖矿与服务器Skills.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 挖矿病毒排查与服务器操作 Skills 创建
|
||||
|
||||
**日期**:2026-03-20
|
||||
|
||||
## 背景
|
||||
|
||||
基于 agent 记录(3b9e0fa0、bc781e1b、1c1a81c3 等)中挖矿病毒排查经验,以及本地部署脚本(devloy.py、master.py、soul-admin/deploy.py、Cunkebao/miner_guard_install.py 等),将经验吸收转化为 Skills。
|
||||
|
||||
## 新增 Skills
|
||||
|
||||
### 1. security-miner-guard
|
||||
|
||||
- **路径**:`.cursor/skills/security-miner-guard/SKILL.md`
|
||||
- **触发词**:挖矿病毒、xmrig、服务器被入侵、miner_guard、安全排查、杀挖矿
|
||||
- **内容**:挖矿病毒特征、入侵链路、排查脚本、加固建议、miner_guard 安装与检查
|
||||
|
||||
### 2. security-server-ops
|
||||
|
||||
- **路径**:`.cursor/skills/security-server-ops/SKILL.md`
|
||||
- **触发词**:部署、服务器操作、SSH、宝塔、devloy、master、Cunkebao 部署
|
||||
- **内容**:服务器索引、部署脚本索引、环境变量一览、常用操作(不含明文密码)
|
||||
|
||||
## 配置更新
|
||||
|
||||
- `paths.py`:新增 `AGENT_SECURITY`、`ROLE_TO_AGENT["安全工程师"]`
|
||||
- `老板分身-索引.mdc`:经验自动收集推断增加「挖矿/安全/服务器操作→安全工程师」
|
||||
- `soul-project-boundary.mdc`:按语义触发词增加安全工程师及对应 Skills
|
||||
|
||||
## 安全提醒
|
||||
|
||||
- Skills 中**不写入明文密码**,仅说明配置来源(环境变量、脚本 get_cfg())
|
||||
- 建议将 master.py、devloy.py 等中的默认密码迁移到环境变量
|
||||
26
.cursor/agent/安全工程师/evolution/2026-03-20-管理端部署触发词.md
Normal file
26
.cursor/agent/安全工程师/evolution/2026-03-20-管理端部署触发词.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 管理端部署触发词约定
|
||||
|
||||
**日期**:2026-03-20
|
||||
|
||||
## 场景
|
||||
|
||||
用户说「管理端帮我部署到xx环境」时,安全工程师应语义化解析 xx,直接执行对应部署脚本。
|
||||
|
||||
## 解决方案
|
||||
|
||||
- **触发词**:管理端帮我部署到xx环境(语义化,理解意图即可)
|
||||
- **脚本映射**:
|
||||
- 含「正式」「线上」「生产」→ `cd soul-admin && python master.py`(正式环境,/www/wwwroot/self/soul-admin)
|
||||
- 含「测试」「dev」→ `cd soul-admin && python deploy.py`(测试环境,/www/wwwroot/self/soul-admin-dev)
|
||||
|
||||
## soul-admin 部署脚本
|
||||
|
||||
| 脚本 | 环境 | 目标目录 | 构建命令 |
|
||||
|------|------|----------|----------|
|
||||
| master.py | 正式 | soul-admin | pnpm build |
|
||||
| deploy.py | 测试 | soul-admin-dev | pnpm run build:dev |
|
||||
|
||||
## 已升级 Skills
|
||||
|
||||
1. **security-server-ops**:何时使用表、2.2 soul-admin 脚本索引、4.4/4.5 常用操作
|
||||
2. **soul-project-boundary**:按场景触发词表新增
|
||||
18
.cursor/agent/安全工程师/evolution/2026-03-20-部署API触发词.md
Normal file
18
.cursor/agent/安全工程师/evolution/2026-03-20-部署API触发词.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 部署 API 触发词约定
|
||||
|
||||
**日期**:2026-03-20
|
||||
|
||||
## 场景
|
||||
|
||||
用户说「帮我部署api到线上」时,安全工程师应直接执行部署脚本,无需再询问或选择。
|
||||
|
||||
## 解决方案
|
||||
|
||||
- **触发词**:帮我部署api到线上
|
||||
- **动作**:直接执行 `cd soul-api && python master.py`
|
||||
- **脚本**:`soul-api/master.py`(soul-api 正式环境部署)
|
||||
|
||||
## 已升级 Skills
|
||||
|
||||
1. **security-server-ops**:何时使用表新增该触发词,明确直接执行命令
|
||||
2. **soul-project-boundary**:按场景触发词表新增,加载 security-server-ops 后执行
|
||||
9
.cursor/agent/安全工程师/evolution/索引.md
Normal file
9
.cursor/agent/安全工程师/evolution/索引.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 安全工程师 经验索引
|
||||
|
||||
> 挖矿病毒排查、服务器加固、部署与运维相关经验。
|
||||
|
||||
| 日期 | 摘要 | 文件 |
|
||||
|------|------|------|
|
||||
| 2026-03-20 | 挖矿病毒排查经验转化为 Skills;服务器操作 Skill 创建 | 2026-03-20-挖矿与服务器Skills.md |
|
||||
| 2026-03-20 | 「帮我部署api到线上」→ 执行 soul-api/master.py | 2026-03-20-部署API触发词.md |
|
||||
| 2026-03-20 | 「管理端帮我部署到xx环境」→ 语义化解析,正式→master.py,测试→deploy.py | 2026-03-20-管理端部署触发词.md |
|
||||
41
.cursor/agent/小程序开发工程师/evolution/2026-03-19-原生按钮覆盖定位.md
Normal file
41
.cursor/agent/小程序开发工程师/evolution/2026-03-19-原生按钮覆盖定位.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 原生按钮覆盖定位,避免样式干扰
|
||||
|
||||
**日期**:2026-03-19
|
||||
**场景**:小程序中需在头像、图片等区域触发 `open-type="chooseAvatar"` 等原生能力,用 `<button>` 包裹会导致原生样式(灰色矩形、边框等)影响界面。
|
||||
|
||||
**方案**:**不用 button 包裹**,改为 **button 绝对定位覆盖** 在目标区域上方。
|
||||
|
||||
## 正确结构
|
||||
|
||||
```html
|
||||
<!-- 外层:position: relative 作为定位参考 -->
|
||||
<view class="avatar-wrap">
|
||||
<!-- 实际展示内容:头像、徽章等 -->
|
||||
<view class="avatar-inner">...</view>
|
||||
<view class="vip-badge">VIP</view>
|
||||
<!-- 透明 button 覆盖在上方,同级而非包裹 -->
|
||||
<button class="avatar-overlay-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"></button>
|
||||
</view>
|
||||
```
|
||||
|
||||
```css
|
||||
.avatar-wrap { position: relative; }
|
||||
.avatar-overlay-btn {
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 130rpx; height: 130rpx; /* 与头像一致 */
|
||||
padding: 0; margin: 0;
|
||||
background: transparent; border: none;
|
||||
}
|
||||
.avatar-overlay-btn::after { border: none; }
|
||||
```
|
||||
|
||||
## 要点
|
||||
|
||||
- **同级关系**:button 与展示元素是 sibling,不是 parent-child
|
||||
- **绝对定位**:`position: absolute` 覆盖在目标区域上,不参与文档流
|
||||
- **透明无内容**:button 仅负责点击事件,样式完全透明
|
||||
- **适用**:chooseAvatar、open-type 等需 button 触发的原生能力
|
||||
|
||||
## 升级 Skill
|
||||
|
||||
已写入 `miniprogram-dev` SKILL §12。
|
||||
39
.cursor/agent/小程序开发工程师/evolution/2026-03-20-手机号登录与公用组件.md
Normal file
39
.cursor/agent/小程序开发工程师/evolution/2026-03-20-手机号登录与公用组件.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 手机号一键登录与登录弹窗公用组件
|
||||
|
||||
> 日期:2026-03-20 | 角色:小程序开发工程师
|
||||
|
||||
## 问题与场景
|
||||
|
||||
1. **getPhoneNumber 不弹窗**:点击手机号登录按钮时,无法弹出手机号选择界面,也无法获取手机号
|
||||
2. **登录弹窗重复**:read、my、gift-pay/detail 三处各自维护一套登录弹窗,逻辑重复、维护成本高
|
||||
3. **登录后手机号未同步**:手机号登录后若响应中 user.phone 为空,本地 userInfo 未更新
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. getPhoneNumber 必须与隐私协议耦合
|
||||
|
||||
- **open-type**:`open-type="getPhoneNumber|agreePrivacyAuthorization"`(基础库 2.32.3+)
|
||||
- **onNeedPrivacyAuthorization**:app.js 中需将使用 getPhoneNumber 的页面加入支持列表,否则会 `resolve({ event: 'disagree' })` 导致获取失败
|
||||
- **支持页面**:avatar-nickname、profile-edit、read、my、gift-pay/detail、index、settings
|
||||
- **隐私弹窗**:当 onNeedPrivacyAuthorization 触发时,页面需有 `showPrivacyModal` + `<button open-type="agreePrivacyAuthorization">` 供用户同意
|
||||
|
||||
### 2. 登录弹窗公用组件
|
||||
|
||||
- **组件路径**:`components/login-modal/`
|
||||
- **使用方式**:`<login-modal show="{{showLoginModal}}" desc="..." showPrivacyModal="{{showPrivacyModal}}" showCancel="{{true}}" bind:close="..." bind:success="..." bind:privacyagree="..." />`
|
||||
- **适用页面**:read、my、gift-pay/detail
|
||||
- **app.json**:全局注册 `"login-modal": "/components/login-modal/login-modal"`
|
||||
|
||||
### 3. 登录后手机号同步
|
||||
|
||||
- **loginWithPhone 后**:若 `user.phone` 为空,调用 `_syncPhoneFromProfileAfterLogin(userId)` 从 profile 拉取最新
|
||||
- **checkLoginStatus 恢复缓存时**:若 `userInfo.phone` 为空,调用 `_refreshUserInfoIfPhoneEmpty()` 静默刷新
|
||||
|
||||
## 管理后台配置
|
||||
|
||||
- **隐私保护指引**:小程序管理后台 → 设置 → 用户隐私保护指引,必须声明「收集你选择的手机号」
|
||||
- **主体限制**:个人主体小程序无法使用 getPhoneNumber
|
||||
|
||||
## 升级 Skill
|
||||
|
||||
- miniprogram-dev SKILL §8 补充 getPhoneNumber 耦合与 onNeedPrivacyAuthorization 页面支持
|
||||
@@ -13,3 +13,5 @@
|
||||
| 2026-03-14 | 我的页设置入口隐藏;资料修改引导场景梳理(登录后、@某人、找伙伴、链接卡若) | [2026-03-14.md](./2026-03-14.md) |
|
||||
| 2026-03-16 | 编辑资料页分享名片:转发/朋友圈特殊处理,Canvas 绘制封面,标题「昵称+为您分享名片」 | [2026-03-16.md](./2026-03-16.md) |
|
||||
| 2026-03-17 | 代付美团式:读页→代付页→分享;详情页双态(发起人/好友);目录 loading、最新新增 5 条折叠 | [2026-03-17.md](./2026-03-17.md) |
|
||||
| 2026-03-19 | 原生按钮覆盖定位:chooseAvatar 等用绝对定位 overlay 覆盖,禁止 button 包裹,避免原生样式影响 | [2026-03-19-原生按钮覆盖定位.md](./2026-03-19-原生按钮覆盖定位.md) |
|
||||
| 2026-03-20 | 手机号一键登录与公用组件:getPhoneNumber 耦合 agreePrivacyAuthorization;login-modal 组件;登录后手机号同步 | [2026-03-20-手机号登录与公用组件.md](./2026-03-20-手机号登录与公用组件.md) |
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
| 2026-03-17 | 后端、团队 | 架构/最佳实践 | api-dev SKILL | Redis 缓存:parts/hot/recommended/stats/config/章节 content;容灾回退 DB;OSS 上传;/health 返回 database/redis 状态 |
|
||||
| 2026-03-18 | 小程序、团队 | 业务规则/最佳实践 | - | 分享链路兼容好友/朋友圈 singlePage:单页模式能力降级(不支付/不自动领取),引导点击底部“前往小程序”进入完整版 |
|
||||
| 2026-03-18 | 产品、后端、管理端、测试 | 文档归档/需求口径 | - | 文档归档整理:以《以界面定需求》为基准,各角色重整“功能需求+验收口径+风险点”并写入各自经验库;补齐《项目落地推进表》 |
|
||||
| 2026-03-19 | 小程序 | 最佳实践 | miniprogram-dev SKILL §11 | 原生按钮覆盖定位:chooseAvatar 等用绝对定位 overlay 覆盖,禁止 button 包裹,避免原生样式影响(灰色矩形等) |
|
||||
| 2026-03-20 | 安全工程师 | 触发词约定 | security-server-ops、soul-project-boundary | 「帮我部署api到线上」→ 直接执行 soul-api/master.py |
|
||||
| 2026-03-20 | 安全工程师 | 触发词约定 | security-server-ops、soul-project-boundary | 「管理端帮我部署到xx环境」→ 语义化解析:正式→master.py,测试→deploy.py |
|
||||
| 2026-03-20 | 小程序 | 最佳实践 | miniprogram-dev SKILL §8 | 手机号登录:getPhoneNumber 需耦合 agreePrivacyAuthorization;onNeedPrivacyAuthorization 支持页面;登录弹窗公用组件 login-modal |
|
||||
|
||||
---
|
||||
|
||||
@@ -63,4 +67,4 @@
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-18
|
||||
**最后更新**:2026-03-20
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 项目总结
|
||||
|
||||
Soul 创业派对产品定位:面向创业者的社区/工具型小程序。核心需求文档在 `开发文档/1、需求/需求汇总.md`,项目推进表在 `开发文档/10、项目管理/项目落地推进表.md`,临时需求/分析在 `临时需求池/`。
|
||||
Soul 创业派对产品定位:面向创业者的社区/工具型小程序。核心需求文档在 `开发文档/1、需求/`(按日期命名,以最新为主;见 `1、需求/索引.md`),项目推进表在 `开发文档/10、项目管理/项目落地推进表.md`,临时需求/分析在 `临时需求池/`。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -31,9 +31,11 @@ Soul 创业派对全项目架构与约定:路由隔离(miniprogram/admin/db
|
||||
| 2026-03-17 | 性能优化与 Redis 缓存方案落地:Redis 容灾回退 DB、OSS 上传容灾;/health 返回 database/redis 状态 | 已完成 |
|
||||
| 2026-03-18 | 吸收经验:分享进入链路需兼容朋友圈 singlePage;单页模式不执行支付/自动领取等强动作并引导“前往小程序” | 已完成 |
|
||||
| 2026-03-18 | 会议:超级个体开通后自动创建@人统一走 Person;幂等键绑定 userId;默认资料 flags 后端输出 | 已完成 |
|
||||
| 2026-03-20 | 「帮我部署api到线上」→ 安全工程师执行 soul-api/master.py;security-server-ops、soul-project-boundary 触发词升级 | 已完成 |
|
||||
| 2026-03-20 | 「管理端帮我部署到xx环境」→ 语义化解析:正式/线上/生产→master.py,测试/dev→deploy.py;soul-admin 部署脚本索引 | 已完成 |
|
||||
|
||||
> **格式说明**:每次架构级讨论后在此追加一行,日期格式 YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-18
|
||||
**最后更新**:2026-03-20
|
||||
|
||||
@@ -41,9 +41,11 @@
|
||||
| 2026-03-17 | 会议收尾:源码优化 5 项全部完成;开发环境测试通过 | 已完成 |
|
||||
| 2026-03-18 | 吸收经验:分享链路需兼容好友/朋友圈 singlePage;单页模式能力降级并引导“前往小程序”进入完整版 | 已完成 |
|
||||
| 2026-03-18 | 会议:支付超级个体前/开通后资料默认校验,跳转 avatar-nickname 引导页(仅头像+昵称) | 已完成 |
|
||||
| 2026-03-19 | 吸收经验:原生按钮覆盖定位,chooseAvatar 用绝对定位 overlay 覆盖头像,禁止 button 包裹,已升级 SKILL §11 | 已完成 |
|
||||
| 2026-03-20 | 手机号一键登录:getPhoneNumber 耦合 agreePrivacyAuthorization;onNeedPrivacyAuthorization 支持 read/my/gift-pay/index/settings;登录弹窗公用组件 components/login-modal;登录后手机号同步 _syncPhoneFromProfileAfterLogin | 已完成 |
|
||||
|
||||
> **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-03-18
|
||||
**最后更新**:2026-03-20
|
||||
|
||||
@@ -53,6 +53,7 @@ AGENT_BACKEND = AGENT / "后端工程师"
|
||||
AGENT_PRODUCT = AGENT / "产品经理"
|
||||
AGENT_TEST = AGENT / "软件测试"
|
||||
AGENT_TEAM = AGENT / "团队"
|
||||
AGENT_SECURITY = AGENT / "安全工程师"
|
||||
|
||||
# ========== 常用文件 ==========
|
||||
RULE_MAIN = RULES / "老板分身-索引.mdc"
|
||||
@@ -80,6 +81,9 @@ ROLE_TO_AGENT = {
|
||||
"软件测试": "软件测试",
|
||||
"测试": "软件测试",
|
||||
"测试人员": "软件测试",
|
||||
# 安全
|
||||
"安全工程师": "安全工程师",
|
||||
"安全": "安全工程师",
|
||||
# 通用
|
||||
"团队": "团队",
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ alwaysApply: false
|
||||
|
||||
### 行为摘要(供模型快速理解,完整流程以 SKILL 文件为准)
|
||||
|
||||
1. **文档同步**:从对话中提炼结论/待办/变更 → 写入 `开发文档/1、需求/需求汇总.md`、`开发文档/10、项目管理/运营与变更.md`、`临时需求池/` 等对应文档
|
||||
1. **文档同步**:从对话中提炼结论/待办/变更 → 写入 `开发文档/1、需求/YYYY-MM-DD-需求.md`(当日文件,以日期最新为主)、`开发文档/10、项目管理/运营与变更.md`、`临时需求池/` 等对应文档
|
||||
2. **经验入库**:提炼经验 → 写入 `agent/{角色}/evolution/YYYY-MM-DD.md` → 更新 `agent/开发助理/项目索引/{索引名}.md`(写日期)→ 更新 `agent/开发助理/经验清单.md` → 升级对应 SKILL
|
||||
|
||||
@@ -20,6 +20,13 @@ alwaysApply: true
|
||||
| 预览/参考 | next-project/ | 仅预览,非线上 | 不依赖 |
|
||||
| **新版管理端** | **new-soul/soul-admin/** | 新版参考实现,迁移时对照 | soul-api |
|
||||
|
||||
## 需求目录与命名约定
|
||||
|
||||
- **需求目录**:`开发文档/1、需求/`
|
||||
- **命名**:`YYYY-MM-DD-需求.md` 或 `YYYY-MM-DD-简短描述.md`
|
||||
- **主需求**:以日期最新的需求文件为主;同步需求时新建/更新当日文件,并更新 `1、需求/索引.md`
|
||||
- **基准**:`以界面定需求.md` 为界面级需求基准,新增/改版界面或业务规则时先更新该文档
|
||||
|
||||
## 核心原则
|
||||
|
||||
- 小程序只调 `/api/miniprogram/*`;管理端只调 `/api/admin/*`、`/api/db/*`;禁止混用。
|
||||
@@ -50,6 +57,7 @@ alwaysApply: true
|
||||
| 小程序、miniprogram、C 端、微信小程序 | 小程序开发工程师 | `e:\Gongsi\Mycontent\.cursor\skills\miniprogram-dev\SKILL.md` |
|
||||
| 产品、需求、验收、排期、需求文档 | 产品经理 | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md` |
|
||||
| 测试、测试用例、回归测试、功能测试、QA | 测试人员 | `e:\Gongsi\Mycontent\.cursor\skills\testing\SKILL.md` |
|
||||
| 挖矿、安全、服务器操作、部署、miner_guard、xmrig、入侵排查 | 安全工程师 | `e:\Gongsi\Mycontent\.cursor\skills\security-miner-guard\SKILL.md`、`e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md` |
|
||||
|
||||
### 按场景触发词
|
||||
|
||||
@@ -63,5 +71,7 @@ alwaysApply: true
|
||||
| 会议结束、散会、会开完了 | `e:\Gongsi\Mycontent\.cursor\skills\assistant-doc-sync\SKILL.md`(会议收尾) |
|
||||
| **加个需求**、加个需求:xxx | `e:\Gongsi\Mycontent\.cursor\skills\product-manager\SKILL.md`(产品经理三端分析 → 功能规划 → 指派) |
|
||||
| **新版分析**、版本对比、迁移分析、甲方代码分析、快速分析新版、抽取需求 | `e:\Gongsi\Mycontent\.cursor\skills\new-version-analyze\SKILL.md`(新版快速分析 → 差异清单 → 接口冲突 → 迁移迭代) |
|
||||
| **帮我部署api到线上** | `e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md`(安全工程师 → 执行 soul-api/master.py) |
|
||||
| **管理端帮我部署到xx环境** | `e:\Gongsi\Mycontent\.cursor\skills\security-server-ops\SKILL.md`(安全工程师 → 语义化解析 xx:正式/线上/生产→master.py,测试/dev→deploy.py) |
|
||||
|
||||
**注意**:「必须 Read」= 使用 Read 工具读取**绝对路径**的完整文件内容后执行,不可跳过或仅凭记忆。
|
||||
|
||||
@@ -39,6 +39,7 @@ alwaysApply: true
|
||||
- 产品/需求/config→**产品经理**
|
||||
- 测试/自检/QA→**软件测试**
|
||||
- 架构/选型/路由约定/三端协同→**团队**
|
||||
- 挖矿/安全/服务器操作/部署/入侵排查→**安全工程师**
|
||||
- 无法判断→**通用**(写入开发助理)
|
||||
3. **若可写文件**:
|
||||
- **有明确目标角色**:写入 `.cursor/agent/{角色}/evolution/YYYY-MM-DD-简短描述.md`,并更新该目录下的 `索引.md`
|
||||
|
||||
@@ -30,7 +30,7 @@ description: 开发团队文档同步与经验升级。小橙、橙子、讨论
|
||||
|
||||
| 要点类型 | 写入位置 | 示例 |
|
||||
|----------|----------|------|
|
||||
| 需求清单项 | `开发文档/1、需求/需求汇总.md` 需求清单表 | 会员分润差异化、VIP 手动设置 |
|
||||
| 需求清单项 | `开发文档/1、需求/YYYY-MM-DD-需求.md`(当日文件,以日期最新为主) | 会员分润差异化、VIP 手动设置;同步后更新 `1、需求/索引.md` |
|
||||
| 近期讨论 | `开发文档/10、项目管理/运营与变更.md` | 第五部分或新增节 |
|
||||
| 技术分析 | `临时需求池/` 或 `开发文档/8、部署/` | 分润需求-技术分析.md |
|
||||
| 项目推进 | `开发文档/10、项目管理/项目落地推进表.md` | 第十二节永平落地表 |
|
||||
@@ -83,6 +83,7 @@ description: 开发团队文档同步与经验升级。小橙、橙子、讨论
|
||||
| 后端开发 | `agent/后端工程师/evolution/` | `agent/开发助理/项目索引/后端.md` |
|
||||
| 产品经理 | `agent/产品经理/evolution/` | `agent/开发助理/项目索引/产品.md` |
|
||||
| 测试人员 | `agent/软件测试/evolution/` | `agent/开发助理/项目索引/测试.md` |
|
||||
| 安全工程师 | `agent/安全工程师/evolution/` | `agent/开发助理/项目索引/团队.md` |
|
||||
| 助理橙子 | `agent/开发助理/evolution/` | `agent/开发助理/项目索引/助理橙子.md` |
|
||||
| 跨角色/团队 | `agent/团队/evolution/` | `agent/开发助理/项目索引/团队.md` |
|
||||
|
||||
@@ -129,7 +130,10 @@ description: 开发团队文档同步与经验升级。小橙、橙子、讨论
|
||||
|
||||
```
|
||||
开发文档/
|
||||
├── 1、需求/需求汇总.md # 需求清单、业务需求
|
||||
├── 1、需求/
|
||||
│ ├── 索引.md # 主需求 = 日期最新的需求文件
|
||||
│ ├── YYYY-MM-DD-需求.md # 需求文件按日期命名,以最新为主
|
||||
│ └── 以界面定需求.md # 界面级需求基准
|
||||
├── 8、部署/ # 技术方案、部署说明
|
||||
├── 10、项目管理/
|
||||
│ ├── 项目落地推进表.md # 里程碑、永平落地
|
||||
@@ -152,7 +156,7 @@ description: 开发团队文档同步与经验升级。小橙、橙子、讨论
|
||||
**小橙**执行:
|
||||
|
||||
1. 提炼:VIP 手动设置已完成;会员分润差异化待实现;好友优惠仅针对文章
|
||||
2. 更新 `需求汇总.md`:新增「需求清单」行
|
||||
2. 更新 `1、需求/YYYY-MM-DD-需求.md`(当日文件):新增「需求清单」行;同步后更新 `1、需求/索引.md`
|
||||
3. 更新 `运营与变更.md`:第五部分追加近期讨论
|
||||
4. 回复:已记录并更新开发文档,详见 xxx
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
## 8. 平台合规与能力检测(2025 起)
|
||||
|
||||
- **隐私按需授权**:涉及用户信息的接口(登录、手机号、位置等)必须在用户**实际触发功能时**再请求授权,禁止在 `app.onLaunch` 中集中请求。需配置《小程序用户隐私保护指引》,使用 `<button open-type="agreePrivacyAuthorization">` 获取同意。
|
||||
- **getPhoneNumber 必须耦合**:`open-type="getPhoneNumber|agreePrivacyAuthorization"`(基础库 2.32.3+),否则无法弹窗获取手机号。同时需在 app.js 的 `onNeedPrivacyAuthorization` 中将该页面加入支持列表,并提供 `showPrivacyModal` + 同意按钮,否则会 resolve(disagree) 导致失败。
|
||||
- **登录弹窗**:使用公用组件 `components/login-modal`,read/my/gift-pay 等页面引入,避免重复实现。
|
||||
- **能力检测**:使用新 API 前用 `wx.canIUse('api.xxx')` 检测,低版本做降级。
|
||||
- **Skyline(可选)**:性能敏感页面可在 `page.json` 中配置 `"renderer": "skyline"`,仍使用 WXML/WXSS。
|
||||
|
||||
@@ -104,7 +106,24 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
|
||||
---
|
||||
|
||||
## 11. 何时使用本 Skill
|
||||
## 11. 原生按钮覆盖定位(避免样式干扰)
|
||||
|
||||
- **场景**:在头像、图片等区域需触发 `open-type="chooseAvatar"` 等原生能力时,**禁止用 button 包裹**目标元素,否则会受原生样式影响(灰色矩形、边框等)。
|
||||
- **正确做法**:用 **button 绝对定位覆盖** 在目标区域上方,与展示元素为同级关系。
|
||||
- **结构示例**:
|
||||
```html
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">...</view>
|
||||
<view class="vip-badge">VIP</view>
|
||||
<button class="avatar-overlay-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"></button>
|
||||
</view>
|
||||
```
|
||||
- **样式**:`.avatar-wrap { position: relative; }`;`.avatar-overlay-btn { position: absolute; top: 0; left: 0; width/height 与目标一致; background: transparent; border: none; }`;`::after { border: none; }`。
|
||||
- **口诀**:同级覆盖,绝对定位,透明按钮。
|
||||
|
||||
---
|
||||
|
||||
## 12. 何时使用本 Skill
|
||||
|
||||
- 在 **miniprogram/** 下新增或修改页面、组件、utils 时。
|
||||
- 在小程序内新增或修改任何网络请求路径时(必须保持 `/api/miniprogram/...`)。
|
||||
@@ -114,5 +133,6 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑
|
||||
- 做个人中心、设置页布局时(遵循 §7,卡片区边距 16rpx)。
|
||||
- 做阅读、文章等需长按复制的文本时(遵循 §9,text 加 user-select)。
|
||||
- 做编辑资料页分享名片时(遵循 §10)。
|
||||
- 做头像上传、chooseAvatar 等需 button 触发的原生能力时(遵循 §11,用绝对定位覆盖,禁止 button 包裹)。
|
||||
|
||||
遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。
|
||||
|
||||
@@ -192,10 +192,10 @@ description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,
|
||||
2. 排除:技术债、规则不清、与稳定版冲突的部分
|
||||
3. 按**最小功能**拆分,保证每个任务迁移后能完整运行
|
||||
4. 排期顺序:**界面修改优先** → 大逻辑排后;P0(逻辑不通)→ P1(功能缺失)→ P2(优化)
|
||||
5. 写入需求汇总,形成迁移任务清单
|
||||
5. 写入需求清单(当日需求文件),形成迁移任务清单
|
||||
|
||||
**产出**:
|
||||
- `开发文档/1、需求/需求汇总.md` 追加需求
|
||||
- `开发文档/1、需求/YYYY-MM-DD-需求.md` 追加需求(当日文件,以日期最新为主;同步后更新 `1、需求/索引.md`)
|
||||
- `开发文档/新版迁移-开发方案与清单.md` 或等价迁移清单
|
||||
|
||||
---
|
||||
@@ -259,7 +259,7 @@ description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,
|
||||
4. **逻辑分层**:每个功能过三层(界面/接口/数据)
|
||||
5. **体验评估**:补充空态、错误态、边界处理
|
||||
6. **接口规范与冲突**:产出接口规范与冲突清单
|
||||
7. **抽取需求**:写入需求汇总,形成迁移任务清单
|
||||
7. **抽取需求**:写入需求清单(当日需求文件),形成迁移任务清单
|
||||
8. **需求评审(迁移前必做)**:列出功能点 + 样式变更,逐一确认,产出评审清单
|
||||
9. **回复用户**:给出分析摘要 + 文档路径 + 建议执行顺序;**迁移须在需求评审通过后开始**
|
||||
|
||||
@@ -273,7 +273,7 @@ description: 新版快速分析 Skill。甲方/第三方 AI 写的新版本,
|
||||
| 接口规范与冲突 | `开发文档/新版迁移-接口规范与冲突清单.md` |
|
||||
| 迁移方案/清单 | `开发文档/新版迁移-开发方案与清单.md` 或 `新版功能迁移到稳定版方案.md` |
|
||||
| **需求评审清单** | `开发文档/新版迁移-需求评审清单.md`(功能点 + 样式变更,含确认状态) |
|
||||
| 需求汇总 | `开发文档/1、需求/需求汇总.md` |
|
||||
| 需求清单 | `开发文档/1、需求/YYYY-MM-DD-需求.md`(以日期最新为主) |
|
||||
|
||||
若已有同名文档,在其基础上**追加或更新**,不重复创建。
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
|
||||
### 0.3 功能规划与协调变更
|
||||
|
||||
1. **输出需求分析**:写入 `临时需求池/YYYY-MM-DD-需求简述.md` 或追加到 `需求汇总.md` 需求清单
|
||||
1. **输出需求分析**:写入 `临时需求池/YYYY-MM-DD-需求简述.md` 或追加到 `开发文档/1、需求/YYYY-MM-DD-需求.md` 需求清单(以日期最新为主,同步后更新 `1、需求/索引.md`)
|
||||
2. **三端任务拆分**:按上表列出「小程序任务」「管理端任务」「后端任务」
|
||||
3. **协调变更**:若需更新《以界面定需求》,同步更新界面清单与业务逻辑
|
||||
4. **指派**:明确各任务对应角色(小程序开发工程师、管理端开发工程师、后端工程师),并给出执行顺序建议(通常:后端 → 小程序;管理端视依赖可并行或后置)
|
||||
@@ -73,7 +73,7 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
| 职责 | 说明 | 产出 |
|
||||
|------|------|------|
|
||||
| 需求分析 | 业务需求拆解、优先级、技术可行性 | 需求分析文档、临时需求池 |
|
||||
| 需求文档 | 需求清单、业务规则、验收标准 | 需求汇总.md、运营与变更.md |
|
||||
| 需求文档 | 需求清单、业务规则、验收标准 | 1、需求/YYYY-MM-DD-需求.md(以最新为主)、运营与变更.md |
|
||||
| 验收 | 功能验收、回归检查 | 验收清单、项目推进表 |
|
||||
| 协调 | 与开发沟通、排期、变更 | 运营与变更、项目落地推进表 |
|
||||
|
||||
@@ -83,7 +83,7 @@ description: Soul 创业派对产品经理需求与验收。需求分析、需
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| `开发文档/1、需求/需求汇总.md` | 需求清单、业务需求 |
|
||||
| `开发文档/1、需求/` | 需求清单(按日期命名,以最新为主)、业务需求;见 `1、需求/索引.md` |
|
||||
| `临时需求池/` | 需求分析、技术分析 |
|
||||
| `开发文档/10、项目管理/项目落地推进表.md` | 里程碑、永平落地 |
|
||||
| `开发文档/10、项目管理/运营与变更.md` | 近期讨论、变更记录 |
|
||||
|
||||
112
.cursor/skills/security-miner-guard/SKILL.md
Normal file
112
.cursor/skills/security-miner-guard/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
description: Soul 挖矿病毒排查与防护。xmrig、kdevtmpfsi、kinsing、minerd、miner_guard。Use when 挖矿病毒、xmrig、服务器被入侵、miner_guard、安全排查、杀挖矿.
|
||||
---
|
||||
# SKILL - 挖矿病毒排查与防护(安全工程师)
|
||||
|
||||
> 基于 Soul 项目历史排查经验沉淀,用于快速识别、清理挖矿病毒并加固服务器。
|
||||
|
||||
## 何时使用
|
||||
|
||||
| 触发词 | 动作 |
|
||||
|--------|------|
|
||||
| 挖矿病毒、xmrig、服务器被入侵 | 执行排查与清理流程 |
|
||||
| miner_guard、安装挖矿守护 | 安装/检查 miner_guard |
|
||||
| 安全排查、杀挖矿 | 按本 Skill 执行 |
|
||||
|
||||
---
|
||||
|
||||
## 一、挖矿病毒特征(Soul 项目实测)
|
||||
|
||||
### 1.1 进程/路径关键词
|
||||
|
||||
| 关键词 | 说明 |
|
||||
|--------|------|
|
||||
| xmrig | 门罗币挖矿程序,最常见 |
|
||||
| xmr-stak, minerd, cpuminer | 其他 CPU 挖矿 |
|
||||
| kdevtmpfsi, kinsing | Linux 常见挖矿木马 |
|
||||
| stratum | 矿池协议 |
|
||||
| libprocesshider, watchbog, ddgs, trace | 隐藏/持久化相关 |
|
||||
|
||||
### 1.2 常见路径
|
||||
|
||||
- `/tmp/xmrig`、`/tmp/config.json`、`/tmp/.x`
|
||||
- `/tmp/kdevtmpfsi`、`/tmp/kinsing`
|
||||
- `/www/wwwroot/**/xmrig*`(宝塔站点目录下残留)
|
||||
- `/www/wwwroot/self/wanzhi/tongzhi/xmrig-*`(历史发现)
|
||||
|
||||
### 1.3 入侵链路(Soul 项目根因分析)
|
||||
|
||||
```
|
||||
公网访问宝塔 9988 → 弱口令/漏洞 → 进入面板 → www 用户执行命令
|
||||
→ 下载 xmrig 到 /tmp → 运行挖矿
|
||||
```
|
||||
|
||||
**最可能入口**:宝塔面板 9988 对公网开放 + 弱口令/历史漏洞。
|
||||
|
||||
---
|
||||
|
||||
## 二、排查与清理脚本(soul-api 目录)
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `miner_guard_check.py` | 检查 miner_guard 安装状态,手动执行一次脚本,查看日志 |
|
||||
| `miner_guard_install.py` | 安装挖矿守护到服务器(上传 miner_guard.sh + 配置 cron/systemd) |
|
||||
| `miner_guard.sh` | 守护脚本本体(杀进程、删文件、扫 /tmp、/www/wwwroot) |
|
||||
| `remove_xmrig_self.py` | 删除固定路径 `/www/wwwroot/self/wanzhi/tongzhi/xmrig-6.24.0` |
|
||||
|
||||
### 2.1 快速检查(本地执行)
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python miner_guard_check.py
|
||||
```
|
||||
|
||||
依赖:`pip install paramiko`。配置来源:`master.py` 的 `get_cfg()` 或环境变量 `DEPLOY_HOST`、`DEPLOY_PASSWORD` 等。
|
||||
|
||||
### 2.2 安装挖矿守护
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python miner_guard_install.py --yes
|
||||
```
|
||||
|
||||
- 上传 `miner_guard.sh` 到 `/root/miner_guard.sh`
|
||||
- 优先写入 `/etc/cron.d/miner_guard`(每 30 分钟),失败则尝试 crontab 或 systemd timer
|
||||
- 日志:`/var/log/miner_guard.log`
|
||||
|
||||
### 2.3 Cunkebao 服务器
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\Cunkebao
|
||||
python miner_guard_install.py --yes
|
||||
```
|
||||
|
||||
使用 Cunkebao 内嵌配置(非 root 用户,日志在 `~/miner_guard.log`)。
|
||||
|
||||
---
|
||||
|
||||
## 三、加固建议(防止再次入侵)
|
||||
|
||||
| 优先级 | 措施 |
|
||||
|--------|------|
|
||||
| 1 | 宝塔面板:9988 仅允许指定 IP 访问;改非常规端口(如 29988);强密码 |
|
||||
| 2 | 修改所有密码:root、宝塔、宝塔 API、MySQL、Redis |
|
||||
| 3 | Redis:`bind 127.0.0.1`,设置 `requirepass` |
|
||||
| 4 | 敏感信息:密码/API Key 用环境变量,不提交到 Git |
|
||||
|
||||
---
|
||||
|
||||
## 四、miner_guard.sh 行为摘要
|
||||
|
||||
1. **杀进程**:`pgrep -f xmrig|kdevtmpfsi|kinsing|...` → `kill -9`
|
||||
2. **删已知路径**:`/tmp/xmrig`、`/tmp/config.json` 等
|
||||
3. **扫 /tmp、/var/tmp、/dev/shm**:含挖矿关键词的可执行文件 → `rm -f`
|
||||
4. **扫 /www/wwwroot**:含 xmrig 的目录/文件 → `rm -rf` / `rm -f`
|
||||
5. **检查 www 用户 crontab**:可疑项仅提示,不自动删
|
||||
|
||||
---
|
||||
|
||||
## 五、注意事项
|
||||
|
||||
- 脚本会按关键词删除,可能与业务目录重叠,部署前确认扫描范围
|
||||
- `master.py`、`devloy.py` 等含默认密码,应改为环境变量并确保 `.env` 不入库
|
||||
147
.cursor/skills/security-server-ops/SKILL.md
Normal file
147
.cursor/skills/security-server-ops/SKILL.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
description: Soul 服务器操作与部署。部署脚本、SSH、宝塔、环境变量。Use when 部署、服务器操作、SSH、宝塔、devloy、master、Cunkebao 部署.
|
||||
---
|
||||
# SKILL - 服务器操作与部署(安全工程师)
|
||||
|
||||
> Soul 项目部署脚本索引与服务器操作规范。**密码等敏感信息仅通过环境变量或脚本内 get_cfg() 读取,不在此文档明文列出。**
|
||||
|
||||
## 何时使用
|
||||
|
||||
| 触发词 | 动作 |
|
||||
|--------|------|
|
||||
| **帮我部署api到线上** | **直接执行 `cd soul-api && python master.py`** |
|
||||
| **管理端帮我部署到xx环境** | **语义化解析 xx,直接执行**:含「正式」「线上」「生产」→ `cd soul-admin && python master.py`;含「测试」「dev」→ `cd soul-admin && python deploy.py` |
|
||||
| 部署、服务器操作、SSH | 按本 Skill 选择对应脚本 |
|
||||
| 宝塔、devloy、master | 查阅部署配置与命令 |
|
||||
| Cunkebao 部署 | 使用 Cunkebao 专用脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 一、服务器与配置来源
|
||||
|
||||
### 1.1 配置读取优先级
|
||||
|
||||
各部署脚本统一约定:
|
||||
|
||||
1. **环境变量**(推荐):`DEPLOY_HOST`、`DEPLOY_USER`、`DEPLOY_PASSWORD`、`DEPLOY_SSH_PORT`、`BT_API_KEY` 等
|
||||
2. **脚本内 get_cfg()**:无环境变量时使用脚本默认值
|
||||
3. **master.py**:soul-api 的 miner_guard_check、remove_xmrig_self 等从 `soul-api/master.py` 的 `get_cfg()` 读取
|
||||
|
||||
### 1.2 服务器索引(配置来源,不含明文密码)
|
||||
|
||||
| 项目 | 主机 | 端口 | 用户 | 配置来源 |
|
||||
|------|------|------|------|----------|
|
||||
| soul-api 正式 | 43.139.27.93 | 22022 | root | master.py / 环境变量 |
|
||||
| soul-api 测试 | 43.139.27.93 | 22022 | root | devloy.py / 环境变量 |
|
||||
| soul-admin 正式 | 43.139.27.93 | 22022 | root | soul-admin/master.py / 环境变量 |
|
||||
| soul-admin 测试 | 43.139.27.93 | 22022 | root | soul-admin/deploy.py / 环境变量 |
|
||||
| Cunkebao | 42.194.245.239 | 6523 | yongpxu | Cunkebao/miner_guard_install.py 内嵌 |
|
||||
|
||||
**密码**:从 `DEPLOY_PASSWORD` 或各脚本 `get_cfg()` 默认值读取,**不在此文档记录**。
|
||||
|
||||
---
|
||||
|
||||
## 二、部署脚本索引
|
||||
|
||||
### 2.1 soul-api
|
||||
|
||||
| 脚本 | 用途 | 命令示例 |
|
||||
|------|------|----------|
|
||||
| `soul-api/devloy.py` | 测试环境部署(binary/docker/runner) | `python devloy.py --mode runner` |
|
||||
| `soul-api/master.py` | 正式环境部署 | `python master.py` |
|
||||
| `soul-api/deploy/runner-init.sh` | Runner 容器首次初始化 | `bash deploy/runner-init.sh` |
|
||||
| `soul-api/deploy/docker-deploy-remote.sh` | 服务器上执行蓝绿切换 | 由 devloy 自动调用 |
|
||||
| `soul-api/deploy/deploy-runner-remote.sh` | Runner 模式部署包拷贝 | 由 devloy 自动调用 |
|
||||
|
||||
**devloy 模式**:
|
||||
|
||||
- `--mode runner`:容器内红蓝切换,宝塔固定 9001
|
||||
- `--mode docker`:宿主机蓝绿,需 Nginx 切换
|
||||
- `--mode binary`:Go 二进制 + 宝塔 soulDev
|
||||
|
||||
### 2.2 soul-admin
|
||||
|
||||
| 脚本 | 用途 | 命令示例 |
|
||||
|------|------|----------|
|
||||
| `soul-admin/master.py` | 正式环境部署(soul-admin,pnpm build) | `python master.py` |
|
||||
| `soul-admin/deploy.py` | 测试环境部署(soul-admin-dev,pnpm build:dev) | `python deploy.py` |
|
||||
|
||||
### 2.3 挖矿防护
|
||||
|
||||
| 脚本 | 用途 | 命令示例 |
|
||||
|------|------|----------|
|
||||
| `soul-api/miner_guard_install.py` | 安装挖矿守护(soul 服务器) | `python miner_guard_install.py --yes` |
|
||||
| `Cunkebao/miner_guard_install.py` | 安装挖矿守护(Cunkebao) | `cd Cunkebao && python miner_guard_install.py --yes` |
|
||||
|
||||
---
|
||||
|
||||
## 三、环境变量一览
|
||||
|
||||
| 变量 | 说明 | 默认来源 |
|
||||
|------|------|----------|
|
||||
| DEPLOY_HOST | SSH 主机 | 各脚本 get_cfg() |
|
||||
| DEPLOY_USER | SSH 用户 | root |
|
||||
| DEPLOY_PASSWORD | SSH 密码 | 脚本默认 / 需设置 |
|
||||
| DEPLOY_SSH_KEY | SSH 私钥路径 | 空 |
|
||||
| DEPLOY_SSH_PORT | SSH 端口 | 22022 |
|
||||
| BT_PANEL_URL | 宝塔面板 URL | https://{host}:9988 |
|
||||
| BT_API_KEY | 宝塔 API 密钥 | 脚本内默认 |
|
||||
| BT_GO_PROJECT_NAME | 宝塔 Go 项目名 | soulDev / soulApi |
|
||||
| DEPLOY_DOCKER_PATH | 部署目录 | /www/wwwroot/self/soul-dev |
|
||||
| DEPLOY_NGINX_CONF | Nginx 配置路径 | 空(可自动探测) |
|
||||
|
||||
---
|
||||
|
||||
## 四、常用操作
|
||||
|
||||
### 4.1 SSH 连接(示例,密码从环境变量读取)
|
||||
|
||||
```powershell
|
||||
# 设置环境变量后
|
||||
$env:DEPLOY_HOST="43.139.27.93"
|
||||
$env:DEPLOY_PASSWORD="<从安全存储读取>"
|
||||
ssh -p 22022 root@43.139.27.93
|
||||
```
|
||||
|
||||
### 4.2 部署 soul-api 测试环境(Runner 模式)
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python devloy.py --mode runner
|
||||
```
|
||||
|
||||
### 4.3 部署 soul-api 正式环境
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python master.py
|
||||
```
|
||||
|
||||
### 4.4 部署 soul-admin 正式环境
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-admin
|
||||
python master.py
|
||||
```
|
||||
|
||||
### 4.5 部署 soul-admin 测试环境
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-admin
|
||||
python deploy.py
|
||||
```
|
||||
|
||||
### 4.6 检查挖矿守护
|
||||
|
||||
```powershell
|
||||
cd e:\Gongsi\Mycontent\soul-api
|
||||
python miner_guard_check.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、安全提醒
|
||||
|
||||
- **不要将密码提交到 Git**:master.py、devloy.py 等中的默认密码应迁移到环境变量
|
||||
- **宝塔 API 密钥**:BT_API_KEY 若泄露需在宝塔面板重新生成
|
||||
- **敏感文件**:`.env`、`master.py` 等应加入 `.gitignore` 或使用 `.env.example` 模板
|
||||
BIN
Cunkebao/__pycache__/devlop.cpython-311.pyc
Normal file
BIN
Cunkebao/__pycache__/devlop.cpython-311.pyc
Normal file
Binary file not shown.
111
Cunkebao/miner_guard_install.py
Normal file
111
Cunkebao/miner_guard_install.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
挖矿病毒守护 - Cunkebao 服务器安装脚本
|
||||
使用 devlop.py 中的服务器配置,上传 miner_guard.sh 并配置每 30 分钟执行。
|
||||
运行: cd Cunkebao && python miner_guard_install.py --yes
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("错误: pip install paramiko")
|
||||
sys.exit(1)
|
||||
|
||||
# Cunkebao 服务器配置(与 devlop.py 一致)
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
cfg = {
|
||||
"host": "42.194.245.239",
|
||||
"port": 6523,
|
||||
"user": "yongpxu",
|
||||
"password": "Aa123456789.",
|
||||
}
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
p = argparse.ArgumentParser(description="Cunkebao 挖矿守护安装")
|
||||
p.add_argument("--yes", "-y", action="store_true", help="跳过确认")
|
||||
args = p.parse_args()
|
||||
|
||||
# miner_guard.sh 来自 soul-api
|
||||
local_sh = os.path.join(os.path.dirname(script_dir), "soul-api", "miner_guard.sh")
|
||||
if not os.path.isfile(local_sh):
|
||||
local_sh = os.path.join(script_dir, "miner_guard.sh")
|
||||
if not os.path.isfile(local_sh):
|
||||
print("[错误] 未找到 miner_guard.sh,请确保 soul-api/miner_guard.sh 存在")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 60)
|
||||
print(" 挖矿病毒守护 - Cunkebao 安装")
|
||||
print("=" * 60)
|
||||
print(" 服务器: %s:%s" % (cfg["host"], cfg["port"]))
|
||||
print(" 用户: %s" % cfg["user"])
|
||||
print(" 脚本: ~/miner_guard.sh")
|
||||
print(" 日志: ~/miner_guard.log")
|
||||
print(" Cron: 每 30 分钟执行")
|
||||
print("=" * 60)
|
||||
if not args.yes:
|
||||
print("\n确认安装? 输入 yes 继续: ", end="")
|
||||
if input().strip().lower() != "yes":
|
||||
print("已取消")
|
||||
return
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
client.connect(cfg["host"], port=cfg["port"], username=cfg["user"],
|
||||
password=cfg["password"], timeout=15)
|
||||
except Exception as e:
|
||||
print("[连接失败]", str(e))
|
||||
sys.exit(1)
|
||||
|
||||
# 获取用户 home 并确定实际路径
|
||||
sin, sout, serr = client.exec_command("echo $HOME", timeout=5)
|
||||
home = sout.read().decode("utf-8", errors="replace").strip() or "/home/%s" % cfg["user"]
|
||||
remote_path = "%s/miner_guard.sh" % home
|
||||
log_path = "%s/miner_guard.log" % home
|
||||
cron_line = "*/30 * * * * /bin/bash %s >> %s 2>&1" % (remote_path, log_path)
|
||||
|
||||
# 非 root 时用 $HOME 做日志,避免 /var/log 无权限
|
||||
with open(local_sh, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
if cfg["user"] != "root":
|
||||
head = '[ "$(id -u)" != "0" ] && LOG="$HOME/miner_guard.log" && LOCK="/tmp/miner_guard_$(whoami).lock"\n'
|
||||
if "LOG=" in content and "id -u" not in content[:200]:
|
||||
content = head + content
|
||||
sftp = client.open_sftp()
|
||||
with sftp.file(remote_path, "w") as f:
|
||||
f.write(content)
|
||||
sftp.chmod(remote_path, 0o755)
|
||||
sftp.close()
|
||||
|
||||
# 用户 crontab
|
||||
crontab_line = cron_line + "\n"
|
||||
tmp_cron = "/tmp/miner_guard_cron_%s" % os.getpid()
|
||||
sftp = client.open_sftp()
|
||||
with sftp.file(tmp_cron, "w") as f:
|
||||
f.write(crontab_line)
|
||||
sftp.close()
|
||||
|
||||
sin, sout, serr = client.exec_command(
|
||||
"(crontab -l 2>/dev/null | grep -v miner_guard || true; cat %s) | crontab - 2>&1; rm -f %s; crontab -l 2>/dev/null" % (tmp_cron, tmp_cron),
|
||||
timeout=10
|
||||
)
|
||||
out = sout.read().decode("utf-8", errors="replace")
|
||||
err = serr.read().decode("utf-8", errors="replace")
|
||||
if "miner_guard" in out:
|
||||
print("\n[成功] 已安装,每 30 分钟执行")
|
||||
else:
|
||||
print("\n[警告] crontab 可能未添加,错误: %s" % (err or out))
|
||||
print(" 请 SSH 登录后执行: (crontab -l 2>/dev/null; echo '%s') | crontab -" % cron_line.strip())
|
||||
|
||||
client.close()
|
||||
print("\n日志: %s" % log_path)
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 小程序入口
|
||||
* 卡若创业派对 - 小程序入口
|
||||
* 开发: 卡若
|
||||
*/
|
||||
|
||||
@@ -13,8 +13,8 @@ const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
App({
|
||||
globalData: {
|
||||
// API 基础地址:开发时修改下面一行切换环境
|
||||
baseUrl: "https://soulapi.quwanzhi.com",
|
||||
// baseUrl: 'http://localhost:8080', // 开发
|
||||
// baseUrl: "https://soulapi.quwanzhi.com",
|
||||
baseUrl: 'http://localhost:8080', // 开发
|
||||
// baseUrl: 'https://souldev.quwanzhi.com', // 测试
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: DEFAULT_APP_ID,
|
||||
@@ -80,10 +80,31 @@ App({
|
||||
supportWechat: '',
|
||||
// config 统一缓存(5min),减少重复请求
|
||||
configCache: null,
|
||||
configCacheExpires: 0
|
||||
configCacheExpires: 0,
|
||||
// VIP 联系方式检测:上次检测时间戳,onShow 节流 5 分钟
|
||||
lastVipContactCheck: 0,
|
||||
// 头像昵称检测:上次检测时间戳,onShow 节流 5 分钟
|
||||
lastAvatarNicknameCheck: 0,
|
||||
},
|
||||
|
||||
|
||||
onLaunch(options) {
|
||||
// 昵称等隐私组件需先授权:input type="nickname" 不会主动触发,需配合 wx.requirePrivacyAuthorize 使用
|
||||
if (typeof wx.onNeedPrivacyAuthorization === 'function') {
|
||||
wx.onNeedPrivacyAuthorization((resolve) => {
|
||||
this._privacyResolve = resolve
|
||||
const pages = getCurrentPages()
|
||||
const cur = pages[pages.length - 1]
|
||||
const route = (cur && cur.route) || ''
|
||||
const needPrivacyPages = ['avatar-nickname', 'profile-edit', 'read', 'my', 'gift-pay/detail', 'index', 'settings']
|
||||
const needShow = needPrivacyPages.some(p => route.includes(p))
|
||||
if (cur && typeof cur.setData === 'function' && needShow) {
|
||||
cur.setData({ showPrivacyModal: true })
|
||||
} else {
|
||||
resolve({ event: 'disagree' })
|
||||
}
|
||||
})
|
||||
}
|
||||
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
|
||||
// 加载 iconfont(字体图标)。注意:小程序不支持在 wxss 里用本地 @font-face 引用字体文件,
|
||||
// 需使用 loadFontFace 动态加载(字体文件建议走 https CDN)。
|
||||
@@ -103,6 +124,11 @@ App({
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
// 每次进入:先获取 VIP 状态,VIP 走 profile-edit,非 VIP 走头像/昵称引导(由 checkVipContactRequiredAndGuide 内部链式调用)
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1500)
|
||||
setTimeout(() => this.connectWsHeartbeat(), 2000)
|
||||
}
|
||||
|
||||
// 加载书籍数据
|
||||
this.loadBookData()
|
||||
@@ -143,6 +169,23 @@ App({
|
||||
this.globalData.lastMpConfigCheck = now
|
||||
this.getAuditMode()
|
||||
}
|
||||
// 从后台切回:先 VIP 强制跳转,再头像/昵称,节流 5 分钟
|
||||
const throttle = 5 * 60 * 1000
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
|
||||
if (!this.globalData.lastVipContactCheck || now - this.globalData.lastVipContactCheck > throttle) {
|
||||
this.globalData.lastVipContactCheck = now
|
||||
this.globalData.lastAvatarNicknameCheck = now
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 500)
|
||||
}
|
||||
// 从后台切回:若 WSS 已断开则重连(微信后台可能回收连接)
|
||||
try {
|
||||
const need = !this._wsSocketTask || (this._wsSocketTask.readyState !== 0 && this._wsSocketTask.readyState !== 1)
|
||||
if (need) {
|
||||
this.clearWsReconnect()
|
||||
setTimeout(() => this.connectWsHeartbeat(), 1000)
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
|
||||
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
|
||||
@@ -322,6 +365,44 @@ App({
|
||||
return false
|
||||
},
|
||||
|
||||
/** 判断头像/昵称是否未完善(默认状态) */
|
||||
_needsAvatarNickname(user) {
|
||||
const u = user || this.globalData.userInfo || {}
|
||||
const avatar = (u.avatar || u.avatarUrl || '').trim()
|
||||
const nickname = (u.nickname || u.nickName || '').trim()
|
||||
return !avatar || avatar.includes('default') || !nickname || nickname === '微信用户' || nickname.startsWith('微信用户')
|
||||
},
|
||||
|
||||
/**
|
||||
* 头像/昵称未改则引导:老用户弹窗后跳 avatar-nickname;新用户由登录处强制 redirectTo
|
||||
* VIP 用户不在此处理,统一走 checkVipContactRequiredAndGuide 只跳 profile-edit,避免乱跳
|
||||
*/
|
||||
checkAvatarNicknameAndGuide() {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
if (this.globalData.isVip) return // VIP 统一走 profile-edit,此处不触发
|
||||
if (!this._needsAvatarNickname()) return
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const last = pages[pages.length - 1]
|
||||
const route = (last && last.route) || ''
|
||||
if (route.indexOf('profile-edit') !== -1 || route.indexOf('avatar-nickname') !== -1) return
|
||||
} catch (_) {}
|
||||
// 老用户:弹窗提示后跳转
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const lastDate = wx.getStorageSync('lastAvatarGuideDate') || ''
|
||||
if (lastDate === today) return
|
||||
wx.setStorageSync('lastAvatarGuideDate', today)
|
||||
wx.showModal({
|
||||
title: '完善个人资料',
|
||||
content: '请设置头像和昵称,让其他创业者更好地认识你',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
try {
|
||||
@@ -335,12 +416,219 @@ App({
|
||||
this.globalData.hasFullBook = userInfo.hasFullBook || false
|
||||
this.globalData.isVip = userInfo.isVip || false
|
||||
this.globalData.vipExpireDate = userInfo.vipExpireDate || ''
|
||||
// 若手机号为空,后台静默刷新用户资料以同步最新手机号(可能在其他设备/页面已绑定)
|
||||
if (!(userInfo.phone || '').trim()) {
|
||||
this._refreshUserInfoIfPhoneEmpty()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查登录状态失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 手机号登录后:若响应中 user.phone 为空,从 profile 拉取最新资料并更新本地(后端已写入 DB)
|
||||
*/
|
||||
async _syncPhoneFromProfileAfterLogin(userId) {
|
||||
try {
|
||||
if (!userId) return
|
||||
const res = await this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const profile = res?.data
|
||||
if (!profile) return
|
||||
const phone = (profile.phone || '').trim()
|
||||
if (!phone) return
|
||||
const updated = { ...this.globalData.userInfo, phone }
|
||||
if (profile.wechatId != null) updated.wechatId = profile.wechatId
|
||||
this.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
wx.setStorageSync('user_phone', phone)
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
/**
|
||||
* 当本地 userInfo.phone 为空时,静默拉取 profile 并更新(用户可能在设置页或其他入口已绑定手机号)
|
||||
*/
|
||||
async _refreshUserInfoIfPhoneEmpty() {
|
||||
try {
|
||||
const userId = this.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
const res = await this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const profile = res?.data
|
||||
if (!profile) return
|
||||
const phone = (profile.phone || '').trim()
|
||||
if (!phone) return
|
||||
const updated = { ...this.globalData.userInfo, phone }
|
||||
if (profile.wechatId != null) updated.wechatId = profile.wechatId
|
||||
this.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
if (phone) wx.setStorageSync('user_phone', phone)
|
||||
} catch (_) {
|
||||
// 静默失败,不影响主流程
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* WSS 在线心跳(占位):登录后连接 ws,发送 auth + 心跳,供管理端统计在线人数
|
||||
* 容错:任意异常均不向外抛出,不影响登录、API 请求等核心功能
|
||||
*/
|
||||
clearWsReconnect() {
|
||||
try {
|
||||
if (this._wsReconnectTimerId) {
|
||||
clearTimeout(this._wsReconnectTimerId)
|
||||
this._wsReconnectTimerId = null
|
||||
}
|
||||
this._wsReconnectDelay = 3000
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
scheduleWsReconnect() {
|
||||
try {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
if (this._wsReconnectTimerId) return
|
||||
const delay = this._wsReconnectDelay || 3000
|
||||
this._wsReconnectTimerId = setTimeout(() => {
|
||||
this._wsReconnectTimerId = null
|
||||
this._wsReconnectDelay = Math.min(60000, (this._wsReconnectDelay || 3000) * 2)
|
||||
this.connectWsHeartbeat()
|
||||
}, delay)
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
connectWsHeartbeat() {
|
||||
try {
|
||||
this.clearWsReconnect()
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
const userId = this.globalData.userInfo.id
|
||||
const base = (this.globalData.baseUrl || '').replace(/\/$/, '')
|
||||
if (!base) return
|
||||
const wsUrl = base.replace(/^http/, 'ws') + '/ws/miniprogram'
|
||||
if (this._wsHeartbeatTimer) {
|
||||
clearInterval(this._wsHeartbeatTimer)
|
||||
this._wsHeartbeatTimer = null
|
||||
}
|
||||
if (this._wsSocketTask) {
|
||||
try { this._wsSocketTask.close() } catch (_) {}
|
||||
this._wsSocketTask = null
|
||||
}
|
||||
let task
|
||||
try {
|
||||
task = wx.connectSocket({
|
||||
url: wsUrl,
|
||||
fail: () => { try { this.scheduleWsReconnect() } catch (_) {} }
|
||||
})
|
||||
} catch (e) {
|
||||
if (typeof console !== 'undefined' && console.warn) console.warn('[WS] 连接失败(静默):', e?.message || e)
|
||||
try { this.scheduleWsReconnect() } catch (_) {}
|
||||
return
|
||||
}
|
||||
task.onOpen(() => {
|
||||
try {
|
||||
this.clearWsReconnect()
|
||||
task.send({ data: JSON.stringify({ type: 'auth', userId }) })
|
||||
this._wsHeartbeatTimer = setInterval(() => {
|
||||
try {
|
||||
if (task && task.readyState === 1) task.send({ data: JSON.stringify({ type: 'heartbeat' }) })
|
||||
} catch (_) {}
|
||||
}, 30000)
|
||||
} catch (_) {}
|
||||
})
|
||||
task.onClose(() => {
|
||||
try {
|
||||
if (this._wsHeartbeatTimer) { clearInterval(this._wsHeartbeatTimer); this._wsHeartbeatTimer = null }
|
||||
this._wsSocketTask = null
|
||||
this.scheduleWsReconnect()
|
||||
} catch (_) {}
|
||||
})
|
||||
task.onError(() => {
|
||||
try {
|
||||
if (this._wsHeartbeatTimer) { clearInterval(this._wsHeartbeatTimer); this._wsHeartbeatTimer = null }
|
||||
this._wsSocketTask = null
|
||||
this.scheduleWsReconnect()
|
||||
} catch (_) {}
|
||||
})
|
||||
this._wsSocketTask = task
|
||||
} catch (e) {
|
||||
if (typeof console !== 'undefined' && console.warn) console.warn('[WS] 心跳异常(静默,不影响业务):', e?.message || e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* VIP 用户登录后检测:手机号/头像昵称等需完善时,统一只跳 profile-edit,避免与 avatar-nickname 乱跳。
|
||||
* 旧数据(VIP 但头像昵称未改):弹窗「为了更好服务,请完善资料」→ redirectTo profile-edit
|
||||
*/
|
||||
async checkVipContactRequiredAndGuide() {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
const now = Date.now()
|
||||
if (this._lastVipGuideRun && now - this._lastVipGuideRun < 3000) return // 3 秒内不重复执行,避免 onLaunch+onShow 双重触发
|
||||
this._lastVipGuideRun = now
|
||||
const userId = this.globalData.userInfo.id
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const last = pages[pages.length - 1]
|
||||
const route = (last && last.route) || ''
|
||||
if (route.indexOf('profile-edit') !== -1 || route.indexOf('avatar-nickname') !== -1) return
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
const [vipRes, profileRes] = await Promise.all([
|
||||
this.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true }).catch(() => null),
|
||||
this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true }).catch(() => null)
|
||||
])
|
||||
const isVip = vipRes?.data?.isVip || this.globalData.isVip || false
|
||||
this.globalData.isVip = isVip
|
||||
if (!isVip) {
|
||||
this.checkAvatarNicknameAndGuide()
|
||||
return
|
||||
}
|
||||
|
||||
const profileData = profileRes?.data || this.globalData.userInfo || {}
|
||||
const phone = (profileData.phone || this.globalData.userInfo?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechatId = (profileData.wechatId || profileData.wechat_id || this.globalData.userInfo?.wechatId || this.globalData.userInfo?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
const needsAvatarNickname = this._needsAvatarNickname(profileData)
|
||||
|
||||
// VIP 头像/昵称未改(含旧数据):统一只跳 profile-edit,弹窗「为了更好服务,请完善资料」
|
||||
if (needsAvatarNickname) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '为了更好为您服务,请完善资料',
|
||||
confirmText: '去完善',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (phone && wechatId) return
|
||||
|
||||
// VIP 无手机号:弹窗说明后跳转
|
||||
if (!phone) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: 'VIP会员需完善手机号,以便使用找伙伴、提现等功能',
|
||||
confirmText: '去完善',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// 有手机号但缺微信号:弹窗引导(非强制)
|
||||
wx.showModal({
|
||||
title: '完善联系方式',
|
||||
content: '请到资料页完善微信号,便于他人联系您',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[App] checkVipContactRequiredAndGuide 失败:', e?.message)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载书籍元数据(totalSections),不再预加载 all-chapters
|
||||
async loadBookData() {
|
||||
try {
|
||||
@@ -659,8 +947,17 @@ App({
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料(规则引擎接管,完善头像吸收到规则引擎)
|
||||
checkAndExecute('after_login', null)
|
||||
// 同步 isVip(与 checkLoginStatus 一致)
|
||||
this.globalData.isVip = user.isVip || false
|
||||
this.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
// 首次登录注册:强制跳转 avatar-nickname 修改头像昵称(不弹窗)
|
||||
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
|
||||
} else {
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
|
||||
setTimeout(() => this.connectWsHeartbeat(), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
return res.data
|
||||
@@ -721,8 +1018,16 @@ App({
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料(规则引擎接管)
|
||||
checkAndExecute('after_login', null)
|
||||
// 同步 isVip
|
||||
this.globalData.isVip = user.isVip || false
|
||||
this.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
// 首次登录注册:强制跳转 avatar-nickname
|
||||
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
|
||||
} else {
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
|
||||
}
|
||||
}
|
||||
return res.data.openId
|
||||
}
|
||||
@@ -764,9 +1069,18 @@ App({
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
this.globalData.isVip = user.isVip || false
|
||||
this.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
// 手机号登录后:若用户资料中手机号为空,从 profile 刷新并更新(后端已写入 DB,可能响应中未带回)
|
||||
const phone = (user.phone || '').trim()
|
||||
if (!phone) {
|
||||
this._syncPhoneFromProfileAfterLogin(user.id)
|
||||
} else {
|
||||
wx.setStorageSync('user_phone', phone)
|
||||
}
|
||||
|
||||
// 登录成功后绑定推荐码
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
@@ -775,9 +1089,14 @@ App({
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料(规则引擎接管)
|
||||
checkAndExecute('after_login', null)
|
||||
|
||||
// 首次登录注册:强制跳转 avatar-nickname
|
||||
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
|
||||
} else {
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
|
||||
}
|
||||
|
||||
return res.data
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
"icon": "/components/icon/icon",
|
||||
"login-modal": "/components/login-modal/login-modal"
|
||||
},
|
||||
"pages": [
|
||||
"pages/chapters/chapters",
|
||||
@@ -28,12 +29,13 @@
|
||||
"pages/avatar-nickname/avatar-nickname",
|
||||
"pages/gift-pay/detail",
|
||||
"pages/gift-pay/list",
|
||||
"pages/gift-pay/redemption-detail"
|
||||
"pages/gift-pay/redemption-detail",
|
||||
"pages/dev-login/dev-login"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTitleText": "Soul创业派对",
|
||||
"navigationBarTitleText": "卡若创业派对",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#000000",
|
||||
"navigationStyle": "custom"
|
||||
|
||||
@@ -42,38 +42,127 @@ Component({
|
||||
},
|
||||
|
||||
methods: {
|
||||
// iconfont 映射:将业务 name(lucide 风格)映射到 iconfont 的 unicode(形如 "\ue6aa")
|
||||
// iconfont 映射:与 static/iconfont.wxss 一一对应(icon-xxx -> xxx)
|
||||
// 小程序不支持通过 :before { content } 渲染,因此必须直接输出 unicode 字符
|
||||
getFontGlyph(name) {
|
||||
const map = {
|
||||
// 基础高频(来自 static/iconfont.css 的 content 值)
|
||||
'wallet': '\ue6c8',
|
||||
// === 来自 iconfont.wxss 完整映射 ===
|
||||
'qianbao': '\ue6c8',
|
||||
'gift': '\ue6c9',
|
||||
'zap1': '\ue75c',
|
||||
'user': '\ue6b9',
|
||||
'upload': '\ue6ba',
|
||||
'work': '\ue6bb',
|
||||
'training': '\ue6bc',
|
||||
'warning': '\ue6bd',
|
||||
'zoom-in': '\ue6be',
|
||||
'zoom-out': '\ue6bf',
|
||||
'arrow-left-bold': '\ue6c1',
|
||||
'arrow-up-bold': '\ue6c2',
|
||||
'close-bold': '\ue6c3',
|
||||
'arrow-down-bold': '\ue6c4',
|
||||
'minus-bold': '\ue6c5',
|
||||
'arrow-right-bold': '\ue6c6',
|
||||
'select-bold': '\ue6c7',
|
||||
'money-wallet': '\ue833',
|
||||
'book-open': '\ue993',
|
||||
'biaoshilei_yonghuzu': '\ue61b',
|
||||
'add': '\ue664',
|
||||
'add-circle': '\ue665',
|
||||
'adjust': '\ue666',
|
||||
'arrow-up-circle': '\ue667',
|
||||
'arrow-right-circle': '\ue668',
|
||||
'arrow-down': '\ue669',
|
||||
'ashbin': '\ue66a',
|
||||
'arrow-right': '\ue66b',
|
||||
'browse': '\ue66c',
|
||||
'bottom': '\ue66d',
|
||||
'back': '\ue66e',
|
||||
'bad': '\ue66f',
|
||||
'arrow-left-circle': '\ue670',
|
||||
'camera': '\ue671',
|
||||
'chart-bar': '\ue672',
|
||||
'attachment': '\ue673',
|
||||
'code': '\ue674',
|
||||
'close': '\ue675',
|
||||
'check-item': '\ue676',
|
||||
'calendar': '\ue677',
|
||||
'comment': '\ue678',
|
||||
'complete': '\ue679',
|
||||
'direction-down': '\ue67a',
|
||||
'direction-down-circle': '\ue67b',
|
||||
'direction-right': '\ue67c',
|
||||
'direction-up': '\ue67d',
|
||||
'discount': '\ue67e',
|
||||
'electronics': '\ue681',
|
||||
'elipsis': '\ue682',
|
||||
'export': '\ue683',
|
||||
'explain': '\ue684',
|
||||
'edit': '\ue685',
|
||||
'eye-close': '\ue686',
|
||||
'email': '\ue687',
|
||||
'error': '\ue688',
|
||||
'favorite': '\ue689',
|
||||
'file-common': '\ue68a',
|
||||
'file-delete': '\ue68b',
|
||||
'file-add': '\ue68c',
|
||||
'film': '\ue68d',
|
||||
'fabulous': '\ue68e',
|
||||
'file': '\ue68f',
|
||||
'folder-close': '\ue690',
|
||||
'filter': '\ue691',
|
||||
'good': '\ue692',
|
||||
'hide': '\ue693',
|
||||
'home': '\ue694',
|
||||
'file-open': '\ue695',
|
||||
'forward': '\ue696',
|
||||
'import': '\ue697',
|
||||
'layers': '\ue698',
|
||||
'lock': '\ue699',
|
||||
'map': '\ue69a',
|
||||
'menu': '\ue69b',
|
||||
'help': '\ue69c',
|
||||
'minus-circle': '\ue69d',
|
||||
'notification': '\ue69e',
|
||||
'more': '\ue69f',
|
||||
'mobile-phone': '\ue6a0',
|
||||
'minus': '\ue6a1',
|
||||
'navigation': '\ue6a2',
|
||||
'prompt': '\ue6a3',
|
||||
'refresh': '\ue6a4',
|
||||
'run-up': '\ue6a5',
|
||||
'picture': '\ue6a6',
|
||||
'run-in': '\ue6a7',
|
||||
'pin': '\ue6a8',
|
||||
'save': '\ue6a9',
|
||||
'search': '\ue6aa',
|
||||
'share': '\ue6ab',
|
||||
'home': '\ue694',
|
||||
'lock': '\ue699',
|
||||
'camera': '\ue671',
|
||||
'warning': '\ue6bd',
|
||||
'scanning': '\ue6ac',
|
||||
'security': '\ue6ad',
|
||||
'sign-out': '\ue6ae',
|
||||
'select': '\ue6af',
|
||||
'stop': '\ue6b0',
|
||||
'success': '\ue6b1',
|
||||
'switch': '\ue6b2',
|
||||
'setting': '\ue6b3',
|
||||
'survey': '\ue6b4',
|
||||
'time': '\ue6b5',
|
||||
'telephone': '\ue6b6',
|
||||
'top': '\ue6b7',
|
||||
'unlock': '\ue6b8',
|
||||
|
||||
// 箭头/展开
|
||||
// === 业务别名(兼容 lucide 等命名)===
|
||||
'wallet': '\ue6c8',
|
||||
'chevron-left': '\ue6c1',
|
||||
'chevron-right': '\ue6c6',
|
||||
'chevron-down': '\ue6c4',
|
||||
'chevron-up': '\ue6c2',
|
||||
'arrow-up-right': '\ue6c2',
|
||||
|
||||
// 交互/状态
|
||||
'x': '\ue6c3',
|
||||
'check': '\ue6c7',
|
||||
'plus': '\ue664',
|
||||
'trash-2': '\ue66a',
|
||||
'pencil': '\ue685',
|
||||
'zap': '\ue75c',
|
||||
'info': '\ue69c',
|
||||
|
||||
// 语义近似映射(iconfont 不一定有同名)
|
||||
'map-pin': '\ue6a8',
|
||||
'message-circle': '\ue678',
|
||||
'smartphone': '\ue6a0',
|
||||
@@ -81,9 +170,6 @@ Component({
|
||||
'shield': '\ue6ad',
|
||||
'star': '\ue689',
|
||||
'heart': '\ue68e',
|
||||
|
||||
// 其他:若 iconfont 里不存在,则继续走 SVG 兜底
|
||||
'book-open': '\ue993',
|
||||
'bar-chart': '\ue672',
|
||||
'clock': '\ue6b5',
|
||||
}
|
||||
|
||||
79
miniprogram/components/login-modal/login-modal.js
Normal file
79
miniprogram/components/login-modal/login-modal.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Soul 创业派对 - 公用登录弹窗
|
||||
* 手机号一键登录 + 隐私协议
|
||||
*/
|
||||
Component({
|
||||
properties: {
|
||||
show: { type: Boolean, value: false },
|
||||
desc: { type: String, value: '登录后可购买章节、解锁更多内容' },
|
||||
showPrivacyModal: { type: Boolean, value: false },
|
||||
showCancel: { type: Boolean, value: false }
|
||||
},
|
||||
data: {
|
||||
agreeProtocol: false,
|
||||
isLoggingIn: false
|
||||
},
|
||||
observers: {
|
||||
show(v) {
|
||||
if (!v) this.setData({ agreeProtocol: false, isLoggingIn: false })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stopPropagation() {},
|
||||
onClose() {
|
||||
this.triggerEvent('close')
|
||||
},
|
||||
onToggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
},
|
||||
onOpenUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
},
|
||||
onOpenPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
},
|
||||
onAgreePrivacy() {
|
||||
const app = getApp()
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.triggerEvent('privacyagree')
|
||||
},
|
||||
async onPhoneLogin(e) {
|
||||
if (!e.detail.code) {
|
||||
return this._fallbackWechatLogin()
|
||||
}
|
||||
const app = getApp()
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
this.setData({ isLoggingIn: false })
|
||||
if (result) {
|
||||
this.triggerEvent('success')
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
this.setData({ isLoggingIn: false })
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async _fallbackWechatLogin() {
|
||||
const app = getApp()
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.login()
|
||||
this.setData({ isLoggingIn: false })
|
||||
if (result) {
|
||||
this.triggerEvent('success')
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
this.setData({ isLoggingIn: false })
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
6
miniprogram/components/login-modal/login-modal.json
Normal file
6
miniprogram/components/login-modal/login-modal.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
}
|
||||
}
|
||||
27
miniprogram/components/login-modal/login-modal.wxml
Normal file
27
miniprogram/components/login-modal/login-modal.wxml
Normal file
@@ -0,0 +1,27 @@
|
||||
<!-- Soul 创业派对 - 公用登录弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{show}}" bindtap="onClose">
|
||||
<view class="modal-content login-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="onClose"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 卡若创业派对</text>
|
||||
<text class="login-desc">{{desc}}</text>
|
||||
|
||||
<button id="agree-phone-btn" class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onPhoneLogin" bindagreeprivacyauthorization="onAgreePrivacy" disabled="{{isLoggingIn || !agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '手机号一键登录'}}</text>
|
||||
</button>
|
||||
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacy">同意</button>
|
||||
</view>
|
||||
<view class="login-modal-cancel" wx:if="{{showCancel}}" bindtap="onClose">取消</view>
|
||||
<view class="login-agree-row" catchtap="onToggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="onOpenUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="onOpenPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
136
miniprogram/components/login-modal/login-modal.wxss
Normal file
136
miniprogram/components/login-modal/login-modal.wxss
Normal file
@@ -0,0 +1,136 @@
|
||||
/* Soul 创业派对 - 公用登录弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
}
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
z-index: 1;
|
||||
padding: 16rpx;
|
||||
}
|
||||
.login-modal {
|
||||
padding: 48rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.login-icon {
|
||||
font-size: 80rpx;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.login-desc {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
.btn-wechat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx;
|
||||
background: #07C160;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: none;
|
||||
}
|
||||
.btn-wechat::after { border: none; }
|
||||
.btn-wechat-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
.login-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-wechat-row {
|
||||
margin: 24rpx 0;
|
||||
padding: 24rpx;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.privacy-wechat-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.privacy-agree-btn {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
background: #07C160;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
}
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
.login-agree-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.agree-checkbox {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 6rpx;
|
||||
margin-right: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agree-checked {
|
||||
background: #00CED1;
|
||||
border-color: #00CED1;
|
||||
}
|
||||
.agree-text { color: rgba(255, 255, 255, 0.6); }
|
||||
.agree-link {
|
||||
color: #00CED1;
|
||||
text-decoration: underline;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
119
miniprogram/docs/资料完善引导流程图.md
Normal file
119
miniprogram/docs/资料完善引导流程图.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Soul 小程序 - 资料完善引导流程
|
||||
|
||||
> **入口统一**:仅 `checkVipContactRequiredAndGuide` 被 onLaunch/onShow/登录 调度;非 VIP 时内部链式调用 `checkAvatarNicknameAndGuide`。
|
||||
|
||||
---
|
||||
|
||||
## 一、整体流程(冷启动 / onShow)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([入口:onLaunch 1.5s / onShow 0.5s]) --> CheckLogin{已登录?}
|
||||
CheckLogin -->|否| End1([结束])
|
||||
CheckLogin -->|是| VIPCheck[checkVipContactRequiredAndGuide]
|
||||
VIPCheck --> CoolDown{3秒内已执行过?}
|
||||
CoolDown -->|是| End0([结束,防重复])
|
||||
CoolDown -->|否| RouteSkip{当前在 profile-edit<br/>或 avatar-nickname?}
|
||||
RouteSkip -->|是| End0
|
||||
RouteSkip -->|否| FetchAPI
|
||||
VIPCheck --> FetchAPI[请求 API:VIP 状态 + 用户资料]
|
||||
FetchAPI --> UpdateIsVip[更新 globalData.isVip]
|
||||
UpdateIsVip --> IsVip{VIP?}
|
||||
|
||||
IsVip -->|否| AvatarCheck[checkAvatarNicknameAndGuide]
|
||||
AvatarCheck --> End2([结束])
|
||||
|
||||
IsVip -->|是| SkipProfile{当前在 profile-edit?}
|
||||
SkipProfile -->|是| End3([结束])
|
||||
SkipProfile -->|否| NeedAvatar{头像/昵称未改?}
|
||||
|
||||
NeedAvatar -->|是| Modal1[弹窗:为了更好为您服务,请完善资料]
|
||||
Modal1 --> Redirect1[redirectTo profile-edit]
|
||||
Redirect1 --> End4([结束])
|
||||
|
||||
NeedAvatar -->|否| HasPhone{有手机号?}
|
||||
HasPhone -->|否| Modal2[弹窗:VIP会员需完善手机号...]
|
||||
Modal2 --> Redirect2[redirectTo profile-edit]
|
||||
Redirect2 --> End5([结束])
|
||||
|
||||
HasPhone -->|是| HasWechat{有微信号?}
|
||||
HasWechat -->|是| End6([结束])
|
||||
HasWechat -->|否| Modal3[弹窗:请到资料页完善微信号]
|
||||
Modal3 --> Nav1[navigateTo profile-edit]
|
||||
Nav1 --> End7([结束])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、头像/昵称引导(非 VIP 用户)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([checkAvatarNicknameAndGuide]) --> B{globalData.isVip?}
|
||||
B -->|是| X([跳过,由 VIP 检测处理])
|
||||
B -->|否| C{头像/昵称已完善?}
|
||||
C -->|是| Y([结束])
|
||||
C -->|否| D{当前在 profile-edit<br/>或 avatar-nickname?}
|
||||
D -->|是| Z([结束])
|
||||
D -->|否| E{今日已提示过?}
|
||||
E -->|是| W([结束])
|
||||
E -->|否| F[弹窗:请设置头像和昵称]
|
||||
F --> G{点击去完善?}
|
||||
G -->|是| H[navigateTo avatar-nickname]
|
||||
G -->|否 稍后| I([结束])
|
||||
H --> I
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、首次登录(新注册用户)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Login([登录成功]) --> NewUser{isNewUser?}
|
||||
NewUser -->|否| VIPFlow[checkVipContactRequiredAndGuide]
|
||||
VIPFlow --> Flow1([见流程图一])
|
||||
|
||||
NewUser -->|是| NeedAvatar{头像/昵称未改?}
|
||||
NeedAvatar -->|否| VIPFlow
|
||||
NeedAvatar -->|是| ForceRedirect[redirectTo avatar-nickname<br/>无弹窗,强制跳转]
|
||||
ForceRedirect --> End([结束])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、VIP 购买成功(超级个体)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Pay([VIP 支付成功]) --> Sync[同步权益 / 拉取 VIP 状态]
|
||||
Sync --> Modal[弹窗:为了更好为您服务,请填写好资料]
|
||||
Modal --> Redirect[redirectTo profile-edit?from=vip]
|
||||
Redirect --> End([结束])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、页面职责
|
||||
|
||||
| 页面 | 用途 |
|
||||
|------|------|
|
||||
| **avatar-nickname** | 仅头像 + 昵称,专注引导新用户/非 VIP |
|
||||
| **profile-edit** | 完整资料(手机号、微信号、MBTI、行业等),VIP 用户统一跳转 |
|
||||
|
||||
---
|
||||
|
||||
## 六、触发时机汇总
|
||||
|
||||
| 时机 | 执行的函数 | 延迟 |
|
||||
|------|------------|------|
|
||||
| onLaunch | checkVipContactRequiredAndGuide | 1500ms |
|
||||
| onShow | checkVipContactRequiredAndGuide | 500ms(节流 5min)|
|
||||
| login 成功 | checkVipContactRequiredAndGuide | 1200ms |
|
||||
| getOpenId 返回 user | checkVipContactRequiredAndGuide | 1200ms |
|
||||
| loginWithPhone 成功 | checkVipContactRequiredAndGuide | 1200ms |
|
||||
| VIP 支付成功 | _onVipPaymentSuccess | 即时 |
|
||||
|
||||
---
|
||||
|
||||
*文档生成于资料完善引导逻辑调整后*
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 关于作者页
|
||||
* 卡若创业派对 - 关于作者页
|
||||
* 开发: 卡若
|
||||
*/
|
||||
const app = getApp()
|
||||
@@ -121,13 +121,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 关于',
|
||||
title: '卡若创业派对 - 关于',
|
||||
path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 关于', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 关于', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -125,13 +125,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 地址管理',
|
||||
title: '卡若创业派对 - 地址管理',
|
||||
path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 地址管理', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 地址管理', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -203,13 +203,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 编辑地址',
|
||||
title: '卡若创业派对 - 编辑地址',
|
||||
path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 编辑地址', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 编辑地址', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 用户协议
|
||||
* 卡若创业派对 - 用户协议
|
||||
* 审核要求:登录前可点击《用户协议》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
@@ -23,13 +23,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 用户协议',
|
||||
title: '卡若创业派对 - 用户协议',
|
||||
path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 用户协议', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 用户协议', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 头像昵称引导页
|
||||
* 卡若创业派对 - 头像昵称引导页
|
||||
* 登录后资料未完善时引导用户修改默认头像和昵称,仅包含头像+昵称两项
|
||||
*/
|
||||
const app = getApp()
|
||||
@@ -10,7 +10,8 @@ Page({
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
saving: false,
|
||||
showAvatarModal: false,
|
||||
showPrivacyModal: false,
|
||||
nicknameInputFocus: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -40,39 +41,38 @@ Page({
|
||||
onNicknameChange(e) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
|
||||
onAvatarTap() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['使用微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.setData({ showAvatarModal: true })
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.chooseAvatarFromAlbum()
|
||||
}
|
||||
onNicknameBlur() {
|
||||
this.setData({ nicknameInputFocus: false })
|
||||
},
|
||||
onNicknameAreaTouch() {
|
||||
if (typeof wx.requirePrivacyAuthorize !== 'function') return
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: () => {
|
||||
this.setData({ nicknameInputFocus: true })
|
||||
},
|
||||
fail: () => {},
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
preventMove() {},
|
||||
handleAgreePrivacy() {
|
||||
const app = getApp()
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false, nicknameInputFocus: true })
|
||||
},
|
||||
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
await this.uploadAndSaveAvatar(tempPath)
|
||||
},
|
||||
})
|
||||
handleDisagreePrivacy() {
|
||||
const app = getApp()
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ event: 'disagree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
await this.uploadAndSaveAvatar(tempAvatarUrl)
|
||||
},
|
||||
@@ -98,7 +98,10 @@ Page({
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--Soul创业派对 - 头像昵称引导页,仅头像+昵称-->
|
||||
<!--卡若创业派对 - 头像昵称引导页,仅头像+昵称-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><icon name="chevron-left" size="44" color="rgba(255,255,255,0.8)" customClass="back-icon"></icon></view>
|
||||
@@ -15,35 +15,48 @@
|
||||
<text class="guide-desc">让他人更好地认识你,展示更专业的形象</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<!-- 头像:点击直接弹出微信原生选择器;头像与文字水平对齐 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<text class="avatar-change">点击更换头像</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<!-- 昵称:点击前先请求隐私授权,解决 errno:104 昵称选择器无法弹出 -->
|
||||
<view class="form-section">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap">
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击输入框自动填充昵称,或手动输入</text>
|
||||
</view>
|
||||
|
||||
<!-- 隐私授权弹窗(昵称需授权后方可唤起微信昵称选择器) -->
|
||||
<view class="privacy-mask" wx:if="{{showPrivacyModal}}" catchtouchmove="preventMove">
|
||||
<view class="privacy-modal">
|
||||
<text class="privacy-title">温馨提示</text>
|
||||
<text class="privacy-desc">为获取微信昵称,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-btn" class="privacy-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacy">同意</button>
|
||||
<view class="privacy-cancel" bindtap="handleDisagreePrivacy">拒绝</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '完成'}}
|
||||
</view>
|
||||
@@ -54,14 +67,4 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像弹窗:使用微信头像 -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<text class="avatar-modal-title">使用微信头像</text>
|
||||
<text class="avatar-modal-desc">点击下方按钮,一键同步当前微信头像</text>
|
||||
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
|
||||
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Soul创业派对 - 头像昵称引导页 */
|
||||
/* 卡若创业派对 - 头像昵称引导页 */
|
||||
.page {
|
||||
background: #050B14;
|
||||
min-height: 100vh;
|
||||
@@ -72,10 +72,20 @@
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
/* 头像按钮:透明无边框,点击直接弹出微信原生选择器 */
|
||||
.avatar-wrap-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
width: 192rpx; height: 192rpx; border-radius: 50%; overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 192rpx;
|
||||
@@ -126,7 +136,6 @@
|
||||
font-size: 28rpx;
|
||||
color: #5EEAD4;
|
||||
font-weight: 500;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
@@ -263,9 +272,79 @@
|
||||
.btn-choose-avatar::after {
|
||||
border: none;
|
||||
}
|
||||
.btn-choose-album {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(94, 234, 212, 0.15);
|
||||
color: #5EEAD4;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: 2rpx solid #5EEAD4;
|
||||
}
|
||||
.avatar-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 隐私授权弹窗(昵称选择器需先授权) */
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 100%;
|
||||
max-width: 560rpx;
|
||||
background: #17212F;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.privacy-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.privacy-desc {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.privacy-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
background: #5EEAD4;
|
||||
color: #050B14;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
.privacy-btn::after { border: none; }
|
||||
.privacy-cancel {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 目录页
|
||||
* 卡若创业派对 - 目录页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
* 数据: 完整真实文章标题
|
||||
@@ -258,13 +258,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 目录',
|
||||
title: '卡若创业派对 - 目录',
|
||||
path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
80
miniprogram/pages/dev-login/dev-login.js
Normal file
80
miniprogram/pages/dev-login/dev-login.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 卡若创业派对 - 开发登录页
|
||||
* 临时:账户=手机号,密码可空,用于切换为对方账号调试
|
||||
*/
|
||||
const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
account: '',
|
||||
password: '',
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
onAccountInput(e) {
|
||||
this.setData({ account: (e.detail.value || '').trim() })
|
||||
},
|
||||
|
||||
onPasswordInput(e) {
|
||||
this.setData({ password: e.detail.value || '' })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
app.goBackOrToHome()
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
const { account, password, loading } = this.data
|
||||
if (!account || loading) return
|
||||
|
||||
const phone = account.replace(/\s/g, '')
|
||||
if (phone.length < 11) {
|
||||
wx.showToast({ title: '请输入11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/dev/login-by-phone', {
|
||||
method: 'POST',
|
||||
data: { phone, password: password || '' }
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
const user = res.data.user
|
||||
app.globalData.userInfo = user
|
||||
app.globalData.isLoggedIn = true
|
||||
app.globalData.purchasedSections = user.purchasedSections || []
|
||||
app.globalData.hasFullBook = user.hasFullBook || false
|
||||
app.globalData.isVip = user.isVip || false
|
||||
app.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || app.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
app.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => app.checkVipContactRequiredAndGuide(), 1200)
|
||||
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 800)
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '登录失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
7
miniprogram/pages/dev-login/dev-login.json
Normal file
7
miniprogram/pages/dev-login/dev-login.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "账户密码登录"
|
||||
}
|
||||
51
miniprogram/pages/dev-login/dev-login.wxml
Normal file
51
miniprogram/pages/dev-login/dev-login.wxml
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--开发登录页 - 临时:账户=手机号,密码可空-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.6)" customClass="back-icon"></icon>
|
||||
</view>
|
||||
<text class="nav-title">账户密码登录</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view class="tip-banner">
|
||||
<text class="tip-text">开发专用:输入对方手机号登录,密码可留空。仅开发环境可用。</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="form-label">账户(手机号)</text>
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="number"
|
||||
placeholder="请输入对方手机号"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{account}}"
|
||||
bindinput="onAccountInput"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">密码(可留空)</text>
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="text"
|
||||
password="{{true}}"
|
||||
placeholder="可选,留空即可"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{password}}"
|
||||
bindinput="onPasswordInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-primary {{!account || loading ? 'btn-disabled' : ''}}" bindtap="handleLogin">
|
||||
{{loading ? '登录中...' : '登录'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
23
miniprogram/pages/dev-login/dev-login.wxss
Normal file
23
miniprogram/pages/dev-login/dev-login.wxss
Normal file
@@ -0,0 +1,23 @@
|
||||
/* 开发登录页 */
|
||||
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
|
||||
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); font-weight: 300; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
|
||||
.nav-placeholder { width: 64rpx; }
|
||||
|
||||
.content { padding: 24rpx 16rpx; }
|
||||
|
||||
.tip-banner { background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 24rpx; }
|
||||
.tip-text { font-size: 24rpx; color: #FFA500; line-height: 1.5; }
|
||||
|
||||
.form-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.form-item { margin-bottom: 32rpx; }
|
||||
.form-item:last-of-type { margin-bottom: 48rpx; }
|
||||
.form-label { font-size: 28rpx; color: rgba(255,255,255,0.8); display: block; margin-bottom: 16rpx; }
|
||||
.form-input-wrap { padding: 16rpx 24rpx; background: #1F2937; border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; }
|
||||
.form-input-inner { width: 100%; font-size: 28rpx; background: transparent; color: #fff; }
|
||||
.input-placeholder { color: rgba(255,255,255,0.25); }
|
||||
.btn-primary { padding: 32rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 28rpx; }
|
||||
.btn-disabled { opacity: 0.5; }
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 代付详情页
|
||||
* 卡若创业派对 - 代付详情页
|
||||
* 改造后:发起人支付,好友领取。支持单页模式引导、登录检测。
|
||||
*/
|
||||
const app = getApp()
|
||||
@@ -18,7 +18,7 @@ Page({
|
||||
amountDisplay: '0.00',
|
||||
isSinglePageMode: false,
|
||||
showLoginModal: false,
|
||||
agreeProtocol: false,
|
||||
showPrivacyModal: false,
|
||||
// 创建态
|
||||
isCreateMode: false,
|
||||
giftQuantity: 1,
|
||||
@@ -28,6 +28,9 @@ Page({
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options?.ref || options?.referralCode) {
|
||||
app.handleReferralCode({ query: { ref: options.ref || options.referralCode } })
|
||||
}
|
||||
const requestSn = (options.requestSn || '').trim()
|
||||
const sectionId = (options.sectionId || '').trim()
|
||||
const isSinglePage = (wx.getSystemInfoSync?.()?.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
@@ -223,7 +226,7 @@ Page({
|
||||
}
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
this.setData({ showLoginModal: true })
|
||||
return
|
||||
}
|
||||
await this._doRedeem()
|
||||
@@ -261,44 +264,15 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
closeLoginModal() {
|
||||
onLoginModalClose() {
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false })
|
||||
},
|
||||
onLoginModalPrivacyAgree() {
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
async onLoginModalSuccess() {
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
},
|
||||
async handleWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (!result) return
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
await this._doRedeem()
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async handlePhoneLogin(e) {
|
||||
if (!e.detail.code) return this.handleWechatLogin()
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
if (!result) return
|
||||
this.setData({ showLoginModal: false })
|
||||
await this._doRedeem()
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
stopPropagation() {},
|
||||
openUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
},
|
||||
openPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
await this._doRedeem()
|
||||
},
|
||||
|
||||
goBack() {
|
||||
@@ -323,12 +297,12 @@ Page({
|
||||
const { requestSn } = this.data
|
||||
const ref = app.getMyReferralCode?.() || app.globalData.userInfo?.referralCode || ''
|
||||
let path = '/pages/gift-pay/detail'
|
||||
if (requestSn) {
|
||||
path = `/pages/gift-pay/detail?requestSn=${requestSn}`
|
||||
if (ref) path += `&ref=${encodeURIComponent(ref)}`
|
||||
}
|
||||
const params = []
|
||||
if (requestSn) params.push(`requestSn=${encodeURIComponent(requestSn)}`)
|
||||
if (ref) params.push(`ref=${encodeURIComponent(ref)}`)
|
||||
if (params.length) path += '?' + params.join('&')
|
||||
return {
|
||||
title: '好友送你一篇好文 - Soul创业派对',
|
||||
title: '好友送你一篇好文 - 卡若创业派对',
|
||||
path
|
||||
}
|
||||
},
|
||||
@@ -336,14 +310,12 @@ Page({
|
||||
onShareTimeline() {
|
||||
const { requestSn } = this.data
|
||||
const ref = app.getMyReferralCode?.() || app.globalData.userInfo?.referralCode || ''
|
||||
let query = ''
|
||||
if (requestSn) {
|
||||
query = `requestSn=${requestSn}`
|
||||
if (ref) query += `&ref=${encodeURIComponent(ref)}`
|
||||
}
|
||||
const params = []
|
||||
if (requestSn) params.push(`requestSn=${encodeURIComponent(requestSn)}`)
|
||||
if (ref) params.push(`ref=${encodeURIComponent(ref)}`)
|
||||
return {
|
||||
title: '好友送你一篇好文 - Soul创业派对',
|
||||
query: query || ''
|
||||
title: '好友送你一篇好文 - 卡若创业派对',
|
||||
query: params.length ? params.join('&') : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Soul创业派对 - 代付详情页(改造后:发起人支付,好友领取) -->
|
||||
<!-- 卡若创业派对 - 代付详情页(改造后:发起人支付,好友领取) -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
@@ -145,26 +145,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗(好友领取时未登录) -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可免费领取并阅读</text>
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
</button>
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" bindtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" bindtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 登录弹窗(公用组件) -->
|
||||
<login-modal
|
||||
show="{{showLoginModal}}"
|
||||
desc="登录后可免费领取并阅读"
|
||||
showPrivacyModal="{{showPrivacyModal}}"
|
||||
bind:close="onLoginModalClose"
|
||||
bind:success="onLoginModalSuccess"
|
||||
bind:privacyagree="onLoginModalPrivacyAgree"
|
||||
/>
|
||||
|
||||
<!-- 背景光效 -->
|
||||
<view class="bg-effects">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Soul创业派对 - 代付详情页(参考 yulan 深色主题、青绿主色) */
|
||||
/* 卡若创业派对 - 代付详情页(参考 yulan 深色主题、青绿主色) */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #050505;
|
||||
@@ -637,6 +637,10 @@
|
||||
}
|
||||
.btn-wechat-disabled { opacity: 0.5; }
|
||||
.btn-wechat-icon { font-weight: 700; margin-right: 8rpx; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
.login-agree-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 我发起的代付(改造后:仅我发起的,含领取记录)
|
||||
* 卡若创业派对 - 我发起的代付(改造后:仅我发起的,含领取记录)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
@@ -10,8 +10,11 @@ Page({
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options && (options.ref || options.referralCode)) {
|
||||
app.handleReferralCode({ query: { ref: options.ref || options.referralCode } })
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
@@ -82,6 +85,8 @@ Page({
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return { title: '我的代付 - Soul创业派对', path: '/pages/gift-pay/list' }
|
||||
const ref = app.getMyReferralCode?.() || app.globalData.userInfo?.referralCode || ''
|
||||
const path = ref ? `/pages/gift-pay/list?ref=${encodeURIComponent(ref)}` : '/pages/gift-pay/list'
|
||||
return { title: '我的代付 - 卡若创业派对', path }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Soul创业派对 - 我的代付(改造后:仅我发起的,含领取记录) -->
|
||||
<!-- 卡若创业派对 - 我的代付(改造后:仅我发起的,含领取记录) -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Soul创业派对 - 我的代付 */
|
||||
/* 卡若创业派对 - 我的代付 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 代付领取详情(发起人查看:文章信息、领取人明细、剩余份数)
|
||||
* 卡若创业派对 - 代付领取详情(发起人查看:文章信息、领取人明细、剩余份数)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Soul创业派对 - 代付领取详情(文章信息、领取人明细、剩余份数) -->
|
||||
<!-- 卡若创业派对 - 代付领取详情(文章信息、领取人明细、剩余份数) -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Soul创业派对 - 代付领取详情 */
|
||||
/* 卡若创业派对 - 代付领取详情 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #050505;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 首页
|
||||
* 卡若创业派对 - 首页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
@@ -61,6 +61,7 @@ Page({
|
||||
// 链接卡若 - 留资弹窗
|
||||
showLeadModal: false,
|
||||
leadPhone: '',
|
||||
showPrivacyModal: false,
|
||||
|
||||
// 展开状态(首页精选/最新)
|
||||
featuredExpanded: false,
|
||||
@@ -380,7 +381,7 @@ Page({
|
||||
},
|
||||
|
||||
closeLeadModal() {
|
||||
this.setData({ showLeadModal: false, leadPhone: '' })
|
||||
this.setData({ showLeadModal: false, leadPhone: '', showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 阻止弹窗内部点击事件冒泡到遮罩层
|
||||
@@ -390,6 +391,15 @@ Page({
|
||||
this.setData({ leadPhone: (e.detail.value || '').trim() })
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
onAgreePrivacyForLead() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 一键获取手机号(微信能力),成功后直接提交链接卡若
|
||||
async onGetPhoneNumberForLead(e) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
@@ -562,13 +572,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 真实商业故事',
|
||||
title: '卡若创业派对 - 真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--pages/index/index.wxml-->
|
||||
<!--Soul创业派对 - 首页(按临时需求池/首页页面设计)-->
|
||||
<!--卡若创业派对 - 首页(按临时需求池/首页页面设计)-->
|
||||
<view class="page page-transition">
|
||||
<!-- 自定义导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
@@ -12,7 +12,7 @@
|
||||
<text class="logo-text">S</text>
|
||||
</view>
|
||||
<view class="logo-info">
|
||||
<text class="logo-title-text">Soul创业派对</text>
|
||||
<text class="logo-title-text">卡若创业派对</text>
|
||||
<text class="logo-subtitle">来自派对房的真实故事</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -38,18 +38,19 @@
|
||||
<!-- Banner卡片 - 最新章节(异步加载) -->
|
||||
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<view class="banner-tag">推荐</view>
|
||||
<view class="banner-title">{{latestSection.title}}</view>
|
||||
<view class="banner-action">
|
||||
<text class="banner-action-text">开始阅读</text>
|
||||
<icon name="chevron-right" size="32" color="#fff" customClass="banner-arrow"></icon>
|
||||
<text class="banner-action-text">点击阅读123</text>
|
||||
<icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<view class="banner-tag">推荐</view>
|
||||
<view class="banner-title">加载中...</view>
|
||||
<view class="banner-action"><text class="banner-action-text">开始阅读</text><icon name="chevron-right" size="32" color="#fff" customClass="banner-arrow"></icon></view>
|
||||
<view class="banner-action"><text class="banner-action-text">点击阅读</text><icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon></view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏) -->
|
||||
@@ -78,7 +79,7 @@
|
||||
>
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.name[0] || '会'}}</text>
|
||||
<text class="super-avatar-text" wx:else>{{(item.name && item.name[0]) || '会'}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
@@ -87,7 +88,7 @@
|
||||
<!-- 已加载无数据 -->
|
||||
<view wx:else class="super-empty">
|
||||
<text class="super-empty-text">成为会员,展示你的项目</text>
|
||||
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 <icon name="chevron-right" size="28" color="#00CED1" customClass="inline-arrow"></icon></view>
|
||||
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -112,7 +113,7 @@
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}">{{item.tag || '精选'}}</text>
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}" wx:if="{{item.tag}}">{{item.tag}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
</view>
|
||||
@@ -142,13 +143,7 @@
|
||||
<view class="timeline-dot"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-row">
|
||||
<view class="timeline-left">
|
||||
<text class="latest-new-tag">NEW</text>
|
||||
<text class="timeline-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="timeline-right">
|
||||
<text class="timeline-price">¥{{item.price}}</text>
|
||||
</view>
|
||||
<text class="timeline-title">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -166,7 +161,11 @@
|
||||
<view class="lead-box" catchtap="stopPropagation">
|
||||
<text class="lead-title">留下联系方式</text>
|
||||
<text class="lead-desc">方便卡若与您联系</text>
|
||||
<button class="lead-get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumberForLead">一键获取手机号</button>
|
||||
<button id="agree-lead-phone-btn" class="lead-get-phone-btn" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onGetPhoneNumberForLead" bindagreeprivacyauthorization="onAgreePrivacyForLead">一键获取手机号</button>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacyForLead">同意</button>
|
||||
</view>
|
||||
<text class="lead-divider">或手动输入</text>
|
||||
<view class="lead-input-wrap">
|
||||
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
|
||||
|
||||
@@ -953,6 +953,10 @@
|
||||
line-height: normal;
|
||||
}
|
||||
.lead-get-phone-btn::after { border: none; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
.lead-divider {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 找伙伴页
|
||||
* 卡若创业派对 - 找伙伴页
|
||||
* 按H5网页端完全重构
|
||||
* 开发: 卡若
|
||||
*/
|
||||
@@ -226,9 +226,9 @@ Page({
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const phone = (res?.data?.phone || '').trim()
|
||||
const wechat = (res?.data?.wechatId || '').trim()
|
||||
if (phone || wechat) {
|
||||
const phone = (res?.data?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechat = (res?.data?.wechatId || res?.data?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (phone && /^1[3-9]\d{9}$/.test(phone)) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
@@ -252,10 +252,10 @@ Page({
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const phone = (this.data.contactPhone || '').trim().replace(/\s/g, '')
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号(必填)', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
@@ -390,7 +390,7 @@ Page({
|
||||
showPurchaseTip() {
|
||||
wx.showModal({
|
||||
title: '需要购买书籍',
|
||||
content: '购买《Soul创业派对》后即可使用匹配功能,仅需9.9元',
|
||||
content: '购买《卡若创业派对》后即可使用匹配功能,仅需9.9元',
|
||||
confirmText: '去购买',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
@@ -761,13 +761,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 找伙伴',
|
||||
title: '卡若创业派对 - 找伙伴',
|
||||
path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 找伙伴', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 找伙伴', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--pages/match/match.wxml-->
|
||||
<!--Soul创业派对 - 找伙伴页 按H5网页端完全重构-->
|
||||
<!--卡若创业派对 - 找伙伴页 按H5网页端完全重构-->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
@@ -274,7 +274,7 @@
|
||||
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
|
||||
<view class="contact-modal" catchtap="preventBubble">
|
||||
<text class="contact-modal-title">请完善联系方式</text>
|
||||
<view class="contact-modal-hint">需完善手机号或微信号才能使用找伙伴功能</view>
|
||||
<view class="contact-modal-hint">手机号必填,微信号建议填写,以便使用找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 超级个体/会员详情页
|
||||
* 卡若创业派对 - 超级个体/会员详情页
|
||||
* 接口:优先 /api/miniprogram/vip/members?id=xx(VIP),回退 /api/miniprogram/users?id=xx(任意用户)
|
||||
* 头像/昵称:统一用用户资料(nickname/avatar)优先,随「我的」修改实时生效
|
||||
* mbti, region, industry, position, businessScale, skills,
|
||||
@@ -186,7 +186,7 @@ Page({
|
||||
const ref = app.getMyReferralCode()
|
||||
const id = this.data.member?.id
|
||||
return {
|
||||
title: 'Soul创业派对 - 创业者详情',
|
||||
title: '卡若创业派对 - 创业者详情',
|
||||
path: id && ref ? `/pages/member-detail/member-detail?id=${id}&ref=${ref}` : id ? `/pages/member-detail/member-detail?id=${id}` : ref ? `/pages/member-detail/member-detail?ref=${ref}` : '/pages/member-detail/member-detail'
|
||||
}
|
||||
},
|
||||
@@ -195,6 +195,6 @@ Page({
|
||||
const ref = app.getMyReferralCode()
|
||||
const id = this.data.member?.id
|
||||
const q = id ? (ref ? `id=${id}&ref=${ref}` : `id=${id}`) : (ref ? `ref=${ref}` : '')
|
||||
return { title: 'Soul创业派对 - 创业者详情', query: q }
|
||||
return { title: '卡若创业派对 - 创业者详情', query: q }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Soul创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
|
||||
<!-- 卡若创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
@@ -18,7 +18,7 @@
|
||||
<view class="avatar-outer">
|
||||
<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="avatar-ph" wx:else><text>{{(member.name && member.name[0]) || '创'}}</text></view>
|
||||
</view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
@@ -132,7 +132,7 @@
|
||||
<view class="bottom-wrap">
|
||||
<view class="btn-super" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<icon name="chevron-right" size="36" color="#00CED1" customClass="btn-arrow"></icon>
|
||||
<icon name="chevron-right" size="36" color="#F59E0B" customClass="btn-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height:160rpx;"></view>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Soul创业派对 - 个人资料页(enhanced_professional_profile 1:1 还原) */
|
||||
/* 卡若创业派对 - 个人资料页(enhanced_professional_profile 1:1 还原) */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
|
||||
/* 导航栏 */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 导师详情(stitch_soul)
|
||||
* 卡若创业派对 - 导师详情(stitch_soul)
|
||||
* 联系导师按钮 → 弹出 v2 弹窗(选择咨询项目)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 选择导师(stitch_soul)
|
||||
* 卡若创业派对 - 选择导师(stitch_soul)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 我的页面
|
||||
* 卡若创业派对 - 我的页面
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
@@ -60,17 +60,12 @@ Page({
|
||||
|
||||
// 登录弹窗
|
||||
showLoginModal: false,
|
||||
isLoggingIn: false,
|
||||
// 用户须主动勾选同意协议(审核要求:不得默认同意)
|
||||
agreeProtocol: false,
|
||||
showPrivacyModal: false,
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
editingNickname: '',
|
||||
|
||||
// 头像弹窗(含 chooseAvatar 按钮,必须用户点击才可获取微信头像)
|
||||
showAvatarModal: false,
|
||||
|
||||
// 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2)
|
||||
showContactModal: false,
|
||||
contactPhone: '',
|
||||
@@ -412,6 +407,8 @@ Page({
|
||||
wx.hideLoading()
|
||||
this.setData({ receivingAll: false })
|
||||
this.loadPendingConfirm()
|
||||
this.loadMyEarnings()
|
||||
this.loadWalletBalance()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -430,9 +427,12 @@ Page({
|
||||
return
|
||||
}
|
||||
const d = res.data
|
||||
// 我的收益 = 累计佣金;我的余额 = 可提现金额(兼容 snake_case)
|
||||
const totalCommission = d.totalCommission ?? d.total_commission ?? 0
|
||||
const availableEarnings = d.availableEarnings ?? d.available_earnings ?? 0
|
||||
this.setData({
|
||||
earnings: formatMoney(d.totalCommission),
|
||||
pendingEarnings: formatMoney(d.availableEarnings),
|
||||
earnings: formatMoney(totalCommission),
|
||||
pendingEarnings: formatMoney(availableEarnings),
|
||||
referralCount: d.referralCount ?? this.data.referralCount,
|
||||
earningsLoading: false,
|
||||
earningsRefreshing: false
|
||||
@@ -458,10 +458,9 @@ Page({
|
||||
wx.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调,真正获取微信头像)
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调,点击头像直接唤起选择器)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
@@ -495,8 +494,11 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
// 2. 获取上传后的完整URL(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
@@ -506,7 +508,7 @@ Page({
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 4. 同步到服务器数据库
|
||||
// 4. 同步到服务器数据库(只保存路径,不含域名)
|
||||
await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, avatar: avatarUrl }
|
||||
@@ -548,12 +550,9 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 打开昵称修改弹窗
|
||||
// 点击昵称:跳转资料编辑页(type="nickname" 在弹窗内无法触发微信昵称选择器,需在主页面)
|
||||
editNickname() {
|
||||
this.setData({
|
||||
showNicknameModal: true,
|
||||
editingNickname: this.data.userInfo?.nickname || ''
|
||||
})
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 关闭昵称弹窗
|
||||
@@ -689,84 +688,23 @@ Page({
|
||||
console.warn('[My] 检测单页模式失败,回退为正常登录弹窗:', e)
|
||||
}
|
||||
try {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
this.setData({ showLoginModal: true })
|
||||
} catch (e) {
|
||||
console.error('[My] showLogin error:', e)
|
||||
this.setData({ showLoginModal: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换协议勾选(用户主动勾选,非默认同意)
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
onLoginModalClose() {
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 打开用户协议页(审核要求:点击《用户协议》需有响应)
|
||||
openUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
onLoginModalPrivacyAgree() {
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
|
||||
openPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
},
|
||||
|
||||
// 关闭登录弹窗
|
||||
closeLoginModal() {
|
||||
if (this.data.isLoggingIn) return
|
||||
onLoginModalSuccess() {
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
|
||||
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
|
||||
async handleWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[My] 微信登录错误:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isLoggingIn: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号登录(需要用户授权)
|
||||
async handlePhoneLogin(e) {
|
||||
// 检查是否有授权code
|
||||
if (!e.detail.code) {
|
||||
// 用户拒绝授权或获取失败,尝试使用微信登录
|
||||
console.log('手机号授权失败,尝试微信登录')
|
||||
return this.handleWechatLogin()
|
||||
}
|
||||
|
||||
this.setData({ isLoggingIn: true })
|
||||
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
if (result) {
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('手机号登录错误:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isLoggingIn: false })
|
||||
}
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
},
|
||||
|
||||
// 点击菜单
|
||||
@@ -875,62 +813,6 @@ Page({
|
||||
} catch (e) { console.log('[My] 余额查询失败', e) }
|
||||
},
|
||||
|
||||
// 头像点击:已登录弹出选项(微信头像 / 相册)
|
||||
onAvatarTap() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.showActionSheet({
|
||||
itemList: ['获取微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) this.setData({ showAvatarModal: true })
|
||||
if (res.tapIndex === 1) this.chooseAvatarFromAlbum()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
},
|
||||
|
||||
// 从相册/相机选择(自定义图片)
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1, mediaType: ['image'], sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
data.success ? resolve(data) : reject(new Error(data.error || '上传失败'))
|
||||
} catch (e) { reject(new Error('解析失败')) }
|
||||
},
|
||||
fail: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
await app.request('/api/miniprogram/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: avatarUrl } })
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
trackClick('my', 'btn_click', '会员中心')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
@@ -974,6 +856,7 @@ Page({
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
this.loadMyEarnings()
|
||||
this.loadWalletBalance()
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
|
||||
@@ -982,18 +865,19 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 提现/找伙伴前检查手机或微信号,未填则弹窗(stitch_soul)
|
||||
// 提现/找伙伴前检查联系方式:手机号必填(与 profile-edit 规则一致)
|
||||
async ensureContactInfo(callback) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const phone = (res?.data?.phone || '').trim()
|
||||
const wechat = (res?.data?.wechatId || '').trim()
|
||||
if (phone || wechat) {
|
||||
const phone = (res?.data?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const hasValidPhone = !!phone && /^1[3-9]\d{9}$/.test(phone)
|
||||
if (hasValidPhone) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const wechat = (res?.data?.wechatId || res?.data?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
this.setData({
|
||||
showContactModal: true,
|
||||
contactPhone: phone || '',
|
||||
@@ -1015,10 +899,14 @@ Page({
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const phone = (this.data.contactPhone || '').trim().replace(/\s/g, '')
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
if (!phone) {
|
||||
wx.showToast({ title: '请输入手机号(必填)', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
@@ -1051,13 +939,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的',
|
||||
title: '卡若创业派对 - 我的',
|
||||
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 我的', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 我的', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,20 +16,21 @@
|
||||
<text wx:else class="guest-avatar-text">{{guestNickname[0] || '游'}}</text>
|
||||
</view>
|
||||
<text class="guest-name">{{guestNickname}}</text>
|
||||
<view class="guest-login-btn" bindtap="showLogin">点击登录</view>
|
||||
<view class="guest-login-btn" bindtap="showLogin">手机号一键登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 已登录:用户卡片(设计稿布局) -->
|
||||
<view class="profile-card" wx:else>
|
||||
<view class="profile-card-inner">
|
||||
<view class="profile-top-row">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
|
||||
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
|
||||
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
|
||||
<view class="vip-badge vip-badge-gray" wx:else>VIP</view>
|
||||
<button class="avatar-overlay-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"></button>
|
||||
</view>
|
||||
<view class="profile-meta">
|
||||
<view class="profile-name-row">
|
||||
@@ -56,7 +57,7 @@
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="profile-stat-val">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
@@ -87,10 +88,7 @@
|
||||
</view>
|
||||
<view class="receive-bottom">
|
||||
<text class="receive-tip">将依次调起微信收款页完成领取</text>
|
||||
<view class="receive-link" bindtap="handleMenuTap" data-id="withdrawRecords">
|
||||
<text>查看提现记录</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.6)" customClass="receive-link-arrow"></icon>
|
||||
</view>
|
||||
<text class="receive-link" bindtap="handleMenuTap" data-id="withdrawRecords">查看提现记录 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -143,7 +141,7 @@
|
||||
</view>
|
||||
<view class="recent-empty" wx:else>
|
||||
<text class="recent-empty-text">暂无阅读记录</text>
|
||||
<view class="recent-empty-btn" bindtap="goToChapters">去阅读 <icon name="chevron-right" size="24" color="#00CED1" customClass="recent-empty-arrow"></icon></view>
|
||||
<view class="recent-empty-btn" bindtap="goToChapters">去阅读 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -173,27 +171,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{isLoggingIn || !agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
|
||||
</button>
|
||||
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 登录弹窗(公用组件) -->
|
||||
<login-modal
|
||||
show="{{showLoginModal}}"
|
||||
desc="登录后可购买章节、解锁更多内容"
|
||||
showPrivacyModal="{{showPrivacyModal}}"
|
||||
showCancel="{{true}}"
|
||||
bind:close="onLoginModalClose"
|
||||
bind:success="onLoginModalSuccess"
|
||||
bind:privacyagree="onLoginModalPrivacyAgree"
|
||||
/>
|
||||
|
||||
<!-- 手机/微信号弹窗 -->
|
||||
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
|
||||
@@ -219,17 +206,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像弹窗:必须点击 button 才能获取微信头像(隐私规范) -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<text class="avatar-modal-title">获取微信头像</text>
|
||||
<text class="avatar-modal-desc">点击下方按钮使用你的微信头像</text>
|
||||
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
|
||||
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改昵称弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
|
||||
<view class="modal-content nickname-modal" catchtap="stopPropagation">
|
||||
|
||||
@@ -41,7 +41,22 @@
|
||||
border: 1rpx solid rgba(75,85,99,0.5);
|
||||
}
|
||||
.profile-top-row { display: flex; align-items: flex-start; gap: 32rpx; }
|
||||
.avatar-wrap { position: relative; flex-shrink: 0; }
|
||||
/* 头像区域:view 负责展示,button 绝对定位覆盖其上,避免原生样式影响 */
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 130rpx; height: 130rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 绝对定位的按钮覆盖在头像上,透明无样式,点击唤起微信选择器(微信头像/相册/拍照) */
|
||||
.avatar-overlay-btn {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
width: 130rpx; height: 130rpx;
|
||||
padding: 0; margin: 0;
|
||||
background: transparent; border: none;
|
||||
display: block;
|
||||
}
|
||||
.avatar-overlay-btn::after { border: none; }
|
||||
.avatar-inner {
|
||||
width: 130rpx; height: 130rpx; border-radius: 50%; overflow: hidden;
|
||||
background: #1C2524; border: 5rpx solid #374151;
|
||||
@@ -208,6 +223,10 @@
|
||||
.agree-text { color: rgba(255,255,255,0.6); }
|
||||
.agree-link { color: #4FD1C5; text-decoration: underline; padding: 0 4rpx; }
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
|
||||
/* 头像弹窗 */
|
||||
.avatar-modal .avatar-modal-title { display: block; font-size: 36rpx; font-weight: bold; color: #fff; text-align: center; margin-bottom: 16rpx; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 隐私政策
|
||||
* 卡若创业派对 - 隐私政策
|
||||
* 审核要求:登录前可点击《隐私政策》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
@@ -23,13 +23,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 隐私政策',
|
||||
title: '卡若创业派对 - 隐私政策',
|
||||
path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 隐私政策', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 隐私政策', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 资料编辑完整版(comprehensive_profile_editor_v1_1)
|
||||
* 卡若创业派对 - 资料编辑完整版(comprehensive_profile_editor_v1_1)
|
||||
* 温馨提示、头像、基本信息、核心联系方式、个人故事、互助需求、项目介绍
|
||||
*
|
||||
* 接口约定(/api/miniprogram/user/profile):
|
||||
@@ -9,6 +9,7 @@
|
||||
* 表单展示:普通用户仅展示 温馨提示、头像、昵称、MBTI、地区、行业、业务体量、职位、核心联系方式;VIP 展示全部
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
@@ -18,6 +19,7 @@ Page({
|
||||
isVip: false,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
shareCardPath: '', // 分享名片封面图(预生成)
|
||||
mbti: '',
|
||||
mbtiIndex: 0,
|
||||
region: '',
|
||||
@@ -37,11 +39,22 @@ Page({
|
||||
showMbtiPicker: false,
|
||||
saving: false,
|
||||
loading: true,
|
||||
showAvatarModal: false,
|
||||
showPrivacyModal: false,
|
||||
nicknameInputFocus: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
onLoad(options) {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
fromVip: options?.from === 'vip',
|
||||
})
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
// 从朋友圈/分享打开且带 id:跳转到名片详情(member-detail)
|
||||
if (options?.id) {
|
||||
const ref = options.ref ? `&ref=${options.ref}` : ''
|
||||
wx.redirectTo({ url: `/pages/member-detail/member-detail?id=${options.id}${ref}` })
|
||||
return
|
||||
}
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
@@ -85,6 +98,7 @@ Page({
|
||||
projectIntro: v('projectIntro'),
|
||||
loading: false,
|
||||
})
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
@@ -95,6 +109,191 @@ Page({
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onNicknameAreaTouch() {
|
||||
if (typeof wx.requirePrivacyAuthorize !== 'function') return
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: () => { this.setData({ nicknameInputFocus: true }) },
|
||||
fail: () => {},
|
||||
})
|
||||
},
|
||||
onNicknameBlur() { this.setData({ nicknameInputFocus: false }) },
|
||||
preventMove() {},
|
||||
handleAgreePrivacy() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false, nicknameInputFocus: true })
|
||||
},
|
||||
handleDisagreePrivacy() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ event: 'disagree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 生成分享名片封面图(参考:头像左+昵称右,分隔线,四栏信息 5:4)
|
||||
async generateShareCard() {
|
||||
const { avatar, nickname, region, mbti, industry, position } = this.data
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('shareCardCanvas', this)
|
||||
const w = 500
|
||||
const h = 400
|
||||
const pad = 32
|
||||
// 背景(深灰卡片感)
|
||||
const grd = ctx.createLinearGradient(0, 0, w, h)
|
||||
grd.addColorStop(0, '#1E293B')
|
||||
grd.addColorStop(1, '#0F172A')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
// 顶部区域:左头像 + 右昵称
|
||||
const avatarSize = 100
|
||||
const avatarX = pad + 10
|
||||
const avatarY = 50
|
||||
const avatarRadius = avatarSize / 2
|
||||
const rightStart = avatarX + avatarSize + 28
|
||||
const drawAvatar = () => new Promise((resolve) => {
|
||||
if (avatar && avatar.startsWith('http')) {
|
||||
wx.downloadFile({
|
||||
url: avatar,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2)
|
||||
ctx.clip()
|
||||
ctx.drawImage(res.tempFilePath, avatarX, avatarY, avatarSize, avatarSize)
|
||||
ctx.restore()
|
||||
} else {
|
||||
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
|
||||
}
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
await drawAvatar()
|
||||
ctx.setStrokeStyle('rgba(94,234,212,0.5)')
|
||||
ctx.setLineWidth(2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
// 右侧:昵称 + 个人名片
|
||||
const displayName = (nickname || '').trim() || '创业者'
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(26)
|
||||
ctx.setTextAlign('left')
|
||||
ctx.fillText(displayName, rightStart, avatarY + 36)
|
||||
ctx.setFillStyle('#94A3B8')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('个人名片', rightStart, avatarY + 62)
|
||||
// 分隔线
|
||||
const divY = 168
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.08)')
|
||||
ctx.setLineWidth(1)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(pad, divY)
|
||||
ctx.lineTo(w - pad, divY)
|
||||
ctx.stroke()
|
||||
// 底部四栏:地区 | MBTI,行业 | 职位
|
||||
const labelGray = '#64748B'
|
||||
const valueWhite = '#F1F5F9'
|
||||
const rowH = 52
|
||||
const colW = (w - pad * 2) / 2
|
||||
const truncate = (text, maxW) => {
|
||||
if (!text) return ''
|
||||
ctx.setFontSize(15)
|
||||
let m = ctx.measureText(text)
|
||||
if (m.width <= maxW) return text
|
||||
for (let i = text.length - 1; i > 0; i--) {
|
||||
const t = text.slice(0, i) + '…'
|
||||
if (ctx.measureText(t).width <= maxW) return t
|
||||
}
|
||||
return text[0] + '…'
|
||||
}
|
||||
const maxValW = colW - 16
|
||||
const items = [
|
||||
{ label: '地区', value: truncate((region || '').trim() || '未填写', maxValW), x: pad },
|
||||
{ label: 'MBTI', value: (mbti || '').trim() || '未填写', x: pad + colW },
|
||||
{ label: '行业', value: truncate((industry || '').trim() || '未填写', maxValW), x: pad },
|
||||
{ label: '职位', value: truncate((position || '').trim() || '未填写', maxValW), x: pad + colW },
|
||||
]
|
||||
items.forEach((item, i) => {
|
||||
const row = Math.floor(i / 2)
|
||||
const baseY = divY + 36 + row * rowH
|
||||
ctx.setFillStyle(labelGray)
|
||||
ctx.setFontSize(12)
|
||||
ctx.fillText(item.label, item.x, baseY - 8)
|
||||
ctx.setFillStyle(valueWhite)
|
||||
ctx.setFontSize(15)
|
||||
ctx.fillText(item.value, item.x, baseY + 14)
|
||||
})
|
||||
ctx.draw(true, () => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'shareCardCanvas',
|
||||
destWidth: 500,
|
||||
destHeight: 400,
|
||||
success: (res) => {
|
||||
this.setData({ shareCardPath: res.tempFilePath })
|
||||
},
|
||||
}, this)
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[ShareCard] 生成失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
drawAvatarPlaceholder(ctx, x, y, size, nickname) {
|
||||
ctx.setFillStyle('rgba(94,234,212,0.2)')
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle('#5EEAD4')
|
||||
ctx.setFontSize(size * 0.42)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.fillText((nickname || '?')[0], x + size / 2, y + size / 2 + size * 0.14)
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const nickname = (this.data.nickname || '').trim() || '我'
|
||||
const path = userId
|
||||
? (ref ? `/pages/member-detail/member-detail?id=${userId}&ref=${ref}` : `/pages/member-detail/member-detail?id=${userId}`)
|
||||
: (ref ? `/pages/profile-edit/profile-edit?ref=${ref}` : '/pages/profile-edit/profile-edit')
|
||||
const result = {
|
||||
title: `${nickname}为您分享名片`,
|
||||
path,
|
||||
}
|
||||
if (this.data.shareCardPath) result.imageUrl = this.data.shareCardPath
|
||||
return result
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const nickname = (this.data.nickname || '').trim() || '我'
|
||||
const query = userId
|
||||
? (ref ? `id=${userId}&ref=${ref}` : `id=${userId}`)
|
||||
: (ref ? `ref=${ref}` : '')
|
||||
const result = {
|
||||
title: `${nickname}为您分享名片`,
|
||||
query: query || '',
|
||||
}
|
||||
if (this.data.shareCardPath) result.imageUrl = this.data.shareCardPath
|
||||
return result
|
||||
},
|
||||
|
||||
onNicknameInput(e) { this.setData({ nickname: e.detail.value }) },
|
||||
onNicknameChange(e) { this.setData({ nickname: e.detail.value }) },
|
||||
onRegionInput(e) { this.setData({ region: e.detail.value }) },
|
||||
@@ -116,76 +315,9 @@ Page({
|
||||
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
|
||||
},
|
||||
|
||||
// 点击头像:选择微信头像或从相册选择
|
||||
onAvatarTap() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['使用微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.setData({ showAvatarModal: true })
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.chooseAvatarFromAlbum()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
},
|
||||
|
||||
// 从相册/相机选择头像
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch { reject(new Error('解析失败')) }
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const rawUrl1 = uploadRes.data.url || ''
|
||||
const avatarUrl = rawUrl1.startsWith('http://') || rawUrl1.startsWith('https://') ? rawUrl1 : app.globalData.baseUrl + rawUrl1
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
})
|
||||
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()
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// 微信原生 chooseAvatar 回调:使用当前微信头像
|
||||
// 微信原生 chooseAvatar 回调(点击头像直接弹出原生选择器:用微信头像/从相册选择/拍照)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
@@ -209,13 +341,16 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
const rawUrl2 = uploadRes.data.url || ''
|
||||
const avatarUrl = rawUrl2.startsWith('http://') || rawUrl2.startsWith('https://') ? rawUrl2 : app.globalData.baseUrl + rawUrl2
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
this.setData({ avatar: avatarUrl })
|
||||
const avatarToSave = toAvatarPath(avatarUrl)
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarToSave },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
@@ -223,6 +358,7 @@ Page({
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} catch (err) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: err.message || '上传失败,请重试', icon: 'none' })
|
||||
@@ -235,20 +371,32 @@ Page({
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const s = (v) => (v || '').toString().trim()
|
||||
const isVip = this.data.isVip
|
||||
// 手机号必填,格式校验(支持带空格/连字符输入)
|
||||
const phoneRaw = s(this.data.phone)
|
||||
if (!phoneRaw) {
|
||||
wx.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const phoneNum = phoneRaw.replace(/\D/g, '')
|
||||
if (!/^1[3-9]\d{9}$/.test(phoneNum)) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const phoneToSave = phoneNum
|
||||
this.setData({ saving: true })
|
||||
try {
|
||||
const s = (v) => (v || '').toString().trim()
|
||||
const isVip = this.data.isVip
|
||||
const payload = {
|
||||
userId,
|
||||
avatar: s(this.data.avatar),
|
||||
avatar: toAvatarPath(s(this.data.avatar)),
|
||||
nickname: s(this.data.nickname),
|
||||
mbti: s(this.data.mbti),
|
||||
region: s(this.data.region),
|
||||
industry: s(this.data.industry),
|
||||
businessScale: s(this.data.businessScale),
|
||||
position: s(this.data.position),
|
||||
phone: s(this.data.phone),
|
||||
phone: phoneToSave,
|
||||
wechatId: s(this.data.wechatId),
|
||||
}
|
||||
const showHelp = isVip || this.data.helpOffer || this.data.helpNeed
|
||||
@@ -271,7 +419,7 @@ Page({
|
||||
this.setData({ saving: false })
|
||||
return
|
||||
}
|
||||
await app.request({
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
@@ -279,7 +427,7 @@ Page({
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
if (app.globalData.userInfo) {
|
||||
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
|
||||
if (payload.avatar) app.globalData.userInfo.avatar = payload.avatar
|
||||
if (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
setTimeout(() => getApp().goBackOrToHome(), 800)
|
||||
|
||||
@@ -9,36 +9,39 @@
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 温馨提示 -->
|
||||
<view class="tip-card">
|
||||
<!-- 温馨提示:from=vip 时强化权益说明 -->
|
||||
<view class="tip-card {{fromVip ? 'tip-card-highlight' : ''}}">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">温馨提示:需完善手机号和微信号才能使用提现和找伙伴功能</text>
|
||||
<text class="tip-text">{{fromVip ? '恭喜成为VIP!完善资料后即可使用找伙伴、提现等功能,手机号必填' : '温馨提示:手机号必填,微信号建议填写,以便使用提现和找伙伴功能'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<!-- 头像:点击直接弹出微信原生选择器(用微信头像/从相册选择/拍照) -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<text class="avatar-change">更换头像</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap">
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
@@ -84,8 +87,8 @@
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
<text class="form-label">手机号<text class="required-mark">*</text></text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号(必填)" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
@@ -146,14 +149,16 @@
|
||||
<view class="bottom-space"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 头像弹窗:通过 button 获取微信头像 -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<text class="avatar-modal-title">使用微信头像</text>
|
||||
<text class="avatar-modal-desc">点击下方按钮,一键同步当前微信头像</text>
|
||||
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
|
||||
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
|
||||
<!-- 分享名片 canvas(隐藏,用于生成分享图 5:4) -->
|
||||
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
|
||||
|
||||
<!-- 隐私授权弹窗(昵称需授权后方可唤起微信昵称选择器) -->
|
||||
<view class="privacy-mask" wx:if="{{showPrivacyModal}}" catchtouchmove="preventMove">
|
||||
<view class="privacy-modal">
|
||||
<text class="privacy-title">温馨提示</text>
|
||||
<text class="privacy-desc">为获取微信昵称,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-btn" class="privacy-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacy">同意</button>
|
||||
<view class="privacy-cancel" bindtap="handleDisagreePrivacy">拒绝</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -26,10 +26,20 @@
|
||||
background: rgba(94,234,212,0.08); border: 1rpx solid rgba(94,234,212,0.25);
|
||||
border-radius: 24rpx; margin-bottom: 48rpx;
|
||||
}
|
||||
.tip-card-highlight {
|
||||
background: rgba(94,234,212,0.12); border-color: rgba(94,234,212,0.4);
|
||||
}
|
||||
.tip-icon { font-size: 40rpx; color: #5EEAD4; flex-shrink: 0; }
|
||||
.tip-text { font-size: 26rpx; color: rgba(94,234,212,0.95); line-height: 1.6; }
|
||||
|
||||
.avatar-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 48rpx; }
|
||||
/* 头像按钮:透明无边框,点击直接弹出微信原生选择器 */
|
||||
.avatar-wrap-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
width: 192rpx; height: 192rpx; border-radius: 50%; overflow: visible;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap {
|
||||
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%;
|
||||
border: 4rpx solid #5EEAD4; box-shadow: 0 0 30rpx rgba(94,234,212,0.3);
|
||||
@@ -62,6 +72,7 @@
|
||||
.form-row-2 { display: flex; gap: 24rpx; }
|
||||
.form-row-2 .form-item { flex: 1; min-width: 0; }
|
||||
.form-label { display: block; font-size: 24rpx; color: #94A3B8; margin-bottom: 12rpx; margin-left: 8rpx; }
|
||||
.required-mark { color: #F87171; margin-left: 4rpx; }
|
||||
|
||||
/* input/textarea 用 view 包裹,padding 写在 view 上 */
|
||||
.form-input-wrap {
|
||||
@@ -70,10 +81,37 @@
|
||||
border-radius: 24rpx;
|
||||
box-sizing: border-box; min-width: 0; width: 100%;
|
||||
}
|
||||
.form-input-suffix { position: relative; padding-right: 64rpx; }
|
||||
/* 地区等带后缀图标的输入框:与 MBTI 同高,图标垂直居中 */
|
||||
.form-input-suffix {
|
||||
position: relative;
|
||||
padding-right: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
.form-input-suffix .form-input-inner {
|
||||
flex: 1;
|
||||
min-height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
.form-input-suffix .form-suffix {
|
||||
position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
|
||||
font-size: 32rpx; color: #94A3B8;
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32rpx;
|
||||
color: #94A3B8;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* form-row-2 下两列统一最小高度,与 MBTI 一致 */
|
||||
.form-row-2 .form-input-wrap,
|
||||
.form-row-2 .form-picker {
|
||||
min-height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-row-2 .form-picker {
|
||||
color: #fff;
|
||||
}
|
||||
.form-input-inner {
|
||||
width: 100%; max-width: 100%; font-size: 28rpx; color: #fff; background: transparent;
|
||||
@@ -175,9 +213,123 @@
|
||||
.btn-choose-avatar::after {
|
||||
border: none;
|
||||
}
|
||||
.btn-choose-album {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(94, 234, 212, 0.15);
|
||||
color: #5EEAD4;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: 2rpx solid #5EEAD4;
|
||||
}
|
||||
.avatar-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 昵称隐私弹窗 */
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 560rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
background: #1E293B;
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-modal .privacy-title { font-size: 34rpx; font-weight: 600; color: #fff; margin-bottom: 24rpx; }
|
||||
.privacy-modal .privacy-desc { font-size: 28rpx; color: #94A3B8; line-height: 1.6; margin-bottom: 40rpx; }
|
||||
.privacy-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #5EEAD4;
|
||||
color: #0F172A;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 隐私授权弹窗 */
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 580rpx;
|
||||
background: #1E293B;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
}
|
||||
.privacy-modal-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #F8FAFC;
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-modal-desc {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.privacy-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #5EEAD4;
|
||||
color: #0F172A;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.privacy-btn:last-of-type {
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
color: #94A3B8;
|
||||
border: 2rpx solid #475569;
|
||||
}
|
||||
|
||||
/* 昵称隐私授权弹窗(解决 errno:104) */
|
||||
.privacy-mask {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999;
|
||||
display: flex; align-items: center; justify-content: center; padding: 48rpx;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 100%; max-width: 600rpx; background: #1E293B; border-radius: 24rpx; padding: 48rpx;
|
||||
}
|
||||
.privacy-title { font-size: 36rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
|
||||
.privacy-desc { font-size: 28rpx; color: #94A3B8; line-height: 1.5; display: block; margin-bottom: 32rpx; }
|
||||
.privacy-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: #5EEAD4; color: #050B14; font-size: 30rpx; font-weight: 600; border-radius: 44rpx; border: none; }
|
||||
.privacy-btn::after { border: none; }
|
||||
.privacy-cancel { text-align: center; margin-top: 24rpx; font-size: 28rpx; color: #94A3B8; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 个人资料展示页(stitch_soul enhanced_professional_profile)
|
||||
* 卡若创业派对 - 个人资料展示页(stitch_soul enhanced_professional_profile)
|
||||
* 从「我的」页编辑图标进入;展示基本信息、个人故事、互助需求、项目介绍
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
@@ -66,13 +66,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 购买记录',
|
||||
title: '卡若创业派对 - 购买记录',
|
||||
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 购买记录', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 购买记录', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 阅读页(标准流程版)
|
||||
* 卡若创业派对 - 阅读页(标准流程版)
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*
|
||||
@@ -74,7 +74,7 @@ Page({
|
||||
giftPaid: false,
|
||||
giftRequestSn: '',
|
||||
showLoginModal: false,
|
||||
agreeProtocol: false,
|
||||
showPrivacyModal: false,
|
||||
showPosterModal: false,
|
||||
isPaying: false,
|
||||
isGeneratingPoster: false,
|
||||
@@ -651,21 +651,21 @@ Page({
|
||||
return
|
||||
}
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
try {
|
||||
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
|
||||
if (profileRes?.success && profileRes.data) {
|
||||
phone = (profileRes.data.phone || '').trim()
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
|
||||
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!phone && !wechatId) {
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号或微信号,以便对方联系您',
|
||||
content: '请先填写手机号(必填),以便对方联系您',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
@@ -866,11 +866,11 @@ Page({
|
||||
copyShareText() {
|
||||
const { section } = this.data
|
||||
|
||||
const shareText = `🔥 刚看完这篇《${section?.title || 'Soul创业派对'}》,太上头了!
|
||||
const shareText = `🔥 刚看完这篇《${section?.title || '卡若创业派对'}》,太上头了!
|
||||
|
||||
62个真实商业案例,每个都是从0到1的实战经验。私域运营、资源整合、商业变现,干货满满。
|
||||
|
||||
推荐给正在创业或想创业的朋友,搜"Soul创业派对"小程序就能看!
|
||||
推荐给正在创业或想创业的朋友,搜"卡若创业派对"小程序就能看!
|
||||
|
||||
#创业派对 #私域运营 #商业案例`
|
||||
|
||||
@@ -894,7 +894,7 @@ Page({
|
||||
if (isGiftShare && requestSn) {
|
||||
let path = `/pages/read/read?${q}&gift=1&requestSn=${encodeURIComponent(requestSn)}`
|
||||
if (ref) path += `&ref=${encodeURIComponent(ref)}`
|
||||
const t = section?.title || 'Soul创业派对'
|
||||
const t = section?.title || '卡若创业派对'
|
||||
const title = `我已为你买单:${t.length > 18 ? t.slice(0, 18) + '...' : t}`
|
||||
return { title, path }
|
||||
}
|
||||
@@ -902,7 +902,7 @@ Page({
|
||||
const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
|
||||
const title = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 Soul创业派对 - 真实商业故事'
|
||||
: '📚 卡若创业派对 - 真实商业故事'
|
||||
return { title, path }
|
||||
},
|
||||
|
||||
@@ -925,7 +925,7 @@ Page({
|
||||
.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 = picked.length > 0 ? title + '\n\n' + picked.join('\n\n') : `🔥 刚看完这篇《${title}》,推荐给你!\n\n#Soul创业派对 #真实商业故事`
|
||||
const copyText = picked.length > 0 ? title + '\n\n' + picked.join('\n\n') : `🔥 刚看完这篇《${title}》,推荐给你!\n\n#卡若创业派对 #真实商业故事`
|
||||
wx.setClipboardData({
|
||||
data: copyText,
|
||||
success: () => {
|
||||
@@ -951,7 +951,7 @@ Page({
|
||||
const articleTitle = (section?.title || chapterTitle || '').trim()
|
||||
const title = articleTitle
|
||||
? (articleTitle.length > 28 ? articleTitle.slice(0, 28) + '...' : articleTitle)
|
||||
: 'Soul创业派对 - 真实商业故事'
|
||||
: '卡若创业派对 - 真实商业故事'
|
||||
return { title, query }
|
||||
},
|
||||
|
||||
@@ -974,68 +974,23 @@ Page({
|
||||
console.warn('[Read] 检测单页模式失败,回退为正常登录流程:', e)
|
||||
}
|
||||
try {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
this.setData({ showLoginModal: true })
|
||||
} catch (e) {
|
||||
console.error('[Read] showLoginModal error:', e)
|
||||
this.setData({ showLoginModal: true })
|
||||
}
|
||||
},
|
||||
|
||||
closeLoginModal() {
|
||||
onLoginModalClose() {
|
||||
this.setData({ showLoginModal: false, showPrivacyModal: false })
|
||||
},
|
||||
onLoginModalPrivacyAgree() {
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
async onLoginModalSuccess() {
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
},
|
||||
|
||||
openUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
},
|
||||
|
||||
openPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
},
|
||||
|
||||
// 从服务端刷新购买状态,避免登录后误用旧数据导致误解锁
|
||||
// 【重构】微信登录(须先勾选同意协议,符合审核要求)
|
||||
async handleWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (!result) return
|
||||
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
await this.onLoginSuccess()
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Read] 登录失败:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 【重构】手机号登录(标准流程)
|
||||
async handlePhoneLogin(e) {
|
||||
if (!e.detail.code) {
|
||||
return this.handleWechatLogin()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
if (!result) return
|
||||
|
||||
this.setData({ showLoginModal: false })
|
||||
await this.onLoginSuccess()
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Read] 手机号登录失败:', e)
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
await this.onLoginSuccess()
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
},
|
||||
|
||||
// 【新增】登录成功后的标准处理流程
|
||||
@@ -1537,7 +1492,7 @@ Page({
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul创业派对', 20, 35)
|
||||
ctx.fillText('📚 卡若创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--pages/read/read.wxml-->
|
||||
<!--Soul创业派对 - 阅读页-->
|
||||
<!--卡若创业派对 - 阅读页-->
|
||||
<view class="page">
|
||||
<!-- 阅读进度条 -->
|
||||
<view class="progress-bar-fixed" style="top: {{statusBarHeight}}px;">
|
||||
@@ -131,7 +131,7 @@
|
||||
<text class="paywall-desc">已阅读50%,登录后查看完整内容</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">立即登录</text>
|
||||
<text class="login-btn-text">手机号一键登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -333,28 +333,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗 - 须勾选同意协议,《用户协议》《隐私政策》可点击查看 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
</button>
|
||||
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 登录弹窗(公用组件) -->
|
||||
<login-modal
|
||||
show="{{showLoginModal}}"
|
||||
desc="登录后可购买章节、解锁更多内容"
|
||||
showPrivacyModal="{{showPrivacyModal}}"
|
||||
bind:close="onLoginModalClose"
|
||||
bind:success="onLoginModalSuccess"
|
||||
bind:privacyagree="onLoginModalPrivacyAgree"
|
||||
/>
|
||||
|
||||
<!-- 支付中提示 -->
|
||||
<view class="modal-overlay" wx:if="{{isPaying}}" catchtap="">
|
||||
|
||||
@@ -1164,6 +1164,10 @@
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
|
||||
/* ===== 支付中加载 ===== */
|
||||
.loading-box {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 分销中心页
|
||||
* 卡若创业派对 - 分销中心页
|
||||
*
|
||||
* 可见数据:
|
||||
* - 绑定用户数(当前有效绑定)
|
||||
@@ -590,25 +590,25 @@ Page({
|
||||
shareToMoments() {
|
||||
// 10条随机文案,基于书的内容
|
||||
const shareTexts = [
|
||||
`🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"Soul创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
|
||||
`🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
|
||||
|
||||
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《Soul创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
|
||||
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《卡若创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
|
||||
|
||||
`📚 一个70后大健康私域,一个月150万流水是怎么做到的?\n\n答案在《Soul创业派对》第9章,全是干货。\n\n搜小程序"Soul创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`,
|
||||
`📚 一个70后大健康私域,一个月150万流水是怎么做到的?\n\n答案在《卡若创业派对》第9章,全是干货。\n\n搜小程序"卡若创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`,
|
||||
|
||||
`🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《Soul创业派对》,创业者必读!\n\n#云阿米巴 #商业思维 #创业`,
|
||||
`🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《卡若创业派对》,创业者必读!\n\n#云阿米巴 #商业思维 #创业`,
|
||||
|
||||
`✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《Soul创业派对》,每章都是实战经验\n\n#资源整合 #社交 #创业故事`,
|
||||
`✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《卡若创业派对》,每章都是实战经验\n\n#资源整合 #社交 #创业故事`,
|
||||
|
||||
`🚀 AI工具推广:一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《Soul创业派对》里的商业机会,你发现了吗?\n\n#AI #副业 #商业机会`,
|
||||
`🚀 AI工具推广:一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《卡若创业派对》里的商业机会,你发现了吗?\n\n#AI #副业 #商业机会`,
|
||||
|
||||
`💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《Soul创业派对》告诉你答案!\n\n#美业 #轻创业 #月入十万`,
|
||||
`💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《卡若创业派对》告诉你答案!\n\n#美业 #轻创业 #月入十万`,
|
||||
|
||||
`🌟 3000万流水是怎么跑出来的?\n\n不是靠运气,是靠系统。\n\n《Soul创业派对》里的电商底层逻辑,值得反复看\n\n#电商 #创业 #商业系统`,
|
||||
`🌟 3000万流水是怎么跑出来的?\n\n不是靠运气,是靠系统。\n\n《卡若创业派对》里的电商底层逻辑,值得反复看\n\n#电商 #创业 #商业系统`,
|
||||
|
||||
`📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句,都在《Soul创业派对》里\n\n#人性 #商业 #创业派对`,
|
||||
`📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句,都在《卡若创业派对》里\n\n#人性 #商业 #创业派对`,
|
||||
|
||||
`🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《Soul创业派对》帮你找到答案!\n\n#职业规划 #创业 #未来`
|
||||
`🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《卡若创业派对》帮你找到答案!\n\n#职业规划 #创业 #未来`
|
||||
]
|
||||
|
||||
// 随机选择一条文案
|
||||
@@ -892,7 +892,7 @@ Page({
|
||||
const ref = this.data.referralCode || app.getMyReferralCode()
|
||||
console.log('[Referral] 分享给好友,推荐码:', ref)
|
||||
return {
|
||||
title: 'Soul创业派对 - 来自派对房的真实商业故事',
|
||||
title: '卡若创业派对 - 来自派对房的真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
// 不设置 imageUrl,使用小程序默认截图
|
||||
// 如需自定义图片,请将图片放在 /assets/ 目录并配置路径
|
||||
@@ -904,7 +904,7 @@ Page({
|
||||
const ref = this.data.referralCode || app.getMyReferralCode()
|
||||
console.log('[Referral] 分享到朋友圈,推荐码:', ref)
|
||||
return {
|
||||
title: `Soul创业派对 - 62个真实商业案例`,
|
||||
title: `卡若创业派对 - 62个真实商业案例`,
|
||||
query: ref ? `ref=${ref}` : ''
|
||||
// 不设置 imageUrl,使用小程序默认截图
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
<view class="user-avatar {{item.status === 'converted' ? 'avatar-converted' : item.status === 'expired' ? 'avatar-expired' : ''}}">
|
||||
<icon wx:if="{{item.status === 'converted'}}" name="check" size="28" color="#34C759"></icon>
|
||||
<icon wx:elif="{{item.status === 'expired'}}" name="clock" size="28" color="#ff9500"></icon>
|
||||
<text wx:else>{{item.nickname[0] || '用'}}</text>
|
||||
<text wx:else>{{(item.nickname && item.nickname[0]) || '用'}}</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{item.nickname || '匿名用户'}}</text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 章节搜索页
|
||||
* 卡若创业派对 - 章节搜索页
|
||||
* 搜索章节标题和内容
|
||||
*/
|
||||
const app = getApp()
|
||||
@@ -129,13 +129,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 搜索',
|
||||
title: '卡若创业派对 - 搜索',
|
||||
path: ref ? `/pages/search/search?ref=${ref}` : '/pages/search/search'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 搜索', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 搜索', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Soul创业派对 - 设置页
|
||||
* 卡若创业派对 - 设置页
|
||||
* 账号绑定功能
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -29,7 +30,8 @@ Page({
|
||||
// 绑定弹窗
|
||||
showBindModal: false,
|
||||
bindType: '', // phone | wechat | alipay
|
||||
bindValue: ''
|
||||
bindValue: '',
|
||||
showPrivacyModal: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -245,70 +247,101 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 微信原生 chooseAvatar 回调
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
// 获取微信头像(新版授权)
|
||||
async getWechatAvatar() {
|
||||
try {
|
||||
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('解析响应失败'))
|
||||
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)
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
// 2. 获取上传后的完整URL(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
})
|
||||
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
await app.request('/api/miniprogram/user/profile', {
|
||||
method: 'POST',
|
||||
data: { userId, nickname, avatar: avatarUrl }
|
||||
console.log('[Settings] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地
|
||||
this.setData({
|
||||
userInfo: {
|
||||
...this.data.userInfo,
|
||||
nickname: 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: toAvatarPath(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'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
onAgreePrivacyForPhone() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-privacy-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 一键获取微信手机号(button组件回调)
|
||||
async onGetPhoneNumber(e) {
|
||||
console.log('[Settings] 获取手机号回调:', e.detail)
|
||||
@@ -369,6 +402,11 @@ Page({
|
||||
this.setData({ showBindModal: false })
|
||||
},
|
||||
|
||||
// 跳转账户密码登录页(开发)
|
||||
goToDevLogin() {
|
||||
wx.navigateTo({ url: '/pages/dev-login/dev-login' })
|
||||
},
|
||||
|
||||
// 打开切换账号弹窗(开发)
|
||||
openSwitchAccountModal() {
|
||||
this.setData({
|
||||
@@ -488,13 +526,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 设置',
|
||||
title: '卡若创业派对 - 设置',
|
||||
path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 设置', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 设置', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,11 +32,15 @@
|
||||
</view>
|
||||
<view class="bind-right">
|
||||
<icon wx:if="{{phoneNumber}}" name="check" size="36" color="#34C759" customClass="bind-check"></icon>
|
||||
<button wx:else class="get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
|
||||
<button wx:else id="agree-settings-phone-btn" class="get-phone-btn" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onGetPhoneNumber" bindagreeprivacyauthorization="onAgreePrivacyForPhone">
|
||||
一键获取
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacyForPhone">同意</button>
|
||||
</view>
|
||||
|
||||
<!-- 微信号 - 简化输入 -->
|
||||
<view class="bind-item">
|
||||
@@ -117,6 +121,13 @@
|
||||
<text class="dev-switch-desc">输入 userId 切换为其他账号调试</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="goToDevLogin">
|
||||
<view class="dev-switch-inner">
|
||||
<icon name="smartphone" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
|
||||
<text class="dev-switch-text">账户密码登录</text>
|
||||
<text class="dev-switch-desc">输入对方手机号登录,密码可留空</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" wx:if="{{isLoggedIn}}" bindtap="handleLogout">退出登录</view>
|
||||
</view>
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
line-height: normal;
|
||||
}
|
||||
.get-phone-btn::after { border: none; }
|
||||
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
|
||||
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
|
||||
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
|
||||
.privacy-agree-btn::after { border: none; }
|
||||
|
||||
/* 自动提现卡片 */
|
||||
.auto-withdraw-card { margin-top: 24rpx; }
|
||||
|
||||
@@ -85,20 +85,7 @@ Page({
|
||||
return
|
||||
}
|
||||
}
|
||||
// 支付前:若头像/昵称仍为默认值,引导先完善(仅头像+昵称)
|
||||
if (this._shouldGuideAvatarNickname()) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '开通超级个体前,请先设置头像和昵称,让他人更好地认识你',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// VIP 购买后才引导完善资料:购买前不拦截,购买成功后跳转 profile-edit
|
||||
this.setData({ purchasing: true })
|
||||
const amount = this.data.price
|
||||
try {
|
||||
@@ -173,23 +160,21 @@ Page({
|
||||
else if (typeof p.updateUserStatus === 'function') p.updateUserStatus()
|
||||
})
|
||||
|
||||
// 开通成功后兜底:仍为默认头像/昵称则引导完善
|
||||
if (this._shouldGuideAvatarNickname()) {
|
||||
wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
}
|
||||
// 超级个体购买后:弹窗提示,强制跳转资料编辑页
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '为了更好为您服务,请填写好资料',
|
||||
confirmText: '去完善',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit?from=vip' })
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[VIP] 支付后同步失败:', e)
|
||||
wx.hideLoading()
|
||||
}
|
||||
wx.hideLoading()
|
||||
},
|
||||
|
||||
_shouldGuideAvatarNickname() {
|
||||
const user = app.globalData.userInfo || {}
|
||||
const avatar = (user.avatar || user.avatarUrl || '').trim()
|
||||
const nickname = (user.nickname || user.nickName || '').trim()
|
||||
// 与 ruleEngine.checkRule_FillAvatar 保持同口径(允许前端兜底)
|
||||
if (avatar && !avatar.includes('default') && nickname && nickname !== '微信用户' && !nickname.startsWith('微信用户')) return false
|
||||
return true
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
@@ -197,13 +182,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - VIP会员',
|
||||
title: '卡若创业派对 - VIP会员',
|
||||
path: ref ? `/pages/vip/vip?ref=${ref}` : '/pages/vip/vip'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - VIP会员', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - VIP会员', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Soul创业派对 - 我的余额 -->
|
||||
<!-- 卡若创业派对 - 我的余额 -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Soul创业派对 - 我的余额 - 深色主题 */
|
||||
/* 卡若创业派对 - 我的余额 - 深色主题 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #0a0a0a;
|
||||
|
||||
@@ -125,13 +125,13 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 提现记录',
|
||||
title: '卡若创业派对 - 提现记录',
|
||||
path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 提现记录', query: ref ? `ref=${ref}` : '' }
|
||||
return { title: '卡若创业派对 - 提现记录', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compileType": "miniprogram",
|
||||
"miniprogramRoot": "",
|
||||
"description": "Soul创业派对 - 来自派对房的真实商业故事",
|
||||
"description": "卡若创业派对 - 来自派对房的真实商业故事",
|
||||
"appid": "wxb8bbb2b10dec74aa",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
|
||||
@@ -24,12 +24,40 @@
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "pages/gift-pay/list",
|
||||
"pathName": "pages/gift-pay/list",
|
||||
"name": "开发登录",
|
||||
"pathName": "pages/dev-login/dev-login",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "pages/member-detail/member-detail",
|
||||
"pathName": "pages/member-detail/member-detail",
|
||||
"query": "id=ogpTW5cVMxd5afBBtXdvmeMO8aho",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/my/my",
|
||||
"pathName": "pages/my/my",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "个人资料",
|
||||
"pathName": "pages/avatar-nickname/avatar-nickname",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/gift-pay/list",
|
||||
"pathName": "pages/gift-pay/list",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "代付",
|
||||
"pathName": "pages/gift-pay/detail",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 内容解析工具
|
||||
* 卡若创业派对 - 内容解析工具
|
||||
* 解析 TipTap HTML 为阅读页可展示的 segments
|
||||
*
|
||||
* segment 类型:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Soul创业派对 - 用户旅程规则引擎
|
||||
* 卡若创业派对 - 用户旅程规则引擎
|
||||
* 从后端 /api/miniprogram/user-rules 读取启用的规则,按场景触发引导
|
||||
* 稳定版兼容:readCount 用 getReadCount(),hasPurchasedFull 用 hasFullBook,完善头像跳 avatar-nickname
|
||||
*
|
||||
* trigger → scene 映射:
|
||||
* 注册 → after_login
|
||||
@@ -15,7 +16,14 @@
|
||||
* 浏览导师页 → browse_mentor
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
function getAppInstance() {
|
||||
try {
|
||||
const a = getApp()
|
||||
return a && a.globalData ? a : null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const RULE_COOLDOWN_KEY = 'rule_engine_cooldown'
|
||||
const COOLDOWN_MS = 60 * 1000
|
||||
@@ -59,11 +67,14 @@ function setCooldown(ruleId) {
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
return app.globalData.userInfo || {}
|
||||
const app = getAppInstance()
|
||||
return app ? (app.globalData.userInfo || {}) : {}
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
if (_cachedRules && Date.now() - _cacheTs < CACHE_TTL) return _cachedRules
|
||||
const app = getAppInstance()
|
||||
if (!app) return _cachedRules || []
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/user-rules', method: 'GET', silent: true })
|
||||
if (res && res.success && res.rules) {
|
||||
@@ -85,8 +96,12 @@ function getRuleInfo(rules, triggerName) {
|
||||
return rules.find(r => r.trigger === triggerName)
|
||||
}
|
||||
|
||||
// 稳定版:跳转 avatar-nickname(专注头像+昵称,首次登录由 app.login 强制 redirect)
|
||||
// VIP 用户不触发:统一由 checkVipContactRequiredAndGuide 引导到 profile-edit,避免与主流程冲突
|
||||
function checkRule_FillAvatar(rules) {
|
||||
if (!isRuleEnabled(rules, '注册')) return null
|
||||
const app = getAppInstance()
|
||||
if (app && app.globalData.isVip) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const avatar = user.avatar || user.avatarUrl || ''
|
||||
@@ -100,7 +115,7 @@ function checkRule_FillAvatar(rules) {
|
||||
title: info?.title || '完善个人信息',
|
||||
message: info?.description || '设置头像和昵称,让其他创业者更容易认识你',
|
||||
action: 'navigate',
|
||||
target: '/pages/profile-edit/profile-edit'
|
||||
target: '/pages/avatar-nickname/avatar-nickname'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +153,13 @@ function checkRule_FillProfile(rules) {
|
||||
}
|
||||
}
|
||||
|
||||
// 稳定版兼容:readCount 用 getReadCount()
|
||||
function checkRule_ShareAfter5Chapters(rules) {
|
||||
if (!isRuleEnabled(rules, '累计浏览5章节')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const readCount = app.globalData.readCount || 0
|
||||
const app = getAppInstance()
|
||||
const readCount = app ? (typeof app.getReadCount === 'function' ? app.getReadCount() : (app.globalData.readCount || 0)) : 0
|
||||
if (readCount < 5) return null
|
||||
if (isInCooldown('share_after_5')) return null
|
||||
setCooldown('share_after_5')
|
||||
@@ -156,11 +173,13 @@ function checkRule_ShareAfter5Chapters(rules) {
|
||||
}
|
||||
}
|
||||
|
||||
// 稳定版兼容:hasPurchasedFull 用 hasFullBook
|
||||
function checkRule_FillVipInfo(rules) {
|
||||
if (!isRuleEnabled(rules, '完成付款')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
if (!app.globalData.hasPurchasedFull) return null
|
||||
const app = getAppInstance()
|
||||
if (!app || !(app.globalData.hasFullBook || app.globalData.hasPurchasedFull)) return null
|
||||
if (user.wechatId && user.address) return null
|
||||
if (isInCooldown('fill_vip_info')) return null
|
||||
setCooldown('fill_vip_info')
|
||||
@@ -212,7 +231,8 @@ function checkRule_Withdraw(rules) {
|
||||
if (!isRuleEnabled(rules, '收益满50元')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const earnings = app.globalData.totalEarnings || 0
|
||||
const app = getAppInstance()
|
||||
const earnings = app ? (app.globalData.totalEarnings || 0) : 0
|
||||
if (earnings < 50) return null
|
||||
if (isInCooldown('withdraw_50')) return null
|
||||
setCooldown('withdraw_50')
|
||||
@@ -274,6 +294,8 @@ function executeRule(rule, pageInstance) {
|
||||
function _trackRuleAction(ruleId, action) {
|
||||
const userId = getUserInfo().id
|
||||
if (!userId) return
|
||||
const app = getAppInstance()
|
||||
if (!app) return
|
||||
app.request({
|
||||
url: '/api/miniprogram/track',
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Soul创业派对 - 小程序码 scene 参数统一编解码(海报生成 ↔ 扫码解析闭环)
|
||||
* 卡若创业派对 - 小程序码 scene 参数统一编解码(海报生成 ↔ 扫码解析闭环)
|
||||
* 官方以 options.scene 接收扫码参数;后端生成码时会把 & 转为 _,故解析时同时支持 & 和 _
|
||||
* scene 同时可带两个参数:章节标识(mid/id) + 推荐人(ref)
|
||||
*/
|
||||
|
||||
@@ -171,6 +171,20 @@ const showConfirm = (title, content) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从头像 URL 提取路径部分(不含域名),用于保存到后端
|
||||
* 例如:https://xxx.com/uploads/avatars/1.jpg → /uploads/avatars/1.jpg
|
||||
* @param {string} url - 完整 URL 或路径
|
||||
* @returns {string}
|
||||
*/
|
||||
const toAvatarPath = url => {
|
||||
if (!url || typeof url !== 'string') return url || ''
|
||||
const idx = url.indexOf('/uploads/')
|
||||
if (idx >= 0) return url.substring(idx)
|
||||
if (url.startsWith('/')) return url
|
||||
return url
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime,
|
||||
formatDate,
|
||||
@@ -188,5 +202,6 @@ module.exports = {
|
||||
showToast,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showConfirm
|
||||
showConfirm,
|
||||
toAvatarPath
|
||||
}
|
||||
|
||||
94
miniprogram/模拟测试清单.md
Normal file
94
miniprogram/模拟测试清单.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Soul 小程序 - 模拟测试清单
|
||||
|
||||
在**微信开发者工具**中打开 `miniprogram` 项目,按以下步骤逐项验证。
|
||||
|
||||
---
|
||||
|
||||
## 一、前置准备
|
||||
|
||||
1. 启动微信开发者工具,打开项目 `e:\Gongsi\Mycontent\miniprogram`
|
||||
2. 确认后端 soul-api 已启动(baseUrl 指向正确,如 `http://localhost:8080`)
|
||||
3. 准备测试账号:① 普通用户(未完善) ② VIP 用户(无手机号) ③ 已完善用户
|
||||
|
||||
---
|
||||
|
||||
## 二、VIP 无手机号 / 头像未改(先判断 VIP)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 2.1 | 用 **VIP 且无手机号** 的账号登录 | 登录成功 |
|
||||
| 2.2 | 完全关闭小程序,重新编译并启动 | 约 1.5s 后弹窗「VIP会员需完善手机号...」→ 点击「去完善」→ redirectTo profile-edit |
|
||||
| 2.3 | 用 **VIP 有手机号但头像/昵称未改**(旧数据) | 弹窗「为了更好为您服务,请完善资料」→ redirectTo profile-edit |
|
||||
| 2.4 | 在资料页填写并保存手机号 | 保存成功,可返回首页 |
|
||||
| 2.5 | 再次关闭并重新启动 | 不再弹窗或跳转,正常进入首页 |
|
||||
|
||||
---
|
||||
|
||||
## 三、头像/昵称引导(非 VIP 老用户)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 3.1 | 用 **非 VIP、头像/昵称仍为默认** 的账号冷启动 | 先获取 VIP 状态 → 非 VIP → 弹窗「请设置头像和昵称」,每日最多 1 次 |
|
||||
| 3.2 | 点击「去完善」 | navigateTo avatar-nickname |
|
||||
| 3.3 | 点击「稍后」 | 弹窗关闭,可正常使用 |
|
||||
| 3.4 | **新注册用户**(isNewUser)且头像未改 | 无弹窗,直接 redirectTo avatar-nickname |
|
||||
| 3.5 | 当前已在 profile-edit 或 avatar-nickname 页 | 不再重复弹窗或跳转 |
|
||||
|
||||
---
|
||||
|
||||
## 四、检测顺序(VIP 优先)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 4.1 | 用 **VIP 无手机号 + 头像默认** 账号登录 | 先执行 VIP 检测 → 直接 redirectTo profile-edit |
|
||||
| 4.2 | 验证不会先到头像页 | VIP 优先,直接进 profile-edit,不会先到 avatar-nickname |
|
||||
|
||||
---
|
||||
|
||||
## 五、VIP 购买流程
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 5.1 | 进入 VIP 页点击「开通」并完成购买 | 购买前不拦截;购买成功后弹窗说明,确认后跳转 profile-edit?from=vip |
|
||||
| 5.2 | VIP 购买成功后点击「去完善」 | 跳转到 profile-edit(含手机号必填) |
|
||||
| 5.3 | 在 profile-edit 不填手机号直接保存(VIP 账号) | 提示「请输入手机号(VIP会员必填)」 |
|
||||
| 5.4 | 填写正确手机号后保存 | 保存成功 |
|
||||
|
||||
---
|
||||
|
||||
## 六、微信号引导(非强制)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 6.1 | 用 **VIP 有手机号但无微信号** 账号登录 | 弹窗「请到资料页完善微信号」 |
|
||||
| 6.2 | 点击「去完善」或「稍后」 | 去完善→跳转;稍后→关闭 |
|
||||
|
||||
---
|
||||
|
||||
## 七、快捷自检命令(开发者工具控制台)
|
||||
|
||||
在**调试器 → Console** 中可粘贴以下代码快速模拟状态:
|
||||
|
||||
```javascript
|
||||
// 查看当前 userInfo
|
||||
getApp().globalData.userInfo
|
||||
|
||||
// 手动触发检测(需已登录)
|
||||
getApp().checkVipContactRequiredAndGuide()
|
||||
getApp().checkAvatarNicknameAndGuide()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题排查
|
||||
|
||||
| 现象 | 可能原因 |
|
||||
|------|----------|
|
||||
| 未触发 VIP redirect | 1) 非 VIP 2) 已填手机号 3) 当前已在 profile-edit 页 |
|
||||
| 头像未改却未强制跳转 | 检查当前页是否为 profile-edit 或 avatar-nickname(会跳过) |
|
||||
| 后端请求失败 | 确认 soul-api 已启动,baseUrl 正确 |
|
||||
| 登录态丢失 | 检查 token、userInfo 是否持久化 |
|
||||
|
||||
---
|
||||
|
||||
测试完成后,可在本文件末尾记录:✅ 通过 / ❌ 失败及原因。
|
||||
166
scripts/test/web/admin_routes_smoke.py
Normal file
166
scripts/test/web/admin_routes_smoke.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
ROUTER_GO = r"e:\\Gongsi\\Mycontent\\soul-api\\internal\\router\\router.go"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Route:
|
||||
group: str # "admin" | "db" | "root"
|
||||
method: str
|
||||
path: str # path within the group, e.g. "/chapters" or "/admin"
|
||||
full_path: str # full path appended to API_BASE_URL
|
||||
|
||||
|
||||
def _read_text(path: str) -> str:
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def extract_admin_and_db_routes() -> list[tuple[str, str]]:
|
||||
"""
|
||||
返回 [(method, full_path_template), ...]
|
||||
full_path_template 已包含 /api/admin 或 /api/db 前缀,保留 :id 占位符。
|
||||
"""
|
||||
text = _read_text(ROUTER_GO)
|
||||
|
||||
routes: list[tuple[str, str]] = []
|
||||
|
||||
# 1) /api/admin 登录/鉴权/登出(不是 admin group 内)
|
||||
# api.GET("/admin", ...) / api.POST("/admin", ...) / api.POST("/admin/logout", ...)
|
||||
for m in re.finditer(r'api\.(GET|POST|PUT|DELETE)\("(/admin(?:/[^"]*)?)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api{m.group(2)}"))
|
||||
|
||||
# 2) admin group:api.Group("/admin") + admin.(GET|POST|PUT|DELETE)("/xxx", ...)
|
||||
for m in re.finditer(r'admin\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/admin{m.group(2)}"))
|
||||
|
||||
# 3) db group:api.Group("/db") + db.(GET|POST|PUT|DELETE)("/xxx", ...)
|
||||
for m in re.finditer(r'db\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/db{m.group(2)}"))
|
||||
|
||||
# 去重(同一 handler 可能存在重复注册)
|
||||
seen: set[tuple[str, str]] = set()
|
||||
out: list[tuple[str, str]] = []
|
||||
for method, p in routes:
|
||||
k = (method, p)
|
||||
if k in seen:
|
||||
continue
|
||||
seen.add(k)
|
||||
out.append((method, p))
|
||||
return out
|
||||
|
||||
|
||||
def replace_path_params(path: str) -> str:
|
||||
# 仅用于 smoke:把 :id 替换成一个固定占位
|
||||
return path.replace(":id", "1")
|
||||
|
||||
|
||||
def request_json(
|
||||
session: requests.Session,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: dict[str, str],
|
||||
payload: Any | None = None,
|
||||
raw_body: str | None = None,
|
||||
) -> tuple[int, dict[str, Any] | None, str]:
|
||||
try:
|
||||
if raw_body is not None:
|
||||
resp = session.request(method, url, headers=headers, data=raw_body, timeout=10)
|
||||
elif payload is None:
|
||||
resp = session.request(method, url, headers=headers, timeout=10)
|
||||
else:
|
||||
resp = session.request(method, url, headers=headers, data=json.dumps(payload), timeout=10)
|
||||
text = resp.text or ""
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
data = None
|
||||
return resp.status_code, data, text[:300]
|
||||
except Exception as e:
|
||||
return 0, None, f"EXC: {e}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
api_base = None
|
||||
# 优先使用本地默认;需要对接测试环境时在 PowerShell 设置 SOUL_API_BASE
|
||||
import os
|
||||
|
||||
api_base = (os.environ.get("SOUL_API_BASE") or "").rstrip("/")
|
||||
if not api_base:
|
||||
# 默认本机
|
||||
api_base = "http://localhost:8080"
|
||||
|
||||
admin_username = os.environ.get("SOUL_ADMIN_USERNAME", "admin")
|
||||
admin_password = os.environ.get("SOUL_ADMIN_PASSWORD", "admin123")
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
# 本 smoke 默认不验证 TLS(如果你用的是 https 且是自签证书,能跑通测试)
|
||||
session.verify = False
|
||||
|
||||
# 登录拿 token
|
||||
login_url = f"{api_base}/api/admin"
|
||||
r = session.post(login_url, json={"username": admin_username, "password": admin_password}, timeout=10)
|
||||
try:
|
||||
login_data = r.json()
|
||||
except Exception:
|
||||
login_data = None
|
||||
if r.status_code != 200 or not (login_data and login_data.get("success") is True and login_data.get("token")):
|
||||
print("LOGIN_FAILED", r.status_code, r.text[:200])
|
||||
return
|
||||
|
||||
token = login_data["token"]
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
|
||||
routes = extract_admin_and_db_routes()
|
||||
print(f"Found routes: {len(routes)}")
|
||||
|
||||
failures: list[dict[str, Any]] = []
|
||||
unexpected_success: list[dict[str, Any]] = []
|
||||
|
||||
for method, path_template in routes:
|
||||
path = replace_path_params(path_template)
|
||||
url = f"{api_base}{path}"
|
||||
|
||||
payload = None
|
||||
raw_body = None
|
||||
if method in ("POST", "PUT", "DELETE"):
|
||||
# 安全模式:发送明显非法 JSON,尽量触发 ShouldBindJSON 失败,避免真实写入。
|
||||
payload = None
|
||||
raw_body = "{invalid_json"
|
||||
|
||||
status, data, preview = request_json(
|
||||
session, method, url, headers, payload=payload, raw_body=raw_body
|
||||
)
|
||||
|
||||
ok = status not in (404, 500) and status != 0
|
||||
# POST/PUT/DELETE 在安全模式下不应返回 success=true
|
||||
if method in ("POST", "PUT", "DELETE") and data and data.get("success") is True:
|
||||
unexpected_success.append(
|
||||
{"method": method, "path": path, "status": status, "data": data, "preview": preview}
|
||||
)
|
||||
|
||||
if not ok:
|
||||
failures.append({"method": method, "path": path, "status": status, "data": data, "preview": preview})
|
||||
|
||||
print("\n=== SMOKE_RESULT ===")
|
||||
print("Failures(404/500/EXC):", len(failures))
|
||||
if failures:
|
||||
for it in failures:
|
||||
print(f"- {it['method']} {it['path']} -> {it['status']}, preview={it.get('preview')}")
|
||||
|
||||
print("\nUnexpected success on write calls:", len(unexpected_success))
|
||||
if unexpected_success:
|
||||
for it in unexpected_success:
|
||||
print(f"- {it['method']} {it['path']} -> success=true (status {it['status']}, preview={it.get('preview')})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
132
scripts/test/web/admin_routes_smoke_authless.py
Normal file
132
scripts/test/web/admin_routes_smoke_authless.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
ROUTER_GO = r"e:\\Gongsi\\Mycontent\\soul-api\\internal\\router\\router.go"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check:
|
||||
method: str
|
||||
path: str
|
||||
status: int
|
||||
preview: str
|
||||
|
||||
|
||||
def _read_text(path: str) -> str:
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def extract_routes() -> list[tuple[str, str]]:
|
||||
"""
|
||||
返回 [(method, full_path_template), ...]
|
||||
full_path_template 保留 :id 占位符。
|
||||
"""
|
||||
text = _read_text(ROUTER_GO)
|
||||
routes: list[tuple[str, str]] = []
|
||||
|
||||
# /api/admin 登录/鉴权/登出
|
||||
for m in re.finditer(r'api\.(GET|POST|PUT|DELETE)\("(/admin(?:/[^"]*)?)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api{m.group(2)}"))
|
||||
|
||||
# /api/admin 组
|
||||
for m in re.finditer(r'admin\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/admin{m.group(2)}"))
|
||||
|
||||
# /api/db 组
|
||||
for m in re.finditer(r'db\.(GET|POST|PUT|DELETE)\("(/[^"]*)",\s*handler\.[A-Za-z0-9_]+', text):
|
||||
routes.append((m.group(1), f"/api/db{m.group(2)}"))
|
||||
|
||||
# 去重
|
||||
seen = set()
|
||||
out = []
|
||||
for method, p in routes:
|
||||
if (method, p) in seen:
|
||||
continue
|
||||
seen.add((method, p))
|
||||
out.append((method, p))
|
||||
return out
|
||||
|
||||
|
||||
def replace_path_params(path: str) -> str:
|
||||
return path.replace(":id", "1")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import os
|
||||
|
||||
api_base = (os.environ.get("SOUL_API_BASE") or "http://localhost:8080").rstrip("/")
|
||||
session = requests.Session()
|
||||
session.verify = False # 如为 https 自签证书也可探测
|
||||
|
||||
routes = extract_routes()
|
||||
print(f"Found routes: {len(routes)}")
|
||||
|
||||
failures: list[Check] = []
|
||||
unexpected: list[Check] = []
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
# 先验证登录接口是否通(只对 /api/admin POST 登录做一次带凭证的检查)
|
||||
admin_username = os.environ.get("SOUL_ADMIN_USERNAME", "admin")
|
||||
admin_password = os.environ.get("SOUL_ADMIN_PASSWORD", "admin123")
|
||||
login_url = f"{api_base}/api/admin"
|
||||
r_login = session.post(
|
||||
login_url,
|
||||
json={"username": admin_username, "password": admin_password},
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
try:
|
||||
login_data = r_login.json()
|
||||
except Exception:
|
||||
login_data = None
|
||||
if r_login.status_code != 200 or not (login_data and login_data.get("success") is True and login_data.get("token")):
|
||||
failures.append(Check("POST", "/api/admin", r_login.status_code, (r_login.text or "")[:200]))
|
||||
print("LOGIN_CHECK_FAILED,后续路由鉴权探测可能不准确。")
|
||||
|
||||
for method, path_template in routes:
|
||||
path = replace_path_params(path_template)
|
||||
url = f"{api_base}{path}"
|
||||
|
||||
# 仅对登录接口放行;其他都不带 token,避免触发写操作
|
||||
json_payload = None
|
||||
if path == "/api/admin" and method == "POST":
|
||||
# 已在上面验证登录;这里跳过
|
||||
continue
|
||||
|
||||
if method in ("POST", "PUT"):
|
||||
# 发空 body,通常也会被 AdminAuth 在更早阶段拦截
|
||||
json_payload = {}
|
||||
|
||||
try:
|
||||
resp = session.request(method, url, headers=headers, json=json_payload, timeout=10)
|
||||
status = resp.status_code
|
||||
preview = (resp.text or "")[:200].replace("\n", " ")
|
||||
except Exception as e:
|
||||
failures.append(Check(method, path, 0, f"EXC: {e}"))
|
||||
continue
|
||||
|
||||
# 非登录接口:预期 AdminAuth 拦截 => 401 或 403
|
||||
if status not in (401, 403):
|
||||
unexpected.append(Check(method, path, status, preview))
|
||||
|
||||
print("\n=== AUTHLESS_SMOKE_RESULT ===")
|
||||
print("Failures(0/404/500 等异常/网络异常):", len(failures))
|
||||
for it in failures[:30]:
|
||||
print(f"- {it.method} {it.path} -> {it.status}, preview={it.preview}")
|
||||
|
||||
print("Unexpected (非 401/403):", len(unexpected))
|
||||
for it in unexpected[:30]:
|
||||
print(f"- {it.method} {it.path} -> {it.status}, preview={it.preview}")
|
||||
|
||||
if len(unexpected) > 30:
|
||||
print("... truncated")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
914
soul-admin/dist/assets/index-34teBEu9.js
vendored
914
soul-admin/dist/assets/index-34teBEu9.js
vendored
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-B7tt33mg.css
vendored
1
soul-admin/dist/assets/index-B7tt33mg.css
vendored
File diff suppressed because one or more lines are too long
914
soul-admin/dist/assets/index-DCoaVA6V.js
vendored
Normal file
914
soul-admin/dist/assets/index-DCoaVA6V.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-DGXqHqcA.css
vendored
Normal file
1
soul-admin/dist/assets/index-DGXqHqcA.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理后台 - Soul创业派对</title>
|
||||
<script type="module" crossorigin src="/assets/index-34teBEu9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B7tt33mg.css">
|
||||
<script type="module" crossorigin src="/assets/index-DCoaVA6V.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DGXqHqcA.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import toast from '@/utils/toast'
|
||||
import toast from '@/utils/toast'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { get, post } from '@/api/client'
|
||||
|
||||
@@ -33,6 +33,13 @@ interface Stats {
|
||||
totalParts: number
|
||||
}
|
||||
|
||||
/** 篇内章区间(按章条数,非节数) */
|
||||
function chapterRangeLabel(chapterCount: number): string {
|
||||
if (chapterCount <= 0) return '暂无章节'
|
||||
if (chapterCount === 1) return '第1章'
|
||||
return `第1章 ~ 第${chapterCount}章`
|
||||
}
|
||||
|
||||
export function ChaptersPage() {
|
||||
const [structure, setStructure] = useState<Part[]>([])
|
||||
const [stats, setStats] = useState<Stats | null>(null)
|
||||
@@ -204,7 +211,8 @@ export function ChaptersPage() {
|
||||
</span>
|
||||
<span className="font-semibold text-white">{part.title}</span>
|
||||
<span className="text-white/40 text-sm">
|
||||
({part.chapters.reduce((acc, ch) => acc + (ch.sections?.length || 1), 0)} 节)
|
||||
({chapterRangeLabel(part.chapters.length)} ·{' '}
|
||||
{part.chapters.reduce((acc, ch) => acc + (ch.sections?.length || 1), 0)} 节)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-white/40">{expandedParts.includes(part.id) ? '▲' : '▼'}</span>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user