From 8d856fb56c3b87bcf2a4d75fa0e38d65ce9f8fc0 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Tue, 25 Aug 2026 00:41:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(h5):=20=E5=B0=8F=E7=A8=8B=E5=BA=8F=20embed?= =?UTF-8?q?=20=E6=A8=A1=E5=BC=8F=E4=B8=8E=E5=85=A8=E7=AB=99=20ICP=20?= =?UTF-8?q?=E5=A4=87=E6=A1=88=E9=A1=B5=E8=84=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H5 支持 ?mp=1 嵌入微信 WebView;user-h5/admin-h5 底部展示蜀ICP备2025140386号-2 并链至工信部;补充小程序+H5 部署说明。 Co-authored-by: Cursor --- apps/admin-h5/src/components/IcpFooter.vue | 25 ++ apps/admin-h5/src/layouts/AdminShell.vue | 14 +- apps/admin-h5/src/pages/LoginPage.vue | 16 +- .../public/logo/愈心谷魔方3D_Smalljpg.jpg | Bin 0 -> 365371 bytes apps/user-h5/src/App.vue | 16 +- apps/user-h5/src/components/IcpFooter.vue | 40 +++ apps/user-h5/src/lib/mpEmbed.spec.ts | 24 ++ apps/user-h5/src/lib/mpEmbed.ts | 17 + apps/user-h5/src/main.ts | 3 + apps/user-h5/src/router/index.ts | 6 + apps/user-h5/src/styles/app.css | 5 +- docs/DEPLOY-MINI-PROGRAM-H5.md | 332 ++++++++++++++++++ packages/utils/src/icp.ts | 5 + packages/utils/src/index.ts | 2 + 14 files changed, 498 insertions(+), 7 deletions(-) create mode 100644 apps/admin-h5/src/components/IcpFooter.vue create mode 100644 apps/user-h5/public/logo/愈心谷魔方3D_Smalljpg.jpg create mode 100644 apps/user-h5/src/components/IcpFooter.vue create mode 100644 apps/user-h5/src/lib/mpEmbed.spec.ts create mode 100644 apps/user-h5/src/lib/mpEmbed.ts create mode 100644 docs/DEPLOY-MINI-PROGRAM-H5.md create mode 100644 packages/utils/src/icp.ts diff --git a/apps/admin-h5/src/components/IcpFooter.vue b/apps/admin-h5/src/components/IcpFooter.vue new file mode 100644 index 0000000..8ef61ce --- /dev/null +++ b/apps/admin-h5/src/components/IcpFooter.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/apps/admin-h5/src/layouts/AdminShell.vue b/apps/admin-h5/src/layouts/AdminShell.vue index 032bd58..b250ce7 100644 --- a/apps/admin-h5/src/layouts/AdminShell.vue +++ b/apps/admin-h5/src/layouts/AdminShell.vue @@ -2,6 +2,7 @@ import { onMounted } from 'vue' import { RouterLink, RouterView, useRouter } from 'vue-router' import BrandLogo from '@/components/BrandLogo.vue' +import IcpFooter from '@/components/IcpFooter.vue' import { useAuthStore } from '@/stores/auth' const auth = useAuthStore() @@ -55,7 +56,10 @@ async function onLogout() {
- +
+ +
+
@@ -91,7 +95,13 @@ nav a.router-link-active { } .foot { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; } .role { font-size: 0.75rem; color: var(--muted); } -.main { padding: 1.5rem 1.75rem; } +.main { + padding: 1.5rem 1.75rem; + display: flex; + flex-direction: column; + min-height: 100vh; +} +.main-body { flex: 1; min-width: 0; } @media (max-width: 800px) { .shell { grid-template-columns: 1fr; } .nav { border-right: 0; border-bottom: 1px solid var(--line); } diff --git a/apps/admin-h5/src/pages/LoginPage.vue b/apps/admin-h5/src/pages/LoginPage.vue index d40bb50..f012f1e 100644 --- a/apps/admin-h5/src/pages/LoginPage.vue +++ b/apps/admin-h5/src/pages/LoginPage.vue @@ -2,6 +2,7 @@ import { ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import BrandLogo from '@/components/BrandLogo.vue' +import IcpFooter from '@/components/IcpFooter.vue' import { useAuthStore } from '@/stores/auth' const auth = useAuthStore() @@ -46,11 +47,24 @@ async function onSubmit() { {{ loading ? '登录中…' : '登录' }} + diff --git a/apps/user-h5/src/lib/mpEmbed.spec.ts b/apps/user-h5/src/lib/mpEmbed.spec.ts new file mode 100644 index 0000000..c0ab93d --- /dev/null +++ b/apps/user-h5/src/lib/mpEmbed.spec.ts @@ -0,0 +1,24 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { initMpEmbed, isMpEmbed } from './mpEmbed' + +describe('mpEmbed', () => { + beforeEach(() => { + sessionStorage.clear() + window.history.replaceState({}, '', '/psy/') + }) + + afterEach(() => { + sessionStorage.clear() + }) + + it('persists embed flag from ?mp=1', () => { + window.history.replaceState({}, '', '/psy/?mp=1') + initMpEmbed() + expect(isMpEmbed()).toBe(true) + }) + + it('returns false without mp query', () => { + initMpEmbed() + expect(isMpEmbed()).toBe(false) + }) +}) diff --git a/apps/user-h5/src/lib/mpEmbed.ts b/apps/user-h5/src/lib/mpEmbed.ts new file mode 100644 index 0000000..ca0fb8b --- /dev/null +++ b/apps/user-h5/src/lib/mpEmbed.ts @@ -0,0 +1,17 @@ +const MP_EMBED_KEY = 'yxg_mp_embed' +const MP_QUERY = 'mp' + +/** Persist ?mp=1 from mini-program web-view entry. Call once before router mount. */ +export function initMpEmbed(): void { + if (typeof window === 'undefined') return + const params = new URLSearchParams(window.location.search) + if (params.get(MP_QUERY) === '1') { + sessionStorage.setItem(MP_EMBED_KEY, '1') + } +} + +/** True when H5 runs inside WeChat mini-program web-view (no H5 tab bar). */ +export function isMpEmbed(): boolean { + if (typeof window === 'undefined') return false + return sessionStorage.getItem(MP_EMBED_KEY) === '1' +} diff --git a/apps/user-h5/src/main.ts b/apps/user-h5/src/main.ts index ad32034..9c96807 100644 --- a/apps/user-h5/src/main.ts +++ b/apps/user-h5/src/main.ts @@ -3,6 +3,9 @@ import { createPinia } from 'pinia' import App from './App.vue' import router from './router' import { initAnalytics, trackPageView } from './lib/analytics' +import { initMpEmbed } from './lib/mpEmbed' + +initMpEmbed() import '@yuxingu/ui/tokens.css' import './styles/app.css' diff --git a/apps/user-h5/src/router/index.ts b/apps/user-h5/src/router/index.ts index 4c99cc6..7f630c5 100644 --- a/apps/user-h5/src/router/index.ts +++ b/apps/user-h5/src/router/index.ts @@ -1,4 +1,5 @@ import { createRouter, createWebHistory } from 'vue-router' +import { isMpEmbed } from '../lib/mpEmbed' const router = createRouter({ history: createWebHistory('/psy/'), @@ -48,4 +49,9 @@ const router = createRouter({ }, }) +router.beforeEach((to) => { + if (!isMpEmbed() || to.query.mp === '1') return true + return { path: to.path, query: { ...to.query, mp: '1' }, hash: to.hash, replace: true } +}) + export default router diff --git a/apps/user-h5/src/styles/app.css b/apps/user-h5/src/styles/app.css index 97c3b4b..a0c0a3b 100644 --- a/apps/user-h5/src/styles/app.css +++ b/apps/user-h5/src/styles/app.css @@ -14,9 +14,12 @@ body{ } .app-shell{ max-width:var(--yxg-max-w);margin:0 auto;min-height:100vh; - padding-bottom:calc(var(--yxg-nav-h) + env(safe-area-inset-bottom,0px) + 12px); + padding-bottom:calc(var(--yxg-nav-h) + env(safe-area-inset-bottom,0px) + 36px); position:relative; } +.app-shell.mp-embed{ + padding-bottom:calc(env(safe-area-inset-bottom,0px) + 36px); +} a{color:inherit;text-decoration:none} /* ── Page shell (测测式:暖色洗底 + 白 sheet) ── */ diff --git a/docs/DEPLOY-MINI-PROGRAM-H5.md b/docs/DEPLOY-MINI-PROGRAM-H5.md new file mode 100644 index 0000000..238080c --- /dev/null +++ b/docs/DEPLOY-MINI-PROGRAM-H5.md @@ -0,0 +1,332 @@ +# 愈心谷小程序 + 愈心魔方 H5 部署说明 + +适用于:**微信小程序**(`yuxingu-Miniprogram-dev`)嵌入 **愈心魔方 H5**(`digital-psychology/apps/user-h5`)的展示期上线。 + +--- + +## 1. 架构 + +``` +微信小程序(uni-app) +├── 首页 / 咨询 / 测评 / 我的 → https://miniapp.yuxingu.com.cn (Java,rp-server-psychic) +└── 愈心魔方 Tab / 首页入口 → https://h5.yuxingu.com.cn/psy/?mp=1 (WebView) + └── /psy/api/* → Go API(digital-psychology) +``` + +| 组件 | 仓库 | 域名 | +|------|------|------| +| 小程序客户端 | `yuxingu-Miniprogram-dev` | —(微信后台上传) | +| 愈心魔方 H5 | `digital-psychology/apps/user-h5` | `h5.yuxingu.com.cn` | +| Go API | `digital-psychology/apps/api` | 同域反代 `/psy/api/` | +| 旧业务 API | `rp-server-psychic`(独立部署) | `miniapp.yuxingu.com.cn` | + +H5 地址配置在小程序 `util/yxgConfig.js`: + +```js +export const YXG_H5_BASE = 'https://h5.yuxingu.com.cn/psy/' +export const YXG_H5_HOME_URL = `${YXG_H5_BASE}?mp=1` +``` + +--- + +## 2. 前置条件 + +### 2.1 域名与证书 + +| 域名 | 用途 | 要求 | +|------|------|------| +| `h5.yuxingu.com.cn` | H5 静态 + Go API 反代 | HTTPS、已备案 | +| `miniapp.yuxingu.com.cn` | 咨询/测评/登录(已有) | HTTPS | + +### 2.2 微信小程序后台 + +路径:**开发 → 开发管理 → 开发设置** + +| 配置项 | 域名 | +|--------|------| +| **业务域名**(WebView 必配) | `h5.yuxingu.com.cn`(需上传校验文件到站点根目录) | +| **request 合法域名** | `https://miniapp.yuxingu.com.cn`、`https://h5.yuxingu.com.cn` | +| uploadFile 合法域名 | `https://miniapp.yuxingu.com.cn`(维持现状) | +| downloadFile 合法域名 | `https://miniapp.yuxingu.com.cn`(维持现状) | +| socket 合法域名 | 无需配置 | + +### 2.3 服务器 + +- 一台可跑 Nginx + Go 二进制(或容器)的 VPS +- PostgreSQL(Go API 用;可与现有库同机或托管) +- Java 微服务 + `miniapp.yuxingu.com.cn` 保持现有部署,本说明不覆盖 + +--- + +## 3. Go API 部署 + +仓库:`digital-psychology` + +### 3.1 构建 + +```bash +cd /path/to/digital-psychology/apps/api +go test ./... +go build -o yuxingu-api ./cmd/server +``` + +### 3.2 配置 + +```bash +cp config.example.yaml config.local.yaml +# 编辑 config.local.yaml:database、deepseek.api_key(Ask 需要)等 +``` + +生产环境建议用环境变量覆盖敏感项,勿把密钥提交 Git。 + +### 3.3 数据库迁移 + +API 启动时会自动应用 `apps/api/migrations/`。首次部署前确保 PostgreSQL 已创建库和用户。 + +```bash +# 本地仅 DB 示例 +cd /path/to/digital-psychology +docker compose -f docker-compose.dev.yml up -d +``` + +### 3.4 运行(示例 systemd) + +```ini +# /etc/systemd/system/yuxingu-api.service +[Unit] +Description=Yuxingu Go API +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/yuxingu/apps/api +ExecStart=/opt/yuxingu/apps/api/yuxingu-api +Environment=APP_ENV=prod +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +API 默认监听 `:8080`(见 `config.local.yaml` 的 `http_addr`)。 + +### 3.5 健康检查 + +```bash +curl -sS http://127.0.0.1:8080/api/v1/healthz +# 期望:{"code":0,"data":{...}} +``` + +--- + +## 4. H5 构建与 Nginx 部署 + +### 4.1 构建静态资源 + +```bash +cd /path/to/digital-psychology +npm ci +npm run build:h5 +# 产物:apps/user-h5/dist/ +``` + +构建产物 `base` 为 `/psy/`,与线上路径一致,**无需改 `baseURL`**(客户端请求 `/psy/api/v1/...`)。 + +### 4.2 上传到服务器 + +```bash +rsync -avz --delete apps/user-h5/dist/ user@your-server:/var/www/yuxingu-h5/psy/ +``` + +目录结构示例: + +``` +/var/www/yuxingu-h5/psy/ +├── index.html +├── assets/ +└── ... +``` + +### 4.3 Nginx 配置(推荐:H5 + API 同域) + +```nginx +server { + listen 443 ssl http2; + server_name h5.yuxingu.com.cn; + + ssl_certificate /etc/ssl/h5.yuxingu.com.cn/fullchain.pem; + ssl_certificate_key /etc/ssl/h5.yuxingu.com.cn/privkey.pem; + + # 微信业务域名校验文件(按后台提示的文件名放置) + location = /MP_verify_xxxx.txt { + root /var/www/wechat-verify; + } + + # user-h5 SPA(history 模式) + location /psy/ { + alias /var/www/yuxingu-h5/psy/; + try_files $uri $uri/ /psy/index.html; + } + + # Go API 反代:/psy/api/v1/* → :8080/api/v1/* + location /psy/api/ { + rewrite ^/psy/api/(.*)$ /api/$1 break; + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # Ask 流式响应 + proxy_buffering off; + proxy_read_timeout 120s; + } + + location = / { + return 301 https://$host/psy/; + } +} +``` + +reload: + +```bash +sudo nginx -t && sudo systemctl reload nginx +``` + +### 4.4 H5 embed 模式说明 + +小程序 WebView 打开 `https://h5.yuxingu.com.cn/psy/?mp=1` 时: + +- H5 **隐藏底部 TabBar**(仅小程序底栏导航) +- 子页路由自动保留 `mp=1` 参数 +- 子页显示顶部返回键,不显示 H5 主导航 + +浏览器直访 `https://h5.yuxingu.com.cn/psy/`(无 `mp=1`)仍为完整 H5 体验(含 TabBar)。 + +--- + +## 5. 微信小程序部署 + +仓库:`yuxingu-Miniprogram-dev` + +### 5.1 开发工具构建 + +1. 用 **HBuilderX** 打开 `yuxingu-Miniprogram-dev` +2. 菜单:**发行 → 小程序-微信** +3. 或使用 CLI(需已安装 `@dcloudio/uni-cli` 等依赖): + +```bash +cd /path/to/yuxingu-Miniprogram-dev +# 具体命令以 HBuilderX / uni-app 版本为准 +npx uni build -p mp-weixin +``` + +编译产物目录:`unpackage/dist/build/mp-weixin/`(或 dev 目录) + +### 5.2 微信开发者工具 + +1. 导入上述 `mp-weixin` 目录 +2. AppID:`wx6782dd88e655f1a7`(见 `manifest.json`) +3. 预览 / 真机调试 + +### 5.3 上传体验版 / 正式版 + +1. 开发者工具 → **上传** +2. 登录 [微信公众平台](https://mp.weixin.qq.com/) → **管理 → 版本管理** +3. 将上传版本设为 **体验版** 供内部验收,或提交审核后发布 + +### 5.4 发布前检查(小程序侧) + +- [ ] `util/yxgConfig.js` 中 H5 地址为生产域名 +- [ ] `util/requestConfig.js` 中 Java API 仍为 `https://miniapp.yuxingu.com.cn` +- [ ] Tab 顺序:首页 | 愈心魔方 | 心理咨询 | 心理测评 | 我的 +- [ ] `static/n-menu/mf-*.png` 图标已替换为正式稿(当前可为占位图) + +--- + +## 6. 上线验证清单 + +### 6.1 服务端 + +```bash +# H5 首页 +curl -I https://h5.yuxingu.com.cn/psy/ + +# API 健康 +curl -sS https://h5.yuxingu.com.cn/psy/api/v1/healthz + +# embed 首页可访问 +curl -I 'https://h5.yuxingu.com.cn/psy/?mp=1' +``` + +### 6.2 微信开发者工具 / 真机 + +| 步骤 | 预期 | +|------|------| +| 打开小程序首页 | Banner、愈心魔方紫色入口条、测评/咨询卡片正常 | +| 点击「愈心魔方」入口 | 切到第 2 个 Tab,加载 H5 | +| 点击底部「愈心魔方」Tab | 同上 | +| H5 内点九宫格进子页(如问答) | 无 H5 底栏,有返回键,接口正常 | +| 咨询 / 测评 Tab | 仍走 miniapp 旧接口,登录与订单正常 | +| 未登录访问需登录能力 | 弹出微信手机号登录 | + +### 6.3 常见故障 + +| 现象 | 原因 | 处理 | +|------|------|------| +| WebView 白屏 | 业务域名未配或校验文件缺失 | 微信后台 → 业务域名 → 重新校验 | +| H5 内接口失败 | request 合法域名缺 `h5.yuxingu.com.cn` | 补充合法域名,等 5 分钟生效 | +| 404 on `/psy/ask` 刷新 | Nginx 未配 SPA fallback | 确认 `try_files` 指向 `/psy/index.html` | +| API 502 | Go 未启动或反代路径错误 | 查 `yuxingu-api` 日志与 `proxy_pass` | +| H5 仍显示底栏 | 未带 `mp=1` | 检查 `yxgConfig.js` 与 WebView URL | +| 双层底栏 | H5 未进 embed 模式 | 确认 URL 含 `?mp=1` 且已部署最新 H5 | + +--- + +## 7. 更新与回滚 + +### 仅更新愈心魔方(H5 + API) + +```bash +# 1. 构建 H5 +cd digital-psychology && npm run build:h5 +rsync -avz --delete apps/user-h5/dist/ user@server:/var/www/yuxingu-h5/psy/ + +# 2. 更新 API(需停服瞬间) +cd apps/api && go build -o yuxingu-api ./cmd/server +sudo systemctl restart yuxingu-api +``` + +小程序 **WebView 无缓存强制刷新**时,用户可能仍看到旧 H5;可在 URL 加版本 query(如 `?mp=1&v=20260824`)或在微信公众平台重新发布小程序。 + +### 仅更新小程序壳 + +重新 HBuilderX 发行 → 上传新版本。不影响 H5 与 API。 + +### 回滚 + +- H5:恢复 `/var/www/yuxingu-h5/psy/` 备份目录 +- API:`systemctl restart` 旧二进制 +- 小程序:版本管理 → 回退到上一审核版本 + +--- + +## 8. 仓库与路径速查 + +| 内容 | 路径 | +|------|------| +| 小程序 Tab / 首页入口 | `yuxingu-Miniprogram-dev/pages/yxg-magic/`、`pages/index/index.vue` | +| H5 地址常量 | `yuxingu-Miniprogram-dev/util/yxgConfig.js` | +| H5 embed 逻辑 | `digital-psychology/apps/user-h5/src/lib/mpEmbed.ts` | +| Go API | `digital-psychology/apps/api/cmd/server` | +| H5 构建命令 | `digital-psychology` 根目录 `npm run build:h5` | +| 本地开发 | Go:`cd apps/api && go run ./cmd/server`;H5:`npm run dev:h5` | + +--- + +## 9. 后续(非展示期) + +- 微信登录与 Go Auth 统一(单账号) +- Java 顾问/测评域逐步迁入 Go API +- CI 镜像发布见 `.ai/deployment.md`、`deploy/README.md` diff --git a/packages/utils/src/icp.ts b/packages/utils/src/icp.ts new file mode 100644 index 0000000..0ece78e --- /dev/null +++ b/packages/utils/src/icp.ts @@ -0,0 +1,5 @@ +/** 工信部 ICP 备案(全站 H5 / 后台页脚) */ +export const ICP_RECORD = '蜀ICP备2025140386号-2' + +/** 工信部备案查询入口 */ +export const ICP_MIIT_URL = 'https://beian.miit.gov.cn/' diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 221335d..4af7db0 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -25,3 +25,5 @@ export { type UserTextKind, type ValidateUserTextResult, } from './textsafe' + +export { ICP_MIIT_URL, ICP_RECORD } from './icp'