开发 / 生产双配置

This commit is contained in:
2026-09-19 00:52:04 +08:00
parent 1ff4dfffb9
commit de412297fb
17 changed files with 303 additions and 591 deletions
-14
View File
@@ -1,14 +0,0 @@
# 租户 / 应用配置
# 默认租户 ID(本地开发或演示站点用,生产环境通过域名自动识别)
NUXT_PUBLIC_TENANT_ID=10626
# 默认模板 ID
NUXT_PUBLIC_TEMPLATE_ID=template-07
# 本地开发:CMS / 内容接口指向本地后端(mp-java)
# 生产默认值见 nuxt.config.tshttps://cms-api.websoft.top/api
# 上线/联调远程时,把这两行注释掉或删掉即可
NUXT_PUBLIC_MODULES_API_BASE=http://127.0.0.1:9500/api
# 若本地后端同时提供图片/文件,可一并打开(默认走 server.websoft.top
# NUXT_PUBLIC_FILE_SERVER_BASE=http://127.0.0.1:9500
NUXT_PUBLIC_MODULES_API_BASE=http://127.0.0.1:9200/api
+40
View File
@@ -0,0 +1,40 @@
# ============================================================
# 本地开发配置(npm run dev
# ------------------------------------------------------------
# 该文件会被提交到 git,供所有开发者共享;个人差异请写进
# .env.local(不提交,优先级更高)。
# ============================================================
# ---------- 租户 / 模板 ----------
# 本地开发用固定租户,生产环境通过域名自动识别租户
NUXT_PUBLIC_TENANT_ID=10626
NUXT_PUBLIC_APP_ID=website
NUXT_PUBLIC_TEMPLATE_ID=template-07
# 本地调试用:强制指定模板,优先级高于应用/站点绑定。留空表示不覆盖
NUXT_PUBLIC_FORCE_TEMPLATE_ID=
# ---------- 后端接口 ----------
# 本地后端(hjc-java)默认端口 9200。
# 若要联调远程环境,把下面两行注释掉即可,nuxt.config.ts 会自动回落到生产地址
NUXT_PUBLIC_MODULES_API_BASE=http://127.0.0.1:9200/api
# 本地后端同时提供图片/文件时打开(默认走 https://server.websoft.top
# NUXT_PUBLIC_FILE_SERVER_BASE=http://127.0.0.1:9200
# 其余接口本地开发直接走远程(一般无需修改)
NUXT_PUBLIC_SERVER_API_BASE=https://server.websoft.top/api
NUXT_PUBLIC_APP_API_BASE=https://websopy-api.websoft.top
# ---------- 站点 / 多租户域名 ----------
NUXT_PUBLIC_BASE_DOMAIN=shoplnk.cn
NUXT_PUBLIC_BASE_DOMAINS=shoplnk.cn,sitelink.cn,wsdns.cn
NUXT_PUBLIC_SUBDOMAIN_PREFIXES=site,shop,store,mp,app,oa
# ---------- 可选覆盖(留空 = 由后台配置决定) ----------
NUXT_PUBLIC_SITE_NAME=
NUXT_PUBLIC_PHONE=
NUXT_PUBLIC_WX_QRCODE=
NUXT_PUBLIC_CONTACT_CAPTCHA=
# 订阅状态缓存秒数(仅服务端可见)
NUXT_SUBSCRIPTION_CACHE_TTL=60
+55
View File
@@ -0,0 +1,55 @@
# ============================================================
# 环境变量总览(参考模板)
# ------------------------------------------------------------
# 本项目的配置分三层,优先级从低到高:
# 1. .env.development / .env.production ← 提交到 git 的共享配置
# 2. .env ← 个人本地覆盖,已从 git 移除(不提交)
# 3. .env.local ← 优先级最高的个人覆盖(不提交)
#
# 常用命令:
# npm run dev # 开发,读取 .env.development
# npm run build:prod # 生产打包,读取 .env.production
#
# ⚠️ 所有 NUXT_PUBLIC_* 变量都会暴露给浏览器,不要放任何密钥。
# ============================================================
# ---------- 租户 / 应用 / 模板 ----------
# 固定租户 ID。生产环境留空,改由域名自动识别
NUXT_PUBLIC_TENANT_ID=
NUXT_PUBLIC_APP_ID=website
# 默认模板 ID
NUXT_PUBLIC_TEMPLATE_ID=
# 强制模板 ID:设置后优先级最高,覆盖应用/站点绑定(仅本地调试用)
NUXT_PUBLIC_FORCE_TEMPLATE_ID=
# ---------- 后端接口地址 ----------
# 内容/CMS 接口。生产为 https://hjc-api.websoft.top/api
NUXT_PUBLIC_MODULES_API_BASE=
# 主服务接口。生产为 https://server.websoft.top/api
NUXT_PUBLIC_SERVER_API_BASE=
# 应用/用户接口。生产为 https://websopy-api.websoft.top
NUXT_PUBLIC_APP_API_BASE=
# 图片/文件服务器。留空则从 SERVER_API_BASE 自动推导
NUXT_PUBLIC_FILE_SERVER_BASE=
# ---------- 多租户域名 ----------
# 兼容保留,实际取 BASE_DOMAINS 的第一个
NUXT_PUBLIC_BASE_DOMAIN=
# 多主域清单,逗号分隔
NUXT_PUBLIC_BASE_DOMAINS=
# 子域名前缀白名单(这些前缀 + 数字租户号 + 主域 才被识别为平台子域租户)
NUXT_PUBLIC_SUBDOMAIN_PREFIXES=
# ---------- 展示覆盖(留空 = 由后台配置决定) ----------
# 站点名称覆盖,留空则用后端 AppProduct / CMS 返回值
NUXT_PUBLIC_SITE_NAME=
# 联系电话覆盖,用于后端 phone 被脱敏时兜底
NUXT_PUBLIC_PHONE=
# 微信二维码覆盖。⚠️ 不要写死图片地址,留空让 Footer 从后台上传读取
NUXT_PUBLIC_WX_QRCODE=
# 留言验证码开关:true / false / 留空(由后台 setting.requireCaptcha 决定)
NUXT_PUBLIC_CONTACT_CAPTCHA=
# ---------- 服务端专用(不会暴露给浏览器) ----------
# 订阅状态缓存秒数
NUXT_SUBSCRIPTION_CACHE_TTL=60
+33
View File
@@ -0,0 +1,33 @@
# ============================================================
# 生产打包配置(npm run build:prod
# ------------------------------------------------------------
# ⚠️ 该文件会被提交到 git,只放「非敏感、稳定」的生产参数。
# 任何密钥类内容都不要写在这里。
# ============================================================
# ---------- 接口(生产必须指向线上) ----------
# 内容/CMS 接口统一使用 hjc-api 域名(原 cms-api.websoft.top 的 /api/* 已返回 404
NUXT_PUBLIC_MODULES_API_BASE=https://hjc-api.websoft.top/api
NUXT_PUBLIC_SERVER_API_BASE=https://server.websoft.top/api
NUXT_PUBLIC_APP_API_BASE=https://websopy-api.websoft.top
# 图片/文件服务器。留空则由 nuxt.config.ts 从 SERVER_API_BASE 推导出
# https://server.websoft.top,无需手写
NUXT_PUBLIC_FILE_SERVER_BASE=
# ---------- 站点 / 多租户域名 ----------
# 生产通过域名自动识别租户,因此不设置 TENANT_ID / TEMPLATE_ID
# 也不设置 FORCE_TEMPLATE_ID,避免写死租户导致所有域名串号
NUXT_PUBLIC_BASE_DOMAIN=shoplnk.cn
NUXT_PUBLIC_BASE_DOMAINS=shoplnk.cn,sitelink.cn,wsdns.cn
NUXT_PUBLIC_SUBDOMAIN_PREFIXES=site,shop,store,mp,app,oa
# ---------- 可选覆盖(留空 = 由后台配置决定) ----------
# 站名/电话/二维码一律留空,从后台 CMS 读取,便于运营自行修改
NUXT_PUBLIC_SITE_NAME=
NUXT_PUBLIC_PHONE=
NUXT_PUBLIC_WX_QRCODE=
# 留言验证码:留空表示由后台「网站设置」的 requireCaptcha 决定
NUXT_PUBLIC_CONTACT_CAPTCHA=
# 订阅状态缓存秒数(仅服务端可见)
NUXT_SUBSCRIPTION_CACHE_TTL=60
+6
View File
@@ -3,3 +3,9 @@ node_modules
.output
.data
dist
# 本地个人覆盖配置,不进版本库
# 注意:.env.development / .env.production / .env.example 需要提交,故不在此忽略
.env
.env.local
.env.*.local
+1 -1
View File
@@ -1,7 +1,7 @@
# 2026-09-08
## 本地开发接口切换(cms-api → 127.0.0.1:9500
- 需求:把 CMS/内容接口从 `https://cms-api.websoft.top/api` 切到本地 `http://127.0.0.1:9500/api`
- 需求:把 CMS/内容接口从 `https://hjc-api.websoft.top/api` 切到本地 `http://127.0.0.1:9500/api`
- 改法:只动 `.env`,未改 `nuxt.config.ts` 默认值(保持生产配置不变)。新增 `NUXT_PUBLIC_MODULES_API_BASE=http://127.0.0.1:9500/api`NUXT_ 前缀环境变量会覆盖 `runtimeConfig.public.modulesApiBase`
- 关联:`modulesApiBase` 被 server/api 下 site/info、page/*、article/*、product/*、case/*、banner、form/submit、sitemap、domain/resolve 共用;文件/图片走另一个变量 `fileServerBase`(默认 origin(serverApiBase)=server.websoft.top),已在 .env 留注释开关。
- 顺带修复:原 `.env``NUXT_PUBLIC_TENANT_ID` / `NUXT_PUBLIC_TEMPLATE_ID` 两行有前导空格,dotenv 无法解析 → 配置一直没生效。已去除空格(tenant 10626 / template-07 将真正生效,若之前靠默认 template-01 调试需注意行为变化)。
+52 -7
View File
@@ -43,6 +43,41 @@ npm run dev
默认访问:http://localhost:3000
## 环境变量与打包
项目已按环境拆分配置文件,**打生产包不再需要手改 `.env`**
| 命令 | 用途 | 读取的配置 |
|---|---|---|
| `npm run dev` | 本地开发 | `.env.development` |
| `npm run build:prod` | 生产打包 | `.env.production` |
| `npm run build` | 等同 `build:prod` | `.env.production` |
| `npm run build:dev` | 用开发配置打包(排查用) | `.env.development` |
| `npm run dev:prod` | 以生产配置本地起服务(排查用) | `.env.production` |
配置文件说明:
| 文件 | 是否提交 | 说明 |
|---|---|---|
| `.env.development` | ✅ | 团队共享的开发配置,接口指向本地后端 |
| `.env.production` | ✅ | 团队共享的生产配置,接口指向线上 |
| `.env.example` | ✅ | 变量总览模板,新增变量时同步更新 |
| `.env.local` | ❌ | 个人通用覆盖 |
| `.env.<环境>.local` | ❌ | 个人针对某环境的覆盖,优先级最高 |
| `.env` | ❌ | 历史遗留的个人配置,优先级最低 |
优先级(高 → 低):
**进程环境变量 > `.env.<环境>.local` > `.env.<环境>` > `.env.local` > `.env`**
只有「变量尚未定义」时才会被下一层填充,因此高优先级不会被低优先级顶掉,
`.env` 里残留的本地地址也不会再污染生产包。
发布前可自查产物是否干净:
```bash
grep -rl "127.0.0.1" .output/ || echo "产物干净,无本地地址"
```
## 模板系统
模板存放在 `app/templates/` 目录下,每个模板独立目录,包含:
@@ -56,20 +91,30 @@ npm run dev
- `template-01`:科技蓝企业模板(完整)
- `template-02` ~ `template-04`:占位模板,后续根据设计稿实现
## 环境变量
## 环境变量清单
复制 `.env.example``.env`,按需修改
完整变量说明见 `.env.example`。核心变量
```bash
NUXT_PUBLIC_TENANT_ID=10398
# 租户 / 模板(生产留空,由域名自动识别)
NUXT_PUBLIC_TENANT_ID=
NUXT_PUBLIC_APP_ID=website
NUXT_PUBLIC_TEMPLATE_ID=template-01
NUXT_PUBLIC_TEMPLATE_ID=
# 接口地址
NUXT_PUBLIC_SERVER_API_BASE=https://server.websoft.top/api
NUXT_PUBLIC_MODULES_API_BASE=https://cms-api.websoft.top/api
NUXT_PUBLIC_APP_API_BASE=https://cms-api.websoft.top
NUXT_PUBLIC_BASE_DOMAIN=shoplnk.cn
NUXT_PUBLIC_MODULES_API_BASE=https://hjc-api.websoft.top/api
NUXT_PUBLIC_APP_API_BASE=https://websopy-api.websoft.top
# 多租户域名
NUXT_PUBLIC_BASE_DOMAINS=shoplnk.cn,sitelink.cn,wsdns.cn
```
> ️ `nuxt.config.ts` 内置的 `NUXT_PUBLIC_MODULES_API_BASE` 默认值已统一为
> `https://hjc-api.websoft.top/api`(原 `cms-api.websoft.top` 域名 `/api/*` 已返回 404
> 故全面切换到 `hjc-api`)。`.env.production` 中也显式指定同一地址。
> 若将来再次更换域名,请同步修改 `nuxt.config.ts`、`.env.production` 与 `.env.example`。
## 接口说明
本项目通过 `server/api/` 代理转发 SaaS 后端接口,前端不直接调用后端地址。
+1 -1
View File
@@ -8,7 +8,7 @@
后台 website-admin `_websopy` 代理 → `websopy-api.websoft.top` = **websopy-java**
`CmsTemplateController.use()``loginUser.getTenantId()``cms_website.template_id`
- **读路径** `GET /cms/cms-website/getSiteInfo`
公开站 `modulesApiBase` 与 后台 `_cms` 代理 → `cms-api.websoft.top` = **mp-java**
公开站 `modulesApiBase` 与 后台 `_cms` 代理 → `hjc-api.websoft.top` = **mp-java**
返回 `ShopVo`(站点信息),被 Redis 缓存 1 天(key `SiteInfo:{tenantId}`)。
- 两后端**共享同一 Redis**prod `1Panel-redis-Q1LE`dev `47.119.165.234`,同密码),
故 websopy-java 的 `use()` 清缓存能清掉 mp-java 读的 `SiteInfo:{tenantId}`
+3 -3
View File
@@ -28,7 +28,7 @@ const serverApiBase =
'https://server.websoft.top/api'
const modulesApiBase =
process.env.NUXT_PUBLIC_MODULES_API_BASE ||
'https://cms-api.websoft.top/api'
'https://hjc-api.websoft.top/api'
const appApiBase =
process.env.NUXT_PUBLIC_APP_API_BASE ||
'https://websopy-api.websoft.top'
@@ -81,9 +81,9 @@ export default defineNuxtConfig({
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
// 提前与内容/接口源站建立连接,降低首字节与图片加载延迟
{ rel: 'preconnect', href: 'https://server.websoft.top', crossorigin: '' },
{ rel: 'preconnect', href: 'https://cms-api.websoft.top', crossorigin: '' },
{ rel: 'preconnect', href: 'https://hjc-api.websoft.top', crossorigin: '' },
{ rel: 'dns-prefetch', href: 'https://server.websoft.top' },
{ rel: 'dns-prefetch', href: 'https://cms-api.websoft.top' },
{ rel: 'dns-prefetch', href: 'https://hjc-api.websoft.top' },
// 思源黑体(Noto Sans SC,SIL 开源许可,版权无忧)网页字体,全站统一字体
{ rel: 'preconnect', href: 'https://fonts.googleapis.cn' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.cn', crossorigin: '' },
File diff suppressed because one or more lines are too long
@@ -43,7 +43,7 @@
| `server/api/payment/query.get.ts` | `tenderApiBase/payment/query` | 支付状态查询(复用现有支付查询) |
| `server/api/supplier/login.post.ts` 等 | `tenderApiBase/...` | 仅当采用账号密码注册登录时新增 |
> **双后端兼容**:所有 `tenderApiBase` 走统一 runtimeConfig`NUXT_TENDER_API_BASE`),默认 `https://cms-api.websoft.top/api`;若确认交易走 guilixu-java,改 env 即可,前端代码不变。租户头 `TenantId`、JWT 透传沿用现有中间件。
> **双后端兼容**:所有 `tenderApiBase` 走统一 runtimeConfig`NUXT_TENDER_API_BASE`),默认 `https://hjc-api.websoft.top/api`;若确认交易走 guilixu-java,改 env 即可,前端代码不变。租户头 `TenantId`、JWT 透传沿用现有中间件。
---
+6 -4
View File
@@ -4,10 +4,12 @@
"private": true,
"packageManager": "pnpm@9.15.9",
"scripts": {
"build": "node --import ./scripts/crypto-hash-polyfill.mjs ./node_modules/nuxt/bin/nuxt.mjs build",
"build:staging": "dotenv -e .env.staging -- node --import ./scripts/crypto-hash-polyfill.mjs ./node_modules/nuxt/bin/nuxt.mjs build",
"dev": "node --import ./scripts/crypto-hash-polyfill.mjs ./node_modules/nuxt/bin/nuxt.mjs dev",
"dev:staging": "dotenv -e .env.staging -- node --import ./scripts/crypto-hash-polyfill.mjs ./node_modules/nuxt/bin/nuxt.mjs dev",
"build": "node --import ./scripts/crypto-hash-polyfill.mjs --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs build --env-name=production",
"build:prod": "node --import ./scripts/crypto-hash-polyfill.mjs --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs build --env-name=production",
"build:dev": "node --import ./scripts/crypto-hash-polyfill.mjs --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs build --env-name=development",
"dev": "node --import ./scripts/crypto-hash-polyfill.mjs --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs dev --env-name=development",
"dev:prod": "node --import ./scripts/crypto-hash-polyfill.mjs --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs dev --env-name=production",
"preview:prod": "node --import ./scripts/crypto-hash-polyfill.mjs --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs preview --env-name=production",
"generate": "node --import ./scripts/crypto-hash-polyfill.mjs ./node_modules/nuxt/bin/nuxt.mjs generate",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
+1 -1
View File
@@ -7,7 +7,7 @@ import os, json, sys, subprocess, time, urllib.request, urllib.error
TOKEN = os.environ.get("TOKEN", "")
UPLOAD = "https://server.websoft.top/api/file/upload"
CMS = "https://cms-api.websoft.top/api"
CMS = "https://hjc-api.websoft.top/api"
FILE_BASE = "https://file.websoft.top/api/file/"
HDR = {"TenantId": "10626", "Authorization": "Bearer " + TOKEN}
+1 -1
View File
@@ -25,7 +25,7 @@
* ============================================================================
*/
const API_BASE = process.env.CMS_API_BASE || 'https://cms-api.websoft.top/api'
const API_BASE = process.env.CMS_API_BASE || 'https://hjc-api.websoft.top/api'
const TOKEN_RAW = process.env.ADMIN_TOKEN || ''
const TENANT_ID = process.env.TENANT_ID || '10546'
const APPLY = process.env.APPLY === '1'
+102
View File
@@ -0,0 +1,102 @@
/**
* 按环境加载 .env 文件,然后启动 Nuxt。
*
* 用法(推荐,跨平台):
* node --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs dev --env-name=development
* node --import ./scripts/with-env.mjs ./node_modules/nuxt/bin/nuxt.mjs build --env-name=production
*
* 为什么不用 `NUXT_ENV=production node ...` 这种内联赋值:
* npm 在 Windows 上通过 cmd.exe 执行脚本,`FOO=bar cmd` 不是合法语法,
* 会报 "'FOO' is not recognized as an internal or external command"。
* 也不要写成 `--import ./with-env.mjs .env.production --`Node 会把
* `--import` 之后、`--` 之前的每个裸参数都当成要加载的模块,
* `.env.production` 会触发 ERR_UNKNOWN_FILE_EXTENSION。
* 因此改用命令行参数 `--env-name=<name>` 传参,平台无关。
*
* 加载顺序(先加载的优先级更高,已存在的变量不会被后续文件覆盖):
* 1. 进程已有的环境变量 ← 最高(CI / 命令行传入)
* 2. .env.<name>.local ← 个人针对某环境的覆盖
* 3. .env.<name>development / production ← 团队共享配置
* 4. .env.local ← 个人通用覆盖
* 5. .env ← 最低(历史遗留的个人配置)
*
* 说明:Nuxt 内部通过 c12 读取 .env,而 c12 只在变量「尚未存在」时才写入
* process.env,因此这里先注入的值会被保留,不会被打包过程顶掉。
*/
import { existsSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
/** 解析 .env 文本,返回键值对。支持 export 前缀、引号与 # 注释。 */
function parseEnv (text) {
const out = {}
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine.trim()
if (!line || line.startsWith('#')) continue
const eq = line.indexOf('=')
if (eq === -1) continue
const key = line.slice(0, eq).trim().replace(/^export\s+/, '')
if (!key) continue
let value = line.slice(eq + 1).trim()
// 去掉成对的引号(单引号内不做转义处理)
if (
(value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)
) {
const quote = value[0]
value = value.slice(1, -1)
if (quote === '"') {
value = value
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t')
}
} else {
// 未加引号时,行尾注释按 dotenv 规则去掉(# 前需有空白)
value = value.replace(/\s+#.*$/, '').trim()
}
out[key] = value
}
return out
}
/** 读取文件并注入 process.env,已存在的键不覆盖。 */
function apply (file) {
const abs = resolve(process.cwd(), file)
if (!existsSync(abs)) return false
for (const [key, value] of Object.entries(parseEnv(readFileSync(abs, 'utf8')))) {
if (process.env[key] === undefined) process.env[key] = value
}
return true
}
// 从命令行参数中取 --env-name=<name>
const envArgIndex = process.argv.findIndex(a => a.startsWith('--env-name='))
const envName = envArgIndex === -1
? ''
: process.argv[envArgIndex].slice('--env-name='.length)
// 读完后从 argv 中移除,避免把这个自定义参数透传给 Nuxt(nuxi 只认 --envName
if (envArgIndex !== -1) process.argv.splice(envArgIndex, 1)
// 高优先级在前:由于「已存在则不覆盖」,先加载者胜出。
// 环境专属文件必须排在 .env 之前,否则历史遗留的 .env 会顶掉生产配置。
const files = []
if (envName) files.push(`.env.${envName}.local`)
if (envName) files.push(`.env.${envName}`)
files.push('.env.local')
files.push('.env')
files.forEach(apply)
if (envName && !existsSync(resolve(process.cwd(), `.env.${envName}`))) {
console.warn(
`[with-env] 警告:未找到 .env.${envName},将使用 nuxt.config.ts 内置默认值`
)
}
// 本模块只负责准备环境变量;Nuxt 由 Node 在加载本文件后继续执行,
// 两者同进程,因此 process.env 的修改对 Nuxt 可见。
-77
View File
@@ -1,77 +0,0 @@
# 单页管理(cms_page)与导航(cms_navigation)关联对接说明
> 面向后端 / website-admin「单页管理」模块负责人。
> 目标:把"菜单结构"与"单页内容"解耦又关联——导航只管菜单层级,单页管理只管正文,二者用 URL(slug)挂钩。
---
## 1. 约定结论(已与前端确认)
| 项 | 约定 |
|---|---|
| **内容唯一源** | 新建的单页内容一律建在「单页管理」(`cms_page`),按 `slug` 存取;导航节点不再存放单页正文(`design.content` 仅保留给旧租户兼容)。 |
| **规范 URL** | 单页对外地址统一为 `/page/{slug}`,例如 `/page/about-us`。 |
| **About / Contact** | `/about``/contact` 为独立富组件路由,是这两个页面的**规范地址**;`/page/about``/page/contact` 仅作兼容入口,前端已 301 跳到顶层(见 §4)。 |
| **导航关联方式** | 后台建菜单关联单页时,把 `nav.path` 写成 `/page/{slug}`(前端已支持,无需额外接口)。`CmsNavigation.pageId` 作为后端对账键保留。 |
| **旧数据兼容** | `/page/{数字}`(数字 navigationId)仍走旧 `cms_navigation + design.content`,不破坏旧租户。 |
---
## 2. 前端已具备的能力(后端无需改动即可生效)
- `getNavLink(nav)`:当 `nav.model === 'page'``nav.path``/page/` 开头时,直接返回该 path,**不会追加 `?navId=`**,单页菜单 URL 干净。
- `app/pages/page/[id].vue`
- 非数字 slug → 调 `/api/page/detail?path={slug}` → 取 `cms_page` 已发布单页正文。
- slug 为 `about` / `contact` → 301 收敛到 `/about``/contact`
- 列表/详情类栏目(article/product/case)维持原 `?navId=` 行为,不受影响。
---
## 3. 后台需要配合的 3 件事
### 3.1 单页管理创建页面时生成 slug
- `cms_page.slug` 必须**全局唯一、SEO 友好**(建议英文小写 + 连字符,如 `about-us``privacy-policy`)。
- 同一站点内 slug 不可重复;应做唯一性校验并报错提示。
- 不建议用中文或含空格的 slug(不利于 URL 与 SEO)。
### 3.2 导航菜单关联单页时填 path
两种方式任选其一(推荐方式 A,零成本):
- **方式 A(推荐)**:在「单页管理」与导航建立关联时,把菜单 `nav.path` 直接写为 `/page/{slug}`。前端拿到即可跳转,无需新增接口。
- **方式 B**:只填 `nav.pageId = cms_page.id`,由后台在返回导航树时**回写** `nav.path = /page/{slug}`(需后台保证返回前已解析)。前端不感知 pageId。
> 无论哪种方式,最终下发给前端的 `nav.path` 必须是 `/page/{slug}` 形态。
### 3.3 关联/解绑时的数据一致性
- 单页被删除或 slug 变更时,应同步清理或更新引用它的导航 `nav.path`,避免产生死链(前端遇到无效 slug 会渲染空状态,但死链对 SEO 不友好)。
- 建议后台在单页管理列表页提供"已绑定菜单"反查,便于排查。
---
## 4. 路由收敛规则(前端已落实,供后端理解)
| 访问地址 | 行为 |
|---|---|
| `/page/{slug}` | 正常渲染 cms_page 单页内容(slug 非 about/contact |
| `/page/{数字}` | 旧逻辑:按 navigationId 取 cms_navigation 正文(兼容旧租户) |
| `/page/about` | 301 → `/about`(独立富组件,规范地址) |
| `/page/contact` | 301 → `/contact`(独立富组件,规范地址) |
| `/about``/contact` | 直接渲染独立富组件 |
---
## 5. 待办(需前后端共同确认,前端暂未做)
- **动态 sitemap**:当前站点地图未纳入 `cms_page` 的 slug。需后台提供"当前站点全部已发布单页 slug 列表"接口(或 site info 携带),前端再补充到 sitemap。
- **顶层兜底层清理**`app/pages/[slug].vue` 旧兜底层可能与 `/page/{slug}` 产生 SEO 重复,后续建议收窄或移除(低优先级,不影响当前功能)。
---
## 6. 对接自查清单
- [ ] 单页管理创建页面 → 生成唯一 SEO slug ✅
- [ ] 导航关联单页 → `nav.path` 输出为 `/page/{slug}`
- [ ] slug 删除/变更 → 同步更新导航 path ✅
- [ ] 单页访问 → `/page/{slug}` 正常出内容 ✅
- [ ] about/contact 访问 → `/about``/contact`(非 `/page/about`)✅
- [ ] sitemap 是否需纳入 cms_page(待排期)⬜
-480
View File
@@ -1,480 +0,0 @@
# 通用企业官网程序开发计划
> 项目目标:基于 PC 端 + H5 响应式,开发一套可复用于多租户 SaaS 的企业官网**纯前端展示系统**,支持模板切换、租户隔离、独立域名绑定、订阅过期校验等能力。
> **项目定位**:本项目仅为官网前台展示,不含任何后端管理端代码。所有数据与业务能力均通过调用自研 SaaS 后端 API 实现。
> 当前阶段:方案规划(暂不写代码)
---
## 一、项目背景与现状分析
### 1.1 项目定位
本项目是一个**纯前端展示项目**
- ✅ 本项目负责:官网页面渲染、模板展示、SEO/SSR、H5 响应式、域名路由
- ❌ 本项目不含:后端管理端、CMS 内容管理后台、租户/应用管理控制台
- 📡 数据来源:统一调用自研 SaaS 后端(Java 多租户系统)提供的 API 接口
管理端能力(模板选择、内容编辑、域名绑定、订阅管理等)由 SaaS 后端的管理后台负责,本项目只消费其 API。
### 1.2 现有资源
| 资源 | 路径 | 说明 |
| ------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| 模板预览 | `/Users/gxwebsoft/VUE/website/templates/cloud-website/template-01.png` ~ `template-04.png` | 4 套企业官网模板效果图(静态设计稿) |
| 参考项目 | `/Users/gxwebsoft/VUE/websopy-pc` | 基于 Nuxt 4 + Vue 3 + TypeScript 的项目,可参考其技术栈、代理模式、租户 header 传递机制 |
| SaaS 后端 | 自研 Java 多租户系统 | 提供租户、应用、CMS 站点数据、订阅管理等 API 接口 |
### 1.3 参考项目 `websopy-pc` 技术栈评估
| 维度 | 现状 | 是否适合参考 |
| ------ | -------------------------------- | ----------------------------- |
| 框架 | Nuxt 4.2 + Vue 3 + TS | ✅ 适合:Nuxt 原生 SSR,对 SEO/GEO 友好 |
| UI 库 | Ant Design Vue | ⚠️ 偏后台,官网前台不使用,模板内自定义样式 |
| 用途 | 管理控制台、开发者中心、租户后台 | ❌ 本项目不包含管理端,仅参考技术栈与代理思路 |
| 已有能力 | 多租户 header、CMS 站点 API、订阅 API、代理层 | ✅ 参考其接口调用方式和代理模式 |
| SEO 支持 | 已有 `usePageSeo` composable,可扩展 | ✅ 参考其 SEO 实现思路 |
**结论**:本项目采用与 `websopy-pc` 相同的 Nuxt 4 + Vue 3 + TS 技术栈,但作为**独立的纯前端展示项目**开发。参考 `websopy-pc` 的 Server API 代理模式、`runtimeConfig` 配置、租户 header 传递机制,但不复用其管理端代码。
### 1.3 核心需求拆解
1. **模板切换**:多套官网模板可配置切换,不同租户/应用使用不同模板。
2. **多租户接入**:通过租户 IDTenantId)和应用 IDAppId)从 SaaS 后端拉取站点配置、页面数据、菜单、内容等。
3. **SEO + GEO**:服务端渲染(SSR)、动态 TDK、结构化数据(JSON-LD)、语义化 HTML、站点地图(Sitemap)、robots 等。
4. **H5 支持**:响应式布局,一套代码同时适配 PC 和 H5。
5. **独立域名绑定**
- 默认二级域名:`https://site-[租户ID].shoplnk.cn`
- 支持反向代理绑定顶级域名(如 `www.example.com`)。
6. **订阅过期校验**:通过应用订阅接口判断网站是否已过期,过期后引导续费。
---
## 二、技术栈选择
| 层级 | 技术 | 选型理由 |
| ----- | ------------------------------ | ------------------------------------------------ |
| 前端框架 | **Nuxt 4**Vue 3 + TypeScript | 原生 SSR/SSGSEO/GEO 友好;同 `websopy-pc` 技术栈,团队学习成本低 |
| 样式方案 | **Tailwind CSS** | 与 `websopy-pc` 一致,响应式能力强,适合多模板隔离 |
| UI 组件 | 模板内自定义 + **Shadcn/Vue** 或轻量组件库 | 官网模板风格差异大,避免强绑定单一组件库 |
| 状态管理 | Pinia / Nuxt 内置 `useState` | 简单场景用 `useState`,复杂用 Pinia |
| 请求库 | `ofetch`(Nuxt 内置) | 与 `websopy-pc``server/api` 代理模式一致 |
| 后端代理 | Nuxt **Server / Nitro** | 隐藏真实 API 域名,统一处理租户 header、缓存、鉴权 |
| 部署 | Node.js + Nginx 反向代理 | 支持自定义域名、SSL、多租户路由分发 |
---
## 三、整体架构设计
```
┌──────────────────────────────────────────────────────────────────────┐
│ 用户访问层 │
│ 默认域名: https://site-[tenantId].shoplnk.cn │
│ 自定义域名: https://www.example.com → Nginx 反向代理 │
└──────────────────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Nginx 网关层 │
│ • 根据 Host 解析租户 / 应用 ID │
│ • 自定义域名 → 查询域名-租户映射表 → 转发到 Nuxt 服务 │
│ • SSL 证书管理(通配符或单域名) │
└──────────────────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Nuxt 官网前台服务(Nitro
│ • Middleware:根据 Host/Header 识别当前租户与应用 │
│ • Server API:代理 SaaS 后端接口(站点信息、页面、订阅、文件等) │
│ • SSR 渲染:动态生成页面 HTML + SEO Meta + JSON-LD │
│ • 模板引擎:根据模板 ID 加载对应模板组件与样式 │
└──────────────────────────────────┬─────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 自研 SaaS 后端(Java
│ • 租户管理 / 应用管理 / 订阅管理 │
│ • CMS 站点数据:站点配置、页面、栏目、文章、产品、案例等 │
│ • 订阅接口:判断应用是否过期、续费引导 │
└──────────────────────────────────────────────────────────────────────┘
```
---
## 四、目录结构规划
本项目即为 `/Users/gxwebsoft/VUE/website`,纯前端展示项目,结构如下:
```
website/
├── app/
│ ├── components/ # 公共组件(SEO、Header、Footer、Loading、Renewal
│ ├── composables/ # 组合式函数
│ │ ├── useSite.ts # 获取当前站点信息
│ │ ├── useTenant.ts # 租户/应用识别
│ │ ├── usePageSeo.ts # SEO Meta 设置
│ │ ├── useSubscription.ts # 订阅状态校验
│ │ └── useTemplate.ts # 模板加载与切换
│ ├── layouts/ # 布局(default、blank、renewal
│ ├── pages/ # 页面路由
│ │ ├── index.vue # 首页
│ │ ├── [slug].vue # 动态 CMS 页面(关于我们、产品中心...)
│ │ ├── news/ # 新闻资讯列表/详情
│ │ ├── products/
│ │ ├── cases/
│ │ └── renewal.vue # 续费引导页
│ ├── templates/ # 模板集合(核心)
│ │ ├── template-01/ # 模板 1:科技感蓝
│ │ │ ├── index.vue # 模板首页
│ │ │ ├── components/ # 模板私有组件
│ │ │ ├── pages/ # 模板私有页面布局
│ │ │ ├── config.ts # 模板配置(名称、预览图、支持模块)
│ │ │ └── theme.css # 模板主题变量
│ │ ├── template-02/
│ │ ├── template-03/
│ │ └── template-04/
│ ├── api/ # 前端业务请求(可选,也可直接调 server/api
│ ├── types/ # TS 类型定义
│ ├── utils/ # 工具函数
│ ├── plugins/ # 插件
│ ├── app.vue # 根入口
│ └── error.vue # 错误页
├── server/
│ ├── api/ # 服务端代理 API
│ │ ├── site/ # 站点信息
│ │ ├── page/ # 页面内容
│ │ ├── subscription/ # 订阅状态
│ │ ├── file/ # 文件代理
│ │ └── sitemap.xml.ts # 动态站点地图
│ ├── middleware/ # 服务端中间件
│ │ └── tenant.ts # 租户识别、域名解析、订阅校验
│ └── utils/ # 服务端工具函数
├── public/ # 静态资源
├── nuxt.config.ts # Nuxt 配置
├── tailwind.config.cjs # Tailwind 配置
├── package.json
├── .env.example # 环境变量示例
└── README.md
```
---
## 五、关键模块设计方案
### 5.1 模板切换机制
模板是核心竞争力,需要设计为“可插拔”结构:
1. **模板注册表**:每个模板在 `templates/[template-id]/config.ts` 中声明:
- `id``name``description``preview`(预览图路径)
- `supportedModules`:该模板支持的功能模块(首页、关于、产品、案例、新闻、联系、留言)
- `themeConfig`:主题色、字体、间距等变量
- `layouts`:模板提供的布局组件
2. **模板加载**:根据后端返回的 `templateId` 动态 `import()` 对应模板组件。
3. **模板隔离**
- CSS 使用 `data-template-id` 命名空间或 CSS Modules 隔离,避免模板间样式冲突。
- 模板组件只负责渲染,数据由 `useSite()` 统一提供。
4. **预览与切换**:模板预览和切换能力由 SaaS 后端管理后台提供,本项目根据后端返回的 `templateId` 动态渲染即可。
### 5.2 多租户识别策略
Nuxt Server Middleware 中统一识别当前租户,优先级:
1. **自定义域名**Host 不是 `*.shoplnk.cn` 时,查询 SaaS 后端“域名绑定表”获取 `tenantId` + `appId`
2. **默认二级域名**`site-[tenantId].shoplnk.cn` → 从 Host 解析 `tenantId`
3. **环境变量默认**:用于本地开发或默认演示站点。
4. **Header 兜底**`TenantId` / `AppId` Header(用于调试或特殊场景)。
识别结果写入 `event.context.tenant``event.context.app`,供 SSR 渲染和 Server API 使用。
### 5.3 SaaS 后端接口对接
本项目作为纯前端展示层,所有数据均通过调用 SaaS 后端 API 获取。参考 `websopy-pc` 的代理模式,在 Nuxt Server 层统一转发,隐藏真实后端地址:
| 能力 | 参考来源 | 本项目实现 |
| ---- | ---------------------------------------------------------- | ------------------------------------------ |
| 站点信息 | `websopy-pc/server/api/cms/cms-website/getSiteInfo.get.ts` | `server/api/site/info.get.ts` 代理 |
| 页面列表 | `websopy-pc/server/api/cms/cms-website/pageAll.get.ts` | `server/api/page/all.get.ts` 代理 |
| 订阅状态 | SaaS 后端订阅接口 | `server/api/subscription/status.get.ts` 代理 |
| 域名映射 | SaaS 后端域名绑定查询 | `server/api/domain/resolve.get.ts` 代理 |
| 文件代理 | `websopy-pc/server/api/_file/[...path].ts` | `server/api/file/[...path].ts` 代理 |
| 表单提交 | SaaS 后端留言/表单接口 | `server/api/form/submit.post.ts` 代理 |
统一在服务端转发 `TenantId` 和必要的鉴权信息,避免前端暴露真实后端地址和鉴权细节。
### 5.4 SEO + GEO 方案
1. **SSR 渲染**:Nuxt 默认 SSR,确保搜索引擎和 AI 爬虫能拿到完整 HTML。
2. **动态 TDK**:每页根据 CMS 数据设置 `title``description``keywords`
3. **Open Graph / Twitter Card**:每页动态设置 `og:title``og:description``og:image``og:url`
4. **Canonical URL**:避免重复内容,每个页面设置 canonical 链接。
5. **结构化数据(JSON-LD**
- `Organization` 企业信息
- `WebSite` 站点信息
- `WebPage` 页面信息
- `Product``Article``BreadcrumbList`
6. **Sitemap**`server/api/sitemap.xml.ts` 根据站点页面动态生成。
7. **Robots**:根据环境配置 `robots` meta,开发/测试环境禁止抓取。
8. **语义化 HTML**:模板组件使用 `<header>``<nav>``<main>``<article>``<footer>` 等标签。
9. **URL 设计**:简洁语义化,如 `/about``/products``/news/[id]`
### 5.5 H5 响应式
- 模板设计采用 **Mobile First** 原则。
- 使用 Tailwind 断点:`sm: md: lg: xl:`
- 导航组件在 H5 下自动切换为汉堡菜单。
- 图片、视频使用响应式尺寸,避免移动端加载过大资源。
- 针对 H5 独立优化首屏加载性能(Lazy loading、资源预加载)。
### 5.6 独立域名绑定
#### 5.6.1 默认二级域名
- 格式:`https://site-[tenantId].shoplnk.cn`
- 在 Nginx 配置泛域名解析:`*.shoplnk.cn` → Nuxt 服务。
- Server Middleware 从 Host 中解析 `tenantId`
#### 5.6.2 自定义顶级域名
- 用户在 SaaS 后台绑定域名 `www.example.com`
- SaaS 后端记录“域名 → 租户 + 应用”映射。
- Nginx 监听 `www.example.com`(或默认 `server_name` 兜底),转发到 Nuxt 服务。
- Nuxt Server Middleware 通过域名查询后端映射表获取租户信息。
- SSL 证书:建议使用 Nginx 统一管理,可采用 Let's Encrypt 自动化证书,或 SaaS 后台提供证书上传。
### 5.7 订阅过期校验与续费引导
1. **订阅校验位置**
- Nuxt Server Middleware:每次请求先校验当前应用的订阅状态。
- 前端挂载时:再次校验(防止 SSR 后状态变化)。
2. **过期状态处理**
- 未过期:正常渲染。
- 已过期/未购买:渲染“续费引导页”(`pages/renewal.vue`),可保留展示站点基础信息,但隐藏关键功能和联系方式,或整站显示续费提示。
3. **续费接口**:调用 SaaS 后端订阅/续费接口,支付完成后刷新页面。
4. **缓存策略**:订阅状态可缓存短时间(如 60 秒),避免每次请求都调用后端,但要保证过期时及时切换。
---
## 六、开发阶段规划
建议按以下阶段推进,每个阶段产出可验证的里程碑。
### 第一阶段:基础框架搭建(约 1 周)
- [ ] 新建独立 Nuxt 4 项目(参考 `websopy-pc` 技术栈,但不含管理端代码)。
- [ ] 配置 TypeScript、Tailwind CSS、ESLint。
- [ ] 搭建基础目录结构(layouts、pages、composables、server)。
- [ ] 配置环境变量(`NUXT_PUBLIC_TENANT_ID``NUXT_PUBLIC_API_BASE``NUXT_PUBLIC_TEMPLATE_ID` 等)。
- [ ] 实现基础 Server Middleware:租户识别、域名解析、请求代理。
- [ ] 实现 `useSite()``useTenant()``usePageSeo()` 等核心 composables。
- [ ] 接入 SaaS 后端“站点信息”和“页面列表”接口。
**里程碑**:访问 `site-[tenantId].shoplnk.cn` 能显示基础首页,并从后端读取到站点标题和页面数据。
### 第二阶段:模板系统(约 1.5 周)
- [ ] 将现有 4 套模板设计稿转化为可运行的 Vue 组件(按 `templates/template-01/` 结构)。
- [ ] 设计模板配置规范(`config.ts`)和模板加载器(`useTemplate()`)。
- [ ] 实现模板样式隔离方案。
- [ ] 实现模板切换逻辑:根据后端 `templateId` 动态渲染对应模板。
- [ ] 提取公共组件:SiteHeader、SiteFooter、HeroSection、SectionTitle、ContactForm 等。
- [ ] 预留 CMS 模块接口:首页、关于我们、产品中心、案例展示、新闻资讯、联系我们。
**里程碑**:切换后端 `templateId`,页面能实时渲染不同模板风格。
### 第三阶段:页面路由与 CMS 渲染(约 1 周)
- [ ] 实现动态路由:`[slug].vue` 根据 CMS 页面配置渲染。
- [ ] 实现新闻、产品、案例列表页和详情页。
- [ ] 实现面包屑导航。
- [ ] 实现联系表单(提交到 SaaS 后端)。
- [ ] 对接 CMS 富文本内容渲染。
- [ ] 实现 404 页面和错误页。
**里程碑**:通过 CMS 配置的新页面能自动出现路由并正确渲染;新闻/产品/案例模块正常展示。
### 第四阶段:SEO / GEO 与性能优化(约 1 周)
- [ ] 全站动态 TDK、Open Graph、Canonical URL。
- [ ] 实现 JSON-LD 结构化数据注入。
- [ ] 实现动态 `sitemap.xml``robots.txt`
- [ ] 配置图片懒加载、资源预加载、字体优化。
- [ ] 接入 Nuxt 性能分析,优化首屏加载时间。
- [ ] 验证 SSR 输出 HTML 是否完整可被爬虫抓取。
**里程碑**:搜索引擎能抓取完整页面;通过 Lighthouse SEO 评分基本项。
### 第五阶段:域名绑定与部署(约 1 周)
- [ ] Nginx 配置泛域名解析 `*.shoplnk.cn`
- [ ] 实现自定义域名反向代理。
- [ ] 实现域名-租户映射查询(后端接口 + Nuxt Server Middleware)。
- [ ] SSL 证书方案确定并配置(通配符证书或单域名证书)。
- [ ] 编写部署脚本和 Dockerfile(可参考 `websopy-pc/Dockerfile`)。
- [ ] 配置 CI/CD 流程。
**里程碑**`site-[tenantId].shoplnk.cn` 和自定义域名都能正常访问对应租户站点。
### 第六阶段:订阅过期与续费(约 0.5 周)
- [ ] 接入 SaaS 订阅查询接口。
- [ ] 在 Server Middleware 中校验订阅状态。
- [ ] 实现续费引导页(`renewal.vue`)。
- [ ] 实现过期后页面降级策略(展示基础信息 vs 完全屏蔽)。
- [ ] 对接续费支付流程。
**里程碑**:订阅过期后自动展示续费引导页;续费成功后恢复正常访问。
### 第七阶段:H5 适配与收尾(约 1 周)
- [ ] 所有模板完成 H5 响应式适配。
- [ ] 移动端导航、表单、图片、视频优化。
- [ ] 多端测试(iOS Safari、Android Chrome、微信内置浏览器)。
- [ ] 编写项目文档、模板开发规范、部署文档。
- [ ] 代码审查、性能测试、安全测试。
**里程碑**:PC 和 H5 都能正常访问;模板开发规范文档化。
---
## 七、环境变量规划(示例)
```bash
# SaaS 后端接口地址
NUXT_PUBLIC_SERVER_API_BASE=https://cms-api.websoft.top
NUXT_PUBLIC_MODULES_API_BASE=https://cms-api.websoft.top/api
# 默认租户/应用(本地开发或演示)
NUXT_PUBLIC_TENANT_ID=1
NUXT_PUBLIC_APP_ID=website
NUXT_PUBLIC_TEMPLATE_ID=template-01
# 域名配置
NUXT_PUBLIC_BASE_DOMAIN=shoplnk.cn
# 缓存与订阅校验
NUXT_SUBSCRIPTION_CACHE_TTL=60
```
---
## 八、风险与注意事项
| 风险点 | 说明 | 建议 |
| ----------------- | ------------------------ | ----------------------------- |
| 模板样式冲突 | 多模板共享全局 CSS 可能互相污染 | 使用 CSS 命名空间或 CSS Modules 隔离 |
| 自定义域名 HTTPS | 每个域名都需要证书,管理复杂 | 优先使用通配符证书或 SaaS 后台统一证书管理 |
| 租户数据安全 | 域名解析错误可能导致串站 | Server Middleware 严格校验域名-租户映射 |
| SEO 数据缺失 | 后端 CMS 未配置 TDK 时页面为空 | 提供默认值和必填校验 |
| 订阅校验性能 | 每次请求都校验会增加延迟 | 使用服务端缓存,过期时间 60 秒内 |
| H5 适配工作量大 | 4 套模板都要做响应式 | 前期约定模板栅格规范,公共组件优先做响应式 |
| 与 `websopy-pc` 耦合 | 本项目为纯展示,不引入管理端依赖 | 独立项目,仅参考接口协议和代理思路 |
| 接口依赖 | 本项目所有功能依赖 SaaS 后端 API 可用 | 做好接口异常降级处理(默认数据、缓存兜底) |
---
## 九、下一步建议
1. **确认技术方案**:本项目作为纯前端展示项目,采用 Nuxt 4 + Vue 3 + TS 技术栈,不含管理端代码,所有功能通过 SaaS 后端 API 实现——是否确认?
2. **确认 SaaS 接口协议**
- 站点信息接口字段定义
- 域名绑定查询接口
- 订阅状态查询接口
- 新闻/产品/案例 CMS 数据接口
- 表单/留言提交接口
3. **确认模板规范**:模板数据由 SaaS 后端管理后台配置,本项目根据 `templateId` 渲染——是否确认?
4. **确认域名与证书方案**`shoplnk.cn` 泛域名证书是否已准备?自定义域名证书如何管理?
5. **确认首期范围**:是否 4 套模板一次全部实现?还是先实现 1 套 MVP,后续扩展?
---
*文档生成时间:2026-07-05*
*版本:v1.1(明确纯前端展示项目定位)*
---
## 十、专项附录:标书购买功能(template-07 / 客户「汇吉采」)
> **性质**:这是叠加在基础官网框架之上的**客户专项需求**,不修改基础框架的定位与计划(一至九节保持不变)。
> **代码边界**:前端改动在本仓库(website-template);后端改动在 **`cms-java-code` 仓库(即线上 cms-api**,本仓库的 `server/api/*` 只做代理转发,不含交易逻辑。
### 10.1 后端选型结论(已评估确定)
**在 `cms-api`= `cms-java-code`,即自研 SaaS 后端)内新增「标书」业务包,复用其已有的 `shop` 订单 + `payment` 微信扫码支付底座;不单独启用 `guilixu-java`。**
依据(已查代码确认):
- 模板当前只连 cms-api`modulesApiBase = https://cms-api.websoft.top/api`),前端零新增对接。
- cms-api 已内置:会员/用户体系(`ShopUser`、JWT)、`ShopOrder``PaymentController.createPaymentWithOrder`(建单+发起支付)、`WechatNativeStrategy`(返回 `codeUrl`)、`PaymentNotifyController`(异步回调)、多租户(`TenantId` 头)。
- `guilixu-java` 只是 cms-api 的裁剪分支(仅 payment+shop+common,无 cms),且独立部署、独立数据库,单独启用只增加跨库/跨服务成本、无收益。
> 详见 `outputs/汇吉采-标书购买-后端选型评估.md` 与 `outputs/汇吉采-标书购买功能方案评估.md`。
### 10.1.1 前端隔离策略(已决策:fork 独立网站端)
**决策依据(2026-08-26 与主人确认)**
1. 标书购买是汇吉采**独家特色**,其他客户基本不会用 → 不应塞进通用模板污染内核。
2. 不排除汇吉采未来出现**更多深度定制**(不止标书) → 深度定制会持续侵蚀通用模板边界,fork 比在通用模板里写 `if (tenantId===10626)` 更干净。
3. 独立部署/运维:暂未知 → fork 不强制立即独立部署,可先 fork 代码、共用部署,待需求明确再切。
**结论**:前端**从本仓库 fork 出独立端(如 `website-huijicai`,基于 git branch 分出)**,汇吉采专属功能(标书购买 + 未来定制)只在独立端内开发,通用 website-template 保持纯净。
**fork 的正确姿势(规避维护噩梦)**
-**git branch / 独立仓库 + 共享子模块**,不要把通用内核复制粘贴成两份。
- 分层:通用内核(SEO、多租户中间件、部署脚本、基础组件)与汇吉采定制区(template-07 + 标书模块 + 未来定制)分离,定制区单独维护。
- 约定:通用安全/框架更新通过 **rebase / cherry-pick** 同步到独立端,防止长期漂移到"老版本分支"。
**后端不 fork(重要)**cms-api 是多租户 monolith,fork 整个后端代价极高且不必要。`tender` 业务包直接在 cms-api 内新增,数据带 `tenantId=10626` 天然隔离,复用现有 shop/payment 底座。**即:前端物理隔离 + 后端租户逻辑隔离。**
### 10.2 前端任务清单(独立端 `website-huijicai`
| 任务 | 做法 | 复用/新增 |
| ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------- |
| ① 激活 `buy` 入口 | 新增 `app/pages/buy.vue`,照搬 `renewal.vue` 加载 `components.BuyDocument`;无需改 `useTemplate` 核心 | 复用现有 routeMap `buy` 钩子 |
| ② `BuyDocument.vue` 改造 | 占位 stub → 购买流程壳(列表 + 详情 + 表单 + 支付二维码 + 结果页) | 改写现有文件 |
| ③ 标书列表/详情 | 仿 `ProductList/ProductDetail` 结构接 `tender` 接口(status=onsale 筛选、关键词、分类) | 复制改造 |
| ④ 联系表单 | 姓名/电话/邮箱/公司,**复用 `ContactForm` 手机号校验 + 滑块验证码** | 复用 |
| ⑤ 注册/登录页 | `register.vue` / `login.vue` + `useSupplier` 登录态 composableToken 存 cookie+ 购买入口权限门控 | 新增 |
| ⑥ 支付页 | 展示后端返回的 `code_url` 二维码 + **轮询订单状态** + 支付成功展示「下载标书」/「已发邮件」 | 新增 |
### 10.3 后端任务清单(cms-java-code 仓库,新增 `tender` 业务包)
- 实体:`Tender`(招标编号、发售起止、状态 onsale/ended、售价、关联文件)、`TenderOrder`(或复用 `ShopOrder` + 类型标识)、可选 `SupplierProfile`
- 接口:
- `GET /api/tender/list`(按 status=onsale + 分类/关键词筛选)
- `GET /api/tender/detail`
- `POST /api/tender/order`(建单 + 微信扫码支付,复用 `createPaymentWithOrder`
- `POST /api/tender/notify`(复用 `WxPayNotifyService` 异步回调置已支付)
- `GET /api/tender/order/status`(前端轮询)
- `GET /api/tender/order/download`(登录态 + 订单校验后下载标书 / 发邮件)
- 供应商账号:复用现有 `shop` 用户体系(加 `supplier` 角色/字段),下单接口要求 `loginUser != null`(已有约束天然实现"注册登录后才能买")。
### 10.4 里程碑(建议)
1. **M1 后端底座就绪**`tender` 实体 + 列表/详情/筛选接口 + 下单/支付/回调/交付全链路(复用 shop/payment)。
2. **M2 前端流程闭环**`buy` 入口激活 + 列表/详情/表单/支付/结果页,能完整走通"注册→筛选项目→填信息→扫码付→下载"。
3. **M3 联调与权限校验**:下单时后端强制校验项目状态=onsale(防绕过)、支付回调稳定、邮件/下载双通道验证。
4. **M4 上线增强(P1**:我的购买记录、后台标书上架审核、邮件模板润色。
### 10.5 仍需拍板(阻塞排期)
1. **生产拓扑**:汇吉采生产是否真的只连 cms-api?(从模板对接关系看是,需确认是否双后端)
2. **供应商建模**:复用 shop 会员表(加角色/字段)还是新建 `supplier` 表?(建议前者)
3. **标书建模**:新建 `Tender` 实体 vs 当特殊商品复用 `ShopGoods`?(建议新建 `Tender`,语义清晰)
4. **微信商户号**:cms-api 的微信支付配置(租户 10626)是否已配好商户号与公网 `notify_url`
### 10.6 拍板结果(2026-08-26 已确认,含修正)
主人就 10.5 四项给出结论,并已 fork 出独立端 `/Users/gxwebsoft/VUE/website-huijicai`(master 分支、干净无改动、remote 指向 `website-huijicai.git`)。
1. **生产拓扑**:不排除双后端。→ **前端代理层预留可切换的 `tenderApiBase`:默认指向 cms-api(复用其 shop+payment 单库闭环),若确认交易走 guilixu-java 仅改 env 即可,前端代码不变**。最终生产拓扑需在开发启动前敲定(决定 baseURL 默认值)。
2. **供应商建模**:主人原提"复用 sys_user",已**纠正为复用 `ShopUser`(前端会员表)**。`sys_user` 是后台管理员表(`common/system` 包),供应商复用它会混进后台权限,不安全。
- `ShopUser` 字段已极全:`type`(0个人/1企业/2其他)、`phone``email``realName``companyId``certification`(实名认证)、`tenantId` 等;加一个 `supplierStatus`(0非供应商/1待认证/2已认证) 标记字段即可,**无需新建表**。
- **⚠️ 缺口(影响 P0)**:cms-api 当前前端会员登录以**微信登录为主**(`WxLoginController.loginByMpWxPhone`),**未找到账号密码/短信验证码的会员注册登录接口**。若客户要求账号密码/短信注册(招投标场景常见),需**新建会员注册登录接口**(P0 增量),待确认客户登录方式。
3. **标书建模**:确认新建 **`Tender` 实体**(标书项目表)。订单直接复用 **`ShopOrder`**(其已有 `type` 订单类型字段,标书订单用 `type=3`;需把 `PaymentWithOrderRequest.OrderInfo.type``@Max(2)` 放开到 3)。
4. **微信商户号**:已就绪(租户 10626 商户号 + 公网 `notify_url`)。✅ 支付可推进。
> 前后端详细清单见 `outputs/汇吉采-标书购买-前端清单与后端接口清单.md`。