From c55e54efbda13a4b9c5bc9886cafb7e96537320d Mon Sep 17 00:00:00 2001 From: Alex-larget <33240357+Alex-larget@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:00:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E4=BC=A0=E7=BB=9F=20emoji=20=E4=B8=BA=20SVG=20=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=EF=BC=8C=E6=8F=90=E5=8D=87=E8=A7=86=E8=A7=89=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=E5=92=8C=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= =?UTF-8?q?=E3=80=82=E6=9B=B4=E6=96=B0=E5=A4=9A=E4=B8=AA=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=96=B0=E5=9B=BE=E6=A0=87=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E4=BD=93=E9=AA=8C=E3=80=82=E5=90=8C=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=9B=B4=E9=AB=98?= =?UTF-8?q?=E6=95=88=E7=9A=84=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BA=A4=E4=BA=92=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/agent/团队/evolution/2026-03-10.md | 96 ---- .cursor/agent/开发助理/经验清单.md | 1 - .cursor/skills/lobster-macos-vm/SKILL.md | 184 -------- miniprogram/app.js | 156 +++++-- miniprogram/app.json | 3 + miniprogram/assets/iconfont.css | 434 ++++++++++++++++++ miniprogram/components/icon/icon.js | 57 ++- miniprogram/custom-tab-bar/index.js | 5 +- miniprogram/pages/about/about.wxml | 4 +- miniprogram/pages/about/about.wxss | 3 +- miniprogram/pages/addresses/addresses.wxml | 6 +- miniprogram/pages/addresses/edit.wxml | 10 +- .../avatar-nickname/avatar-nickname.wxml | 4 +- miniprogram/pages/chapters/chapters.js | 32 +- miniprogram/pages/gift-pay/detail.js | 2 +- miniprogram/pages/gift-pay/detail.wxml | 4 +- miniprogram/pages/index/index.js | 251 ++++------ miniprogram/pages/index/index.wxml | 2 +- miniprogram/pages/match/match.js | 14 +- miniprogram/pages/match/match.wxml | 30 +- .../pages/member-detail/member-detail.wxml | 22 +- .../pages/mentor-detail/mentor-detail.wxml | 2 +- miniprogram/pages/mentors/mentors.wxml | 2 +- miniprogram/pages/my/my.js | 6 +- miniprogram/pages/my/my.wxml | 6 +- .../pages/profile-edit/profile-edit.wxml | 12 +- .../pages/profile-show/profile-show.wxml | 16 +- .../pages/profile-show/profile-show.wxss | 3 +- miniprogram/pages/purchases/purchases.js | 4 +- miniprogram/pages/purchases/purchases.wxml | 2 +- miniprogram/pages/read/read.js | 109 ++--- miniprogram/pages/read/read.wxml | 26 +- miniprogram/pages/referral/referral.wxml | 4 +- miniprogram/pages/search/search.js | 2 +- miniprogram/pages/search/search.wxml | 4 +- miniprogram/pages/settings/settings.wxml | 12 +- miniprogram/pages/vip/vip.js | 14 +- miniprogram/pages/vip/vip.wxml | 4 +- miniprogram/pages/wallet/wallet.wxml | 8 +- miniprogram/utils/chapterAccessManager.js | 4 +- soul-admin/src/pages/content/ChapterTree.tsx | 6 +- soul-admin/src/pages/content/ContentPage.tsx | 27 +- soul-api/internal/cache/cache.go | 14 +- soul-api/internal/handler/book.go | 59 +++ soul-api/internal/handler/db.go | 241 +++++++--- soul-api/internal/router/router.go | 11 +- soul-api/scripts/fix-part-titles.sql | 5 + soul-api/wechat/info.log | 4 + 开发文档/config接口优化分析.md | 163 +++++++ 开发文档/小程序-图标清单-阿里云iconfont.md | 147 ++++++ 开发文档/小程序变更影响分析-20260318.md | 67 +++ 开发文档/小程序性能分析-20260318.md | 142 ++++++ 开发文档/废弃接口清单.md | 34 ++ 开发文档/性能优化-综合分析.md | 124 +++++ .../Windows-WSL2-QEMU-macOS虚拟机安装方案.md | 220 +++++++++ .../服务器管理/Windows-macOS虚拟机安装指南.md | 157 ------- .../服务器管理/scripts/lobster_macos_vm.py | 163 ------- .../服务器管理/scripts/macOS-VM-setup-auto.sh | 24 - .../服务器管理/scripts/macOS-VM-一键安装.ps1 | 51 -- .../scripts/macOS-VM-安装-步骤1-WSL.ps1 | 13 - .../scripts/macOS-VM-安装-步骤2-克隆与检查.ps1 | 44 -- .../服务器管理/scripts/macOS-VM-安装-简化版.bat | 27 -- 62 files changed, 2033 insertions(+), 1270 deletions(-) delete mode 100644 .cursor/skills/lobster-macos-vm/SKILL.md create mode 100644 miniprogram/assets/iconfont.css create mode 100644 soul-api/scripts/fix-part-titles.sql create mode 100644 开发文档/config接口优化分析.md create mode 100644 开发文档/小程序-图标清单-阿里云iconfont.md create mode 100644 开发文档/小程序变更影响分析-20260318.md create mode 100644 开发文档/小程序性能分析-20260318.md create mode 100644 开发文档/废弃接口清单.md create mode 100644 开发文档/性能优化-综合分析.md create mode 100644 开发文档/服务器管理/Windows-WSL2-QEMU-macOS虚拟机安装方案.md delete mode 100644 开发文档/服务器管理/Windows-macOS虚拟机安装指南.md delete mode 100644 开发文档/服务器管理/scripts/lobster_macos_vm.py delete mode 100644 开发文档/服务器管理/scripts/macOS-VM-setup-auto.sh delete mode 100644 开发文档/服务器管理/scripts/macOS-VM-一键安装.ps1 delete mode 100644 开发文档/服务器管理/scripts/macOS-VM-安装-步骤1-WSL.ps1 delete mode 100644 开发文档/服务器管理/scripts/macOS-VM-安装-步骤2-克隆与检查.ps1 delete mode 100644 开发文档/服务器管理/scripts/macOS-VM-安装-简化版.bat diff --git a/.cursor/agent/团队/evolution/2026-03-10.md b/.cursor/agent/团队/evolution/2026-03-10.md index 331e0bd1..4e7ab4f2 100644 --- a/.cursor/agent/团队/evolution/2026-03-10.md +++ b/.cursor/agent/团队/evolution/2026-03-10.md @@ -1,101 +1,5 @@ # 团队共享 经验记录 - 2026-03-10 -## 在 Windows 上一键启动 macOS 虚拟机(“龙虾”智能体经验) - -### 场景 / 问题 - -- 成员希望在 **Windows 10/11** 上通过 **Docker** 一键安装 macOS,用于演示 / 测试。 -- 实际上: - - **macOS 不能在 Docker 中运行**(Docker 只跑 Linux 容器,没有合法的 macOS 镜像)。 - - 唯一可行路径是:**WSL2 + Ubuntu + QEMU/KVM + OneClick-macOS-Simple-KVM**。 - -### 关键决策 - -1. **明确技术与法律边界** - - 不支持也不承诺在 Docker 中直接跑 macOS。 - - 统一采用「**WSL2 + QEMU/KVM + OneClick**」这个方案,仅用于演示 / 测试。 - -2. **固定目录与流程约定** - - Windows 侧统一放在:`C:\Users\{USERNAME}\Mycontent\macos-vm` - - WSL 侧路径:`/mnt/c/Users/{USERNAME}/Mycontent/macos-vm` - - 内部结构: - - `OneClick-macOS-Simple-KVM/` - - `BaseSystem.dmg` / `BaseSystem.img` - - `macOS.qcow2` - -3. **获取 OneClick 源码时优先走 zip,而不是 git clone** - - 直接 `git clone` 很容易在国内网络环境下触发 `GnuTLS recv error (-110)` 等 TLS 超时。 - - 统一约定使用: - - `https://codeload.github.com/notAperson535/OneClick-macOS-Simple-KVM/zip/refs/heads/master` - - 然后在 WSL 中: - - `curl + unzip` → 解压 → 重命名为 `OneClick-macOS-Simple-KVM`。 - -4. **依赖安装与 KVM 检查** - - 在 Ubuntu-24.04 内安装: - - `qemu-system qemu-utils python3 python3-pip cpu-checker` - - 使用 `kvm-ok` 检查: - - 期望输出:`/dev/kvm exists` + `KVM acceleration can be used`。 - - 若 `nested` 为 `N` 且需要嵌套虚拟化,使用 `.wslconfig` 打开 nested。 - -5. **下载 macOS Ventura 恢复镜像并生成 BaseSystem.img** - - 通过 `python3 fetch-macOS-v2.py -s ventura` 下载官方 Recovery 镜像。 - - 将 `RecoveryImage.dmg` 重命名为 `BaseSystem.dmg`,再用 `qemu-img convert` 生成 `BaseSystem.img`。 - - 验收标准: - - `BaseSystem.dmg` ≈ 678 MB - - `BaseSystem.img` ≈ 3.0 GB - -6. **以 HEADLESS + VNC 方式启动 VM** - - 使用 `sudo HEADLESS=1 ./basic.sh` 启动 QEMU。 - - 在 Windows 中确认 `127.0.0.1:5900` 端口监听。 - - 统一告知使用 VNC 客户端连接 `localhost:5900`,再在图形界面内完成 macOS 安装向导。**用户环境已采用 TightVNC**,与 RealVNC Viewer 等方式等效。 - -7. **WSL 卡死与多 wsl 进程清理策略** - - 当 `wsl --shutdown` 卡住、或者有大量 `wsl` 进程残留时: - - 使用 PowerShell `Get-Process wsl | Stop-Process -Force` 清理。 - - 再执行 `wsl --shutdown` + `wsl -l -v` 验证状态恢复。 - -### 对应 Skill / 智能体 - -- 新建 Skill:`.cursor/skills/lobster-macos-vm/SKILL.md` - - 技能名:`lobster-macos-vm` - - 智能体名(对外):**“龙虾”** - - 职责:当用户在 Windows 上提出安装 / 维护 macOS 虚拟机的需求时,统一按该 Skill 流程执行: - - 解释 Docker 不可用 → 切换到 WSL2 + QEMU 方案。 - - 固定目录 → 下载 OneClick → 安装依赖 → 下载 Ventura → 生成 `BaseSystem.img` → HEADLESS 启动 → 引导 VNC 安装。 - -- 计划脚本化: - - 在 `开发文档/服务器管理/scripts/lobster_macos_vm.py` 中实现一键部署脚本,封装上述流程,供“龙虾”及人类成员复用。 - -### 用户环境补充 - -- **VNC 客户端**:当前环境使用 **TightVNC** 连接 `localhost:5900`,已写入龙虾 Skill,后续回复可一并推荐 TightVNC / RealVNC / TigerVNC 等。 - -### 安装完成与使用规范快照 - -- 当前虚拟机参数: - - 内存:8G(`-m 8G`) - - CPU:1 颗 CPU × 4 核 × 2 线程(共 8 线程) - - 系统盘:`macOS.qcow2`,逻辑容量 64G,可后续通过 `qemu-img resize` 扩容。 -- 启动方式快照: - - 优先使用 `C:\Users\{USERNAME}\Mycontent\macos-vm\一键启动-macOS虚拟机.bat` - - bat 内部做两件事: - 1. 启动 WSL → 进入 `OneClick-macOS-Simple-KVM` → `sudo HEADLESS=1 ./basic.sh` - 2. 等待约 10 秒后自动启动 TightVNC Viewer 连接 `localhost:5900` - - 关闭规则:不要关名为「macOS 虚拟机 - 勿关此窗口」的终端窗口,否则虚拟机会被强制关闭。 -- 安装阶段经验: - - 安装过程中多次重启属正常,出现 `X86PlatformPlugin::systemWillShutdown!` / `IOPlatformHaltRestartAction -> AppleSMC` 说明在正常关机/重启。 - - OpenCore 菜单的选择顺序: - - 安装阶段:多次选择 `macOS Installer` 直至不再出现安装向导。 - - 安装完成:只选系统盘(如 `Macintosh HD`),不要选 `mac - Data`。 - - 安装完成后,为避免反复从恢复盘启动,`basic.sh` 默认不再挂载 `BaseSystem.img`;需要重装时可通过 `INSTALL_MEDIA=1 HEADLESS=1 ./basic.sh` 临时挂载。 - -### 适用角色 - -- 后端 / 运维:需要在本地或服务器上快速拉起 macOS VM 做兼容性验证或演示。 -- 团队:对外说明 **“我们不支持 Docker macOS,统一用龙虾方案”**。 - ---- - ## 管理端迁移 Mycontent-temp:菜单/布局新规范基线 ### 决议(团队共享) diff --git a/.cursor/agent/开发助理/经验清单.md b/.cursor/agent/开发助理/经验清单.md index e550249c..3ab0261c 100644 --- a/.cursor/agent/开发助理/经验清单.md +++ b/.cursor/agent/开发助理/经验清单.md @@ -25,7 +25,6 @@ | 2026-02-27 | 小程序、团队 | 最佳实践 | SKILL-小程序开发 §6、SKILL-管理端开发 §4.1 | 输入框 padding 用 view/div 包裹 | | 2026-02-28 | 小程序、管理端 | 最佳实践 | miniprogram §6、admin §4.1 | input 边距口诀「外边包 view、内部 width 100%」;match 弹窗已修正 | | 2026-03-03 | 小程序 | 最佳实践 | miniprogram §8 | 我的页面卡片区边距 16rpx,个人中心类页面布局规范 | -| 2026-03-10 | 团队 | 架构/运维约定 | lobster-macos-vm Skill | Windows 上统一使用 WSL2+QEMU+OneClick 的"龙虾"方案安装 macOS 虚拟机,禁止 Docker 直跑 macOS | | 2026-03-10 | 小程序 | 最佳实践 | miniprogram-dev SKILL §my | my.js 阅读统计改为后端接口(loadDashboardStats),禁止用随机数时间/标题占位 | | 2026-03-10 | 后端 | bug 修复 | api-dev SKILL | 聚合接口三处修复:recentChapters 去重、totalReadMinutes 最小1分钟、DB 错误返回 500 | | 2026-03-10 | 团队 | 方法论 | - | 新旧版代码对比:以功能完整性为基准,批量 diff + 分类取舍,不以日期判优劣 | diff --git a/.cursor/skills/lobster-macos-vm/SKILL.md b/.cursor/skills/lobster-macos-vm/SKILL.md deleted file mode 100644 index 558e7124..00000000 --- a/.cursor/skills/lobster-macos-vm/SKILL.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -name: lobster-macos-vm -description: Automates provisioning and troubleshooting of macOS virtual machines on Windows using WSL2, QEMU/KVM, and the OneClick-macOS-Simple-KVM project. Use when the user mentions 龙虾, macOS 虚拟机, 一键安装苹果系统 on Windows, or needs to re-deploy the Ventura/Sonoma VM. ---- - -# 龙虾(lobster-macos-vm) - -> 专门负责:在 **Windows 10/11** 上,通过 **WSL2 + Ubuntu + QEMU/KVM + OneClick-macOS-Simple-KVM** 自动拉起一台 macOS 虚拟机(Ventura 为默认),并处理常见网络 / WSL / KVM 问题。 - -## 触发场景 - -- 用户提到:**“龙虾”**、**“苹果系统虚拟机”**、**“Windows 上跑 macOS”**、**“一键安装 macOS 虚拟机”** -- 用户需要:在 Windows 上**演示 / 测试** macOS,而不是 Docker 容器 -- 用户遇到:WSL 安装失败、`0x80072ee2` 网络错误、`kvm-ok`、`nested` 配置问题、`git clone` TLS 超时、OneClick 项目下载问题 - -## 核心能力 - -1. **环境检测与前置说明** - - 明确告诉用户:**macOS 不能在 Docker 里运行,必须用 WSL2 + 虚拟机**。 - - 检查: - - `wsl -l -v` → 是否有 `Ubuntu-24.04`,是否为 Version 2 - - `docker --version` 仅作背景信息,不作为必需条件 - - 若缺失 WSL2: - - 指导用户在**管理员 PowerShell**中执行: - - `wsl --install` - - 重启后执行 `wsl --install -d Ubuntu-24.04 --web-download`(必要时) - -2. **固定部署目录约定** - - 所有与 macOS VM 相关的文件,统一放到: - - Windows 路径:`C:\Users\{USERNAME}\Mycontent\macos-vm` - - WSL 路径:`/mnt/c/Users/{USERNAME}/Mycontent/macos-vm` - - 该目录下结构: - - `OneClick-macOS-Simple-KVM/`(从 GitHub 下载的项目) - - `BaseSystem.dmg` / `BaseSystem.img` - - `macOS.qcow2` - - 可能还有 `OneClick.zip` 等临时文件 - -3. **获取 OneClick 源码(优先 zip,退而求其次 git)** - -优先使用 **codeload.zip**,避免长时间 `git clone` TLS 超时: - -- 在 `macos-vm/` 目录内执行: - -```bash -sudo apt-get update -qq -sudo apt-get install -y curl unzip -rm -rf OneClick-macOS-Simple-KVM OneClick.zip -curl -L --retry 8 --retry-delay 2 --connect-timeout 20 --max-time 600 \ - -o OneClick.zip \ - https://codeload.github.com/notAperson535/OneClick-macOS-Simple-KVM/zip/refs/heads/master -unzip -q OneClick.zip -mv OneClick-macOS-Simple-KVM-master OneClick-macOS-Simple-KVM -``` - -仅当用户网络环境允许且确有需要时,才尝试: - -```bash -git clone --depth 1 https://github.com/notAperson535/OneClick-macOS-Simple-KVM.git -``` - -出现 `GnuTLS recv error (-110)` 或 TLS 断开时,**不要重复 git clone**,改走 zip 方案。 - -4. **依赖安装与 KVM 检查** - -在 `Ubuntu-24.04` 内执行: - -```bash -sudo apt-get update -qq -sudo apt-get install -y qemu-system qemu-utils python3 python3-pip cpu-checker -kvm-ok -``` - -预期输出: - -- `INFO: /dev/kvm exists` -- `KVM acceleration can be used` - -若 `nested` 为 `N` 但 `kvm-ok` 正常: - -- 提示用户在 `C:\Users\{USERNAME}\.wslconfig` 中写入: - -```ini -[wsl2] -nestedVirtualization=true -kernel=C:\\Users\\{USERNAME}\\bzImage -debugConsole=true -pageReporting=true -kernelCommandLine=intel_iommu=on iommu=pt kvm.ignore_msrs=1 kvm-intel.nested=1 kvm-intel.ept=1 kvm-intel.emulate_invalid_guest_state=0 kvm-intel.enable_shadow_vmcs=1 kvm-intel.enable_apicv=1 -``` - -并执行 `wsl --shutdown` 之后重试。 - -5. **下载 macOS Ventura 恢复镜像并生成 BaseSystem.img** - -在 `OneClick-macOS-Simple-KVM` 目录内: - -```bash -cd /mnt/c/Users/{USERNAME}/Mycontent/macos-vm/OneClick-macOS-Simple-KVM -chmod +x *.sh *.py -[ -f macOS.qcow2 ] || qemu-img create -f qcow2 macOS.qcow2 64G -python3 fetch-macOS-v2.py -s ventura -[ -f RecoveryImage.dmg ] && mv RecoveryImage.dmg BaseSystem.dmg -qemu-img convert BaseSystem.dmg -O raw BaseSystem.img -ls -lah BaseSystem.* macOS.qcow2 -``` - -成功标志: - -- `BaseSystem.dmg` ≈ 678 MB -- `BaseSystem.img` ≈ 3.0 GB -- `macOS.qcow2` 已存在(几十 KB 起步) - -6. **启动虚拟机(headless + VNC: localhost:5900)** - -在 `OneClick-macOS-Simple-KVM` 目录内执行: - -```bash -sudo HEADLESS=1 ./basic.sh -``` - -常见日志: - -- ALSA / audio 报错(没有声卡驱动)→ **可以忽略** -- `BdsDxe: loading Boot0001 "UEFI QEMU HARDDISK QM00017"...` → 已经开始从虚拟硬盘启动 - -在 Windows 侧确认端口: - -```powershell -Get-NetTCPConnection -LocalPort 5900 -State Listen -``` - -若监听正常,提示用户: - -- 安装 VNC 客户端并连接 `localhost:5900`,进入 macOS 安装向导(磁盘工具抹盘 + 安装系统)。 -- **常用 VNC 客户端**:RealVNC Viewer、**TightVNC**(用户环境已采用)、TigerVNC 等均可,连接地址均为 `localhost:5900`。 - -7. **WSL / 网络故障排查** - -- 若 `wsl` 进程过多、`wsl --shutdown` 卡死: - - 在 PowerShell 中执行: - -```powershell -Get-Process -Name wsl -ErrorAction SilentlyContinue | Stop-Process -Force -wsl --shutdown -wsl -l -v -``` - -- 若 `wsl --install` 报 `0x80072ee2` 或无法访问 `raw.githubusercontent.com`: - - 提醒用户这是 **网络 / DNS 问题**,可尝试: - - 切换 DNS 到 `8.8.8.8` - - 使用合规代理 / VPN - - 使用 `--web-download` 方式安装发行版: - -```powershell -wsl --install -d Ubuntu-24.04 --web-download -``` - -8. **Python 一键脚本(lobster_macos_vm.py)协同** - -当仓库中存在 `开发文档/服务器管理/scripts/lobster_macos_vm.py` 时: - -- 优先引导用户在 **PowerShell** 中执行: - -```powershell -python C:\Users\{USERNAME}\Mycontent\macos-vm\lobster_macos_vm.py -``` - -- 该脚本应负责: - - 检查 / 安装 WSL2 + Ubuntu-24.04(必要时提示用户重启) - - 确保 `C:\Users\{USERNAME}\Mycontent\macos-vm` 目录存在 - - 在 WSL 内下载 OneClick 源码 zip、解压到固定目录 - - 安装 QEMU / Python 依赖并检查 `kvm-ok` - - 下载 Ventura 恢复镜像并生成 `BaseSystem.img` - - 启动 `sudo HEADLESS=1 ./basic.sh` - - 输出清晰的步骤说明(包括如何用 VNC 连接) - -## 使用示例 - -- 用户说:「**龙虾,帮我在这台 Windows 上一键装一个 macOS 虚拟机,用来演示**」 - - 按上述步骤依次执行:环境检测 → 创建 `macos-vm` 目录 → 下载 OneClick → 安装依赖 → 下载 Ventura → 生成 `BaseSystem.img` → 启动虚拟机,并提醒用户用 VNC 连 `localhost:5900` 完成图形安装。 - -- 用户说:「**龙虾,之前的 macOS 虚拟机挂了,重装一遍**」 - - 复用相同目录和镜像文件,必要时重新下载 `BaseSystem.dmg`,再启动 `HEADLESS=1 ./basic.sh`。 - diff --git a/miniprogram/app.js b/miniprogram/app.js index 0b784dda..681faff1 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -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, @@ -30,7 +30,7 @@ App({ openId: null, // 微信openId,支付必需 isLoggedIn: false, - // 书籍数据 + // 书籍数据(bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters) bookData: null, totalSections: 62, @@ -76,7 +76,10 @@ App({ // 审核模式:后端 /api/miniprogram/config 返回 auditMode=true 时隐藏所有支付相关UI auditMode: false, // 客服/微信:mp_config 返回 supportWechat - supportWechat: '' + supportWechat: '', + // config 统一缓存(5min),减少重复请求 + configCache: null, + configCacheExpires: 0 }, onLaunch(options) { @@ -109,15 +112,15 @@ App({ this.handleReferralCode(options) }, - // 小程序显示时:处理分享参数、检测更新、刷新 mpConfig(从后台切回时) + // 小程序显示时:处理分享参数、检测更新、刷新审核模式(从后台切回时) onShow(options) { this.handleReferralCode(options) this.checkUpdate() - // 从后台切回时刷新审核模式等配置(节流 30 秒,避免频繁请求) + // 从后台切回时仅刷新审核模式(轻量接口 /config/audit-mode),节流 30 秒 const now = Date.now() if (!this.globalData.lastMpConfigCheck || now - this.globalData.lastMpConfigCheck > 30 * 1000) { this.globalData.lastMpConfigCheck = now - this.loadMpConfig() + this.getAuditMode() } }, @@ -225,12 +228,6 @@ App({ return code.replace(/[\s\-_]/g, '').toUpperCase().trim() }, - // 根据业务 id 从 bookData 查 mid(用于跳转) - getSectionMid(sectionId) { - const list = this.globalData.bookData || [] - const ch = list.find(c => c.id === sectionId) - return ch?.mid || 0 - }, // 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串) getMyReferralCode() { @@ -321,32 +318,112 @@ App({ } }, - // 加载书籍数据 + // 加载书籍元数据(totalSections),不再预加载 all-chapters async loadBookData() { try { - // 先从缓存加载 - const cachedData = wx.getStorageSync('bookData') - if (cachedData) { - this.globalData.bookData = cachedData - } - - // 从服务器获取最新数据 - const res = await this.request('/api/miniprogram/book/all-chapters') - if (res && (res.data || res.chapters)) { - const chapters = res.data || res.chapters || [] - this.globalData.bookData = chapters - this.globalData.totalSections = (chapters && chapters.length) ? chapters.length : 62 - wx.setStorageSync('bookData', chapters) + const res = await this.request({ url: '/api/miniprogram/book/parts', silent: true }) + if (res?.success && res.totalSections != null) { + this.globalData.totalSections = res.totalSections } } catch (e) { - console.error('加载书籍数据失败:', e) + try { + const statsRes = await this.request({ url: '/api/miniprogram/book/stats', silent: true }) + if (statsRes?.success && statsRes?.data?.totalChapters != null) { + this.globalData.totalSections = statsRes.data.totalChapters + } + } catch (_) {} } }, + /** + * 获取 config(统一缓存 5min,各页优先读缓存) + * 使用拆分接口 core + audit-mode,体积更小、审核模式独立刷新 + * @param {boolean} forceRefresh - 强制刷新,跳过缓存 + * @returns {Promise} 完整 config 或 null + */ + async getConfig(forceRefresh = false) { + const now = Date.now() + const CACHE_TTL = 5 * 60 * 1000 + if (!forceRefresh && this.globalData.configCache && now < this.globalData.configCacheExpires) { + return this.globalData.configCache + } + try { + const [coreRes, auditRes] = await Promise.all([ + this.request({ url: '/api/miniprogram/config/core', silent: true, timeout: 5000 }), + this.request({ url: '/api/miniprogram/config/audit-mode', silent: true, timeout: 3000 }) + ]) + if (coreRes) { + const auditMode = auditRes && typeof auditRes.auditMode === 'boolean' ? auditRes.auditMode : false + const mp = (coreRes.mpConfig && typeof coreRes.mpConfig === 'object') ? { ...coreRes.mpConfig } : {} + mp.auditMode = auditMode + const res = { + success: coreRes.success, + prices: coreRes.prices, + features: coreRes.features, + userDiscount: coreRes.userDiscount, + mpConfig: mp + } + this.globalData.configCache = res + this.globalData.configCacheExpires = now + CACHE_TTL + return res + } + } catch (e) { + if (this.globalData.configCache) return this.globalData.configCache + } + return null + }, + + /** + * 获取阅读页扩展配置(linkTags、linkedMiniprograms),懒加载 + */ + async getReadExtras() { + if (Array.isArray(this.globalData.linkTagsConfig) && this.globalData.linkTagsConfig.length > 0) { + return { + linkTags: this.globalData.linkTagsConfig, + linkedMiniprograms: this.globalData.linkedMiniprograms || [] + } + } + try { + const res = await this.request({ url: '/api/miniprogram/config/read-extras', silent: true, timeout: 5000 }) + if (res) { + if (Array.isArray(res.linkTags)) this.globalData.linkTagsConfig = res.linkTags + if (Array.isArray(res.linkedMiniprograms)) this.globalData.linkedMiniprograms = res.linkedMiniprograms + return res + } + } catch (e) {} + return { linkTags: [], linkedMiniprograms: [] } + }, + + /** + * 仅刷新审核模式(从后台切回时用,轻量) + */ + async getAuditMode() { + try { + const res = await this.request({ url: '/api/miniprogram/config/audit-mode', silent: true, timeout: 3000 }) + if (res && typeof res.auditMode === 'boolean') { + this.globalData.auditMode = res.auditMode + if (this.globalData.configCache && this.globalData.configCache.mpConfig) { + this.globalData.configCache.mpConfig.auditMode = res.auditMode + } + try { + const pages = getCurrentPages() + pages.forEach(p => { + if (p && p.data && 'auditMode' in p.data) { + p.setData({ auditMode: res.auditMode }) + } + }) + } catch (_) {} + return res.auditMode + } + } catch (e) {} + return this.globalData.auditMode + }, + // 加载 mpConfig(appId、mchId、withdrawSubscribeTmplId、auditMode、supportWechat 等),失败时保留 globalData 默认值 async loadMpConfig() { try { - const res = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 }) + const res = await this.getConfig() + if (!res) return const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config) if (mp && typeof mp === 'object') { if (mp.appId) this.globalData.appId = mp.appId @@ -423,6 +500,7 @@ App({ /** * 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。 + * GET 请求 200ms 内相同 url 去重,避免并发重复请求。 * @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent } * @param {object} options - { method, data, header, silent } * @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计) @@ -437,6 +515,7 @@ App({ } else { url = '' } + const method = (options.method || 'GET').toUpperCase() const silent = !!options.silent const showError = (msg) => { if (!silent && msg) { @@ -444,7 +523,16 @@ App({ } } - return new Promise((resolve, reject) => { + // GET 短时去重:相同 url 的并发请求共享同一 promise + if (method === 'GET') { + const dedupKey = url + (options.data ? JSON.stringify(options.data) : '') + const pending = this._requestPending || (this._requestPending = {}) + if (pending[dedupKey]) { + return pending[dedupKey].promise + } + } + + const promise = new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: this.globalData.baseUrl + url, @@ -492,6 +580,14 @@ App({ } }) }) + + if (method === 'GET') { + const dedupKey = url + (options.data ? JSON.stringify(options.data) : '') + const pending = this._requestPending || (this._requestPending = {}) + pending[dedupKey] = { promise, ts: Date.now() } + promise.finally(() => { delete pending[dedupKey] }) + } + return promise }, // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”) diff --git a/miniprogram/app.json b/miniprogram/app.json index c1909ab6..320b1cc2 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -1,4 +1,7 @@ { + "usingComponents": { + "icon": "/components/icon/icon" + }, "pages": [ "pages/chapters/chapters", "pages/index/index", diff --git a/miniprogram/assets/iconfont.css b/miniprogram/assets/iconfont.css new file mode 100644 index 00000000..311e2b1f --- /dev/null +++ b/miniprogram/assets/iconfont.css @@ -0,0 +1,434 @@ +@font-face { + font-family: "iconfont"; /* Project id 5142223 */ + src: url('//at.alicdn.com/t/c/font_5142223_1sq6pv9vvbt.woff2?t=1773819902347') format('woff2'), + url('//at.alicdn.com/t/c/font_5142223_1sq6pv9vvbt.woff?t=1773819902347') format('woff'), + url('//at.alicdn.com/t/c/font_5142223_1sq6pv9vvbt.ttf?t=1773819902347') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-wallet:before { + content: "\e6c8"; +} + +.icon-gift:before { + content: "\e6c9"; +} + +.icon-zap1:before { + content: "\e75c"; +} + +.icon-user:before { + content: "\e6b9"; +} + +.icon-upload:before { + content: "\e6ba"; +} + +.icon-work:before { + content: "\e6bb"; +} + +.icon-training:before { + content: "\e6bc"; +} + +.icon-warning:before { + content: "\e6bd"; +} + +.icon-zoom-in:before { + content: "\e6be"; +} + +.icon-zoom-out:before { + content: "\e6bf"; +} + +.icon-arrow-left-bold:before { + content: "\e6c1"; +} + +.icon-arrow-up-bold:before { + content: "\e6c2"; +} + +.icon-close-bold:before { + content: "\e6c3"; +} + +.icon-arrow-down-bold:before { + content: "\e6c4"; +} + +.icon-minus-bold:before { + content: "\e6c5"; +} + +.icon-arrow-right-bold:before { + content: "\e6c6"; +} + +.icon-select-bold:before { + content: "\e6c7"; +} + +.icon-money-wallet:before { + content: "\e833"; +} + +.icon-book-open:before { + content: "\e993"; +} + +.icon-biaoshilei_yonghuzu:before { + content: "\e61b"; +} + +.icon-add:before { + content: "\e664"; +} + +.icon-add-circle:before { + content: "\e665"; +} + +.icon-adjust:before { + content: "\e666"; +} + +.icon-arrow-up-circle:before { + content: "\e667"; +} + +.icon-arrow-right-circle:before { + content: "\e668"; +} + +.icon-arrow-down:before { + content: "\e669"; +} + +.icon-ashbin:before { + content: "\e66a"; +} + +.icon-arrow-right:before { + content: "\e66b"; +} + +.icon-browse:before { + content: "\e66c"; +} + +.icon-bottom:before { + content: "\e66d"; +} + +.icon-back:before { + content: "\e66e"; +} + +.icon-bad:before { + content: "\e66f"; +} + +.icon-arrow-left-circle:before { + content: "\e670"; +} + +.icon-camera:before { + content: "\e671"; +} + +.icon-chart-bar:before { + content: "\e672"; +} + +.icon-attachment:before { + content: "\e673"; +} + +.icon-code:before { + content: "\e674"; +} + +.icon-close:before { + content: "\e675"; +} + +.icon-check-item:before { + content: "\e676"; +} + +.icon-calendar:before { + content: "\e677"; +} + +.icon-comment:before { + content: "\e678"; +} + +.icon-complete:before { + content: "\e679"; +} + +.icon-direction-down:before { + content: "\e67a"; +} + +.icon-direction-down-circle:before { + content: "\e67b"; +} + +.icon-direction-right:before { + content: "\e67c"; +} + +.icon-direction-up:before { + content: "\e67d"; +} + +.icon-discount:before { + content: "\e67e"; +} + +.icon-direction-left:before { + content: "\e67f"; +} + +.icon-download:before { + content: "\e680"; +} + +.icon-electronics:before { + content: "\e681"; +} + +.icon-elipsis:before { + content: "\e682"; +} + +.icon-export:before { + content: "\e683"; +} + +.icon-explain:before { + content: "\e684"; +} + +.icon-edit:before { + content: "\e685"; +} + +.icon-eye-close:before { + content: "\e686"; +} + +.icon-email:before { + content: "\e687"; +} + +.icon-error:before { + content: "\e688"; +} + +.icon-favorite:before { + content: "\e689"; +} + +.icon-file-common:before { + content: "\e68a"; +} + +.icon-file-delete:before { + content: "\e68b"; +} + +.icon-file-add:before { + content: "\e68c"; +} + +.icon-film:before { + content: "\e68d"; +} + +.icon-fabulous:before { + content: "\e68e"; +} + +.icon-file:before { + content: "\e68f"; +} + +.icon-folder-close:before { + content: "\e690"; +} + +.icon-filter:before { + content: "\e691"; +} + +.icon-good:before { + content: "\e692"; +} + +.icon-hide:before { + content: "\e693"; +} + +.icon-home:before { + content: "\e694"; +} + +.icon-file-open:before { + content: "\e695"; +} + +.icon-forward:before { + content: "\e696"; +} + +.icon-import:before { + content: "\e697"; +} + +.icon-layers:before { + content: "\e698"; +} + +.icon-lock:before { + content: "\e699"; +} + +.icon-map:before { + content: "\e69a"; +} + +.icon-menu:before { + content: "\e69b"; +} + +.icon-help:before { + content: "\e69c"; +} + +.icon-minus-circle:before { + content: "\e69d"; +} + +.icon-notification:before { + content: "\e69e"; +} + +.icon-more:before { + content: "\e69f"; +} + +.icon-mobile-phone:before { + content: "\e6a0"; +} + +.icon-minus:before { + content: "\e6a1"; +} + +.icon-navigation:before { + content: "\e6a2"; +} + +.icon-prompt:before { + content: "\e6a3"; +} + +.icon-refresh:before { + content: "\e6a4"; +} + +.icon-run-up:before { + content: "\e6a5"; +} + +.icon-picture:before { + content: "\e6a6"; +} + +.icon-run-in:before { + content: "\e6a7"; +} + +.icon-pin:before { + content: "\e6a8"; +} + +.icon-save:before { + content: "\e6a9"; +} + +.icon-search:before { + content: "\e6aa"; +} + +.icon-share:before { + content: "\e6ab"; +} + +.icon-scanning:before { + content: "\e6ac"; +} + +.icon-security:before { + content: "\e6ad"; +} + +.icon-sign-out:before { + content: "\e6ae"; +} + +.icon-select:before { + content: "\e6af"; +} + +.icon-stop:before { + content: "\e6b0"; +} + +.icon-success:before { + content: "\e6b1"; +} + +.icon-switch:before { + content: "\e6b2"; +} + +.icon-setting:before { + content: "\e6b3"; +} + +.icon-survey:before { + content: "\e6b4"; +} + +.icon-time:before { + content: "\e6b5"; +} + +.icon-telephone:before { + content: "\e6b6"; +} + +.icon-top:before { + content: "\e6b7"; +} + +.icon-unlock:before { + content: "\e6b8"; +} diff --git a/miniprogram/components/icon/icon.js b/miniprogram/components/icon/icon.js index b2dec23f..8c0e59cb 100644 --- a/miniprogram/components/icon/icon.js +++ b/miniprogram/components/icon/icon.js @@ -41,20 +41,55 @@ Component({ }, methods: { - // SVG 图标数据映射 + // SVG 图标数据映射(Lucide 风格,替换原 emoji) getSvgPath(name) { + const s = '' const svgMap = { - 'share': '', - - 'arrow-up-right': '', - - 'chevron-left': '', - - 'search': '', - - 'heart': '' + 'share': s + '', + 'arrow-up-right': s + '', + 'chevron-left': s + '', + 'search': s + '', + 'heart': s + '', + 'user': s + '', + 'smartphone': s + '', + 'map-pin': s + '', + 'home': s + '', + 'star': s + '', + 'message-circle': s + '', + 'package': s + '', + 'book-open': s + '', + 'lightbulb': s + '', + 'handshake': s + '', + 'rocket': s + '', + 'trophy': s + '', + 'refresh-cw': s + '', + 'shield': s + '', + 'wallet': s + '', + 'wrench': s + '', + 'camera': s + '', + 'phone': s + '', + 'clipboard': s + '', + 'megaphone': s + '', + 'image': s + '', + 'gift': s + '', + 'lock': s + '', + 'sparkles': s + '', + 'save': s + '', + 'globe': s + '', + 'users': s + '', + 'gamepad': s + '', + 'check': s + '', + 'trash-2': s + '', + 'clock': s + '', + 'plus': s + '', + 'briefcase': s + '', + 'target': s + '', + 'rotate-ccw': s + '', + 'corner-down-left': s + '', + 'folder': s + '', + 'bar-chart': s + '', + 'link': s + '' } - return svgMap[name] || '' }, diff --git a/miniprogram/custom-tab-bar/index.js b/miniprogram/custom-tab-bar/index.js index 4acd9546..27268ea8 100644 --- a/miniprogram/custom-tab-bar/index.js +++ b/miniprogram/custom-tab-bar/index.js @@ -67,10 +67,7 @@ Component({ console.log('[TabBar] 开始加载功能配置...') console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config') - // app.request 的第一个参数是 url 字符串,第二个参数是 options 对象 - const res = await app.request('/api/miniprogram/config', { - method: 'GET' - }) + const res = await app.getConfig() // 兼容两种返回格式 diff --git a/miniprogram/pages/about/about.wxml b/miniprogram/pages/about/about.wxml index 3ddb3660..8ad748aa 100644 --- a/miniprogram/pages/about/about.wxml +++ b/miniprogram/pages/about/about.wxml @@ -38,7 +38,7 @@ - 📚 {{bookInfo.title}} + {{bookInfo.title}} {{bookInfo.totalChapters}} @@ -65,7 +65,7 @@ 联系作者 - 🎉 + Soul派对房 每天早上6-9点开播 diff --git a/miniprogram/pages/about/about.wxss b/miniprogram/pages/about/about.wxss index 06ad5a00..7e10afcc 100644 --- a/miniprogram/pages/about/about.wxss +++ b/miniprogram/pages/about/about.wxss @@ -17,7 +17,8 @@ .stat-value { font-size: 36rpx; font-weight: 700; color: #00CED1; display: block; } .stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); } .contact-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; } -.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 24rpx; } +.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: flex; align-items: center; gap: 12rpx; margin-bottom: 24rpx; } +.card-title .card-title-icon { flex-shrink: 0; } .contact-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 16rpx; } .contact-item:last-child { margin-bottom: 0; } .contact-icon { font-size: 40rpx; } diff --git a/miniprogram/pages/addresses/addresses.wxml b/miniprogram/pages/addresses/addresses.wxml index cec2ef6e..dbdf8da8 100644 --- a/miniprogram/pages/addresses/addresses.wxml +++ b/miniprogram/pages/addresses/addresses.wxml @@ -18,7 +18,7 @@ - 📍 + 暂无收货地址 点击下方按钮添加 @@ -50,7 +50,7 @@ bindtap="deleteAddress" data-id="{{item.id}}" > - 🗑️ + 删除 @@ -59,7 +59,7 @@ - + 新增收货地址 diff --git a/miniprogram/pages/addresses/edit.wxml b/miniprogram/pages/addresses/edit.wxml index c5429207..ceb915f9 100644 --- a/miniprogram/pages/addresses/edit.wxml +++ b/miniprogram/pages/addresses/edit.wxml @@ -15,7 +15,7 @@ - 👤 + 收货人 - 📱 + 手机号 - 📍 + 所在地区 - 🏠 + 详细地址