diff --git a/index.html b/index.html index 8f6b554..5babd69 100644 --- a/index.html +++ b/index.html @@ -119,9 +119,9 @@ > - + - + + + +
+

全能查合作政策指南_合作伙伴权益与结算说明_官方文档

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/agent.html b/public/seo-templates/agent.html new file mode 100644 index 0000000..03df2e9 --- /dev/null +++ b/public/seo-templates/agent.html @@ -0,0 +1,80 @@ + + + + + + + + 全能查代理 - 免费开通代理权限 | 大数据风险报告代理 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

全能查代理 - 免费开通代理权限 | 大数据风险报告代理

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/example.html b/public/seo-templates/example.html new file mode 100644 index 0000000..937eb1d --- /dev/null +++ b/public/seo-templates/example.html @@ -0,0 +1,80 @@ + + + + + + + + 示例报告 - 全能查报告展示 | 大数据风险报告样例 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

示例报告 - 全能查报告展示 | 大数据风险报告样例

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/help-guide.html b/public/seo-templates/help-guide.html new file mode 100644 index 0000000..b9ba181 --- /dev/null +++ b/public/seo-templates/help-guide.html @@ -0,0 +1,80 @@ + + + + + + + + 使用指南 - 全能查操作教程 | 功能说明 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

使用指南 - 全能查操作教程 | 功能说明

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/help.html b/public/seo-templates/help.html new file mode 100644 index 0000000..d4955b3 --- /dev/null +++ b/public/seo-templates/help.html @@ -0,0 +1,80 @@ + + + + + + + + 帮助中心 - 全能查使用指南 | 常见问题解答 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

帮助中心 - 全能查使用指南 | 常见问题解答

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/index.html b/public/seo-templates/index.html new file mode 100644 index 0000000..0619b93 --- /dev/null +++ b/public/seo-templates/index.html @@ -0,0 +1,80 @@ + + + + + + + + 全能查官网_个人婚姻状态报告_综合风险排查工具箱 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

全能查官网_个人婚姻状态报告_综合风险排查工具箱

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-backgroundcheck.html b/public/seo-templates/inquire-backgroundcheck.html new file mode 100644 index 0000000..3e2da15 --- /dev/null +++ b/public/seo-templates/inquire-backgroundcheck.html @@ -0,0 +1,80 @@ + + + + + + + + 职场背景核验报告_候选人职业风险与竞业核验_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

职场背景核验报告_候选人职业风险与竞业核验_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-companyinfo.html b/public/seo-templates/inquire-companyinfo.html new file mode 100644 index 0000000..1ca8fce --- /dev/null +++ b/public/seo-templates/inquire-companyinfo.html @@ -0,0 +1,80 @@ + + + + + + + + 企业工商信用画像_经营异常与商业风险透视_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

企业工商信用画像_经营异常与商业风险透视_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-consumerFinanceReport.html b/public/seo-templates/inquire-consumerFinanceReport.html new file mode 100644 index 0000000..b180b9c --- /dev/null +++ b/public/seo-templates/inquire-consumerFinanceReport.html @@ -0,0 +1,80 @@ + + + + + + + + 个人履约能力评估_经济风险与收支压力参考_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

个人履约能力评估_经济风险与收支压力参考_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-homeservice.html b/public/seo-templates/inquire-homeservice.html new file mode 100644 index 0000000..603f30f --- /dev/null +++ b/public/seo-templates/inquire-homeservice.html @@ -0,0 +1,80 @@ + + + + + + + + 家政人员背景核实_保姆月嫂司法安全评估_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

家政人员背景核实_保姆月嫂司法安全评估_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-marriage.html b/public/seo-templates/inquire-marriage.html new file mode 100644 index 0000000..f0122f0 --- /dev/null +++ b/public/seo-templates/inquire-marriage.html @@ -0,0 +1,80 @@ + + + + + + + + 婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-preloanbackgroundcheck.html b/public/seo-templates/inquire-preloanbackgroundcheck.html new file mode 100644 index 0000000..2aed849 --- /dev/null +++ b/public/seo-templates/inquire-preloanbackgroundcheck.html @@ -0,0 +1,80 @@ + + + + + + + + 综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/inquire-riskassessment.html b/public/seo-templates/inquire-riskassessment.html new file mode 100644 index 0000000..1c93cc5 --- /dev/null +++ b/public/seo-templates/inquire-riskassessment.html @@ -0,0 +1,80 @@ + + + + + + + + 个人综合风险分析_履约能力画像_多维数据检测_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

个人综合风险分析_履约能力画像_多维数据检测_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/promote.html b/public/seo-templates/promote.html new file mode 100644 index 0000000..3bd627e --- /dev/null +++ b/public/seo-templates/promote.html @@ -0,0 +1,80 @@ + + + + + + + + 全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/seo-templates/service.html b/public/seo-templates/service.html new file mode 100644 index 0000000..fd9f4bd --- /dev/null +++ b/public/seo-templates/service.html @@ -0,0 +1,80 @@ + + + + + + + + 客服中心 - 全能查在线客服 | 技术支持 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

客服中心 - 全能查在线客服 | 技术支持

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+ +
+
+ + \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml index 261c820..6cee3fa 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,67 +1,67 @@ - https://www.zhinengcha.cn/ + https://www.quannengcha.com/ 2025-08-01 daily 1.0 - https://www.zhinengcha.cn/agent + https://www.quannengcha.com/agent 2025-08-01 weekly 0.8 - https://www.zhinengcha.cn/help + https://www.quannengcha.com/help 2025-08-01 monthly 0.7 - https://www.zhinengcha.cn/help/guide + https://www.quannengcha.com/help/guide 2025-08-01 monthly 0.6 - https://www.zhinengcha.cn/example + https://www.quannengcha.com/example 2025-08-01 monthly 0.6 - https://www.zhinengcha.cn/service + https://www.quannengcha.com/service 2025-08-01 monthly 0.5 - https://www.zhinengcha.cn/privacyPolicy + https://www.quannengcha.com/privacyPolicy 2025-08-01 yearly 0.3 - https://www.zhinengcha.cn/userAgreement + https://www.quannengcha.com/userAgreement 2025-08-01 yearly 0.3 - https://www.zhinengcha.cn/agentManageAgreement + https://www.quannengcha.com/agentManageAgreement 2025-08-01 yearly 0.3 - https://www.zhinengcha.cn/agentSerivceAgreement + https://www.quannengcha.com/agentSerivceAgreement 2025-08-01 yearly 0.3 - https://www.zhinengcha.cn/authorization + https://www.quannengcha.com/authorization 2025-08-01 yearly 0.3 diff --git a/server/README-SEO.md b/server/README-SEO.md new file mode 100644 index 0000000..6df075e --- /dev/null +++ b/server/README-SEO.md @@ -0,0 +1,355 @@ +# SPA SEO 优化解决方案 + +## 📋 方案概述 + +针对 SPA 应用 SEO 问题,采用**爬虫检测 + 静态 HTML 回退**方案: + +1. **爬虫检测**:识别搜索引擎爬虫(百度、Google、必应、搜狗等) +2. **静态 HTML**:为爬虫提供预渲染的 HTML 模板,包含完整 TDK、OG、canonical、结构化数据 +3. **正常用户**:继续使用 SPA,体验不受影响 + +**配置统一**:服务端 SEO 模板内容与前端 `src/composables/useSEO.js` 保持一致(标题、描述、关键词、域名),域名默认为 `https://www.quannengcha.com`(全能查)。可通过环境变量 `SEO_BASE_URL` 覆盖。 + +## 🏗️ 项目结构 + +``` +server/ +├── crawler-detector.js # 爬虫检测模块 +├── middleware.js # SEO 中间件(Express/Koa),路由与 useSEO.js 一致 +├── generate-seo-templates.cjs # SEO 模板生成器(与 useSEO.js 同步) +├── server-example-express.js # Express 服务器示例 +├── nginx-www.quannengcha.com.conf # 全能查 Nginx 配置(quannengcha.com) +├── nginx-www.tianyuancha.cn.conf # 天远查 Nginx 配置(tianyuancha.cn) +├── nginx-www.tianyuandb.com.conf # 天远数据 Nginx 配置(tianyuandb.com) +├── test-seo.js # SEO 端到端检测脚本 +└── README-SEO.md # 本文档 + +public/ +└── seo-templates/ # SEO 静态模板目录(运行 generate 后生成) + ├── index.html + ├── agent-system-guide.html + ├── inquire-riskassessment.html + ├── inquire-companyinfo.html + ├── inquire-preloanbackgroundcheck.html + ├── inquire-marriage.html + ├── inquire-backgroundcheck.html + ├── inquire-homeservice.html + ├── inquire-consumerFinanceReport.html + ├── agent.html + ├── help.html + ├── help-guide.html + ├── example.html + ├── service.html + ├── promote.html + └── ... +``` + +## 🚀 快速开始 + +### 步骤1:生成 SEO 模板 + +```bash +cd server +node generate-seo-templates.cjs +# 或 npm run generate +``` + +这会在 `public/seo-templates/` 下生成所有页面的静态 HTML 模板,内容与 `src/composables/useSEO.js` 一致。 + +### 步骤2:集成到你的服务器 + +#### 选项A:使用Express服务器 + +```javascript +const express = require('express') +const SEOMiddleware = require('./server/middleware') + +const app = express() + +// 初始化SEO中间件 +const seoMiddleware = new SEOMiddleware({ + templateDir: path.join(__dirname, 'public/seo-templates'), + debug: true // 开发环境开启调试日志 +}) + +// 应用SEO中间件(必须在静态文件服务之前) +app.use(seoMiddleware.express()) + +// 静态文件服务 +app.use(express.static(path.join(__dirname, 'dist'))) + +// SPA路由处理 +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'dist/index.html')) +}) +``` + +#### 选项B:使用 Nginx + +- 全能查站点:参考 `server/nginx-www.quannengcha.com.conf`,将 `root` 和 `server_name`、证书路径改为你的服务器路径。 +- 天远查站点:参考 `server/nginx-www.tianyuancha.cn.conf`。 +- 部署时把 `public/seo-templates/` 整目录上传到服务器 `root` 下的 `static-pages/`。 + +```bash +# 复制并修改配置 +cp server/nginx-www.quannengcha.com.conf /etc/nginx/sites-available/quannengcha +nano /etc/nginx/sites-available/quannengcha # 修改 root、证书等 + +# 启用并重载 +ln -s /etc/nginx/sites-available/quannengcha /etc/nginx/sites-enabled/ +nginx -t && systemctl restart nginx +``` + +#### 选项C:使用Koa + +```javascript +const Koa = require('koa') +const serve = require('koa-static') +const SEOMiddleware = require('./server/middleware') + +const app = new Koa() + +// 应用SEO中间件 +const seoMiddleware = new SEOMiddleware({ + templateDir: path.join(__dirname, 'public/seo-templates'), + debug: true +}) + +app.use(seoMiddleware.koa()) + +// 静态文件服务 +app.use(serve(path.join(__dirname, 'dist'))) + +app.listen(3000) +``` + +### 步骤3:测试爬虫检测 + +```bash +# 模拟百度爬虫 +curl -A "Baiduspider" http://localhost:3000/ + +# 模拟Google爬虫 +curl -A "Googlebot/2.1" http://localhost:3000/ + +# 模拟普通用户 +curl http://localhost:3000/ +``` + +## 🔧 配置说明 + +### 爬虫检测器配置 + +`crawler-detector.js` 包含以下爬虫识别: + +- **中文搜索引擎**:百度、360、搜狗、必应、有道、搜搜、头条搜索 +- **国际搜索引擎**:Google、Bing、Yahoo +- **社交媒体爬虫**:Facebook、Twitter、LinkedIn、WhatsApp等 + +你可以根据需要添加或修改爬虫模式: + +```javascript +// 在crawler-detector.js中添加新的爬虫模式 +this.crawlerPatterns.push('your-custom-bot') +``` + +### 路由到模板映射 + +在 `middleware.js` 中配置路由与模板的对应关系: + +```javascript +this.routeTemplateMap = { + '/': 'index.html', + '/agent': 'agent.html', + // 添加新的路由映射 + '/new-route': 'new-template.html' +} +``` + +### 模板生成配置 + +**推荐**:页面 SEO 以 `src/composables/useSEO.js` 为唯一来源;修改标题/描述/关键词时只改 `useSEO.js` 中的 `routeConfigs`,然后同步到服务端: + +- 在 `server/generate-seo-templates.cjs` 的 `pageSEOConfigs` 中保持与 `useSEO.js` 一致(含新增路由与 `BASE_URL`)。 +- 在 `server/middleware.js` 的 `routeTemplateMap` 中为新路由添加映射。 +- 若用 Nginx,在对应 conf 的 `$seo_file` 中增加 `if ($uri = '/新路径') { set $seo_file 新模板.html; }`。 + +新增页面示例(`generate-seo-templates.cjs`): + +```javascript +'new-template.html': { + title: '页面标题', + description: '页面描述', + keywords: '关键词1,关键词2', + url: 'https://www.tianyuandb.com/new-route' +} +``` + +## 📝 自定义模板 + +### 修改模板样式 + +编辑 `generate-seo-templates.js` 中的 `generateHTMLTemplate` 函数: + +```javascript +function generateHTMLTemplate(config) { + return ` + + + + + + + + + ` +} +``` + +### 添加结构化数据 + +模板已包含基本的结构化数据(JSON-LD格式),如需扩展: + +```javascript +const structuredData = { + "@context": "https://schema.org", + "@type": "WebPage", + // 添加更多字段 + "breadcrumb": { + "@type": "BreadcrumbList", + "itemListElement": [...] + } +} +``` + +## 🧪 验证SEO效果 + +### 使用在线工具 + +1. **百度资源平台**:https://ziyuan.baidu.com/ +2. **Google Search Console**:https://search.google.com/search-console +3. **必应网站管理员工具**:https://www.bing.com/webmasters + +### 使用命令行工具与检测脚本 + +```bash +# 本地/线上 SEO 检测(会请求爬虫 UA 与普通 UA) +cd server +SEO_TEST_URL=http://localhost:3000 node test-seo.js +# 或线上:SEO_TEST_URL=https://www.tianyuandb.com node test-seo.js +``` + +```bash +# 查看爬虫看到的标题 +curl -s -A "Baiduspider/2.0" https://www.tianyuandb.com/ | grep -o '.*' + +# 检查 meta 与 canonical +curl -s -A "Googlebot" https://www.tianyuandb.com/ | grep -E ' { + return userAgent.includes(pattern.toLowerCase()) + }) + } + + /** + * 检查请求头 + * @param {Object} headers + * @returns {Boolean} + */ + checkHeaders(headers) { + for (const header of this.crawlerHeaders) { + const headerValue = headers[header]?.toLowerCase() + if (headerValue && (headerValue.includes('bot') || headerValue.includes('crawler'))) { + return true + } + } + return false + } + + /** + * 检查IP地址是否为已知爬虫IP + * @param {String} ip + * @returns {Boolean} + */ + checkIP(ip) { + // 这里可以添加已知爬虫IP段的检测 + // 需要定期更新爬虫IP列表 + return false + } + + /** + * 获取爬虫类型 + * @param {String} userAgent + * @returns {String} 爬虫类型 + */ + getCrawlerType(userAgent) { + const ua = userAgent.toLowerCase() + + if (ua.includes('baiduspider')) return 'baidu' + if (ua.includes('googlebot')) return 'google' + if (ua.includes('bingbot') || ua.includes('msnbot')) return 'bing' + if (ua.includes('360spider')) return '360' + if (ua.includes('sogou spider')) return 'sogou' + if (ua.includes('facebookexternalhit')) return 'facebook' + if (ua.includes('twitterbot')) return 'twitter' + if (ua.includes('linkedinbot')) return 'linkedin' + + return 'unknown' + } +} + +module.exports = CrawlerDetector diff --git a/server/generate-seo-templates.cjs b/server/generate-seo-templates.cjs new file mode 100644 index 0000000..a856bcc --- /dev/null +++ b/server/generate-seo-templates.cjs @@ -0,0 +1,245 @@ +/** + * SEO模板生成器 + * 根据 useSEO.js 的页面配置自动生成静态 HTML 模板,供爬虫访问时返回 + * 配置与 src/composables/useSEO.js 保持一致 + * + * 多站点:通过环境变量 SEO_BASE_URL 指定 canonical/og:url 域名后生成 + * 例:SEO_BASE_URL=https://www.quannengcha.com node generate-seo-templates.cjs + */ + +const fs = require('fs') +const path = require('path') + +const BASE_URL = process.env.SEO_BASE_URL || 'https://www.quannengcha.com' + +// 页面 SEO 配置(与 src/composables/useSEO.js 的 routeConfigs 保持一致) +const pageSEOConfigs = { + 'index.html': { + title: '全能查官网_个人婚姻状态报告_综合风险排查工具箱', + description: '全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。', + keywords: '全能查,婚姻状态核实,风险排查工具,个人风险预警,第三方背调,商业信用评估', + url: BASE_URL + }, + 'agent-system-guide.html': { + title: '全能查合作政策指南_合作伙伴权益与结算说明_官方文档', + description: '全能查官方合作体系说明文档。详细解读合作伙伴的等级权益、服务费结算标准及晋升机制。致力于构建公平、透明的商业合作生态,助力合作伙伴快速上手业务。', + keywords: '合作伙伴政策,服务费结算,渠道等级说明,业务操作指南,代理系统后台', + url: `${BASE_URL}/agent/system-guide` + }, + 'inquire-riskassessment.html': { + title: '个人综合风险分析_履约能力画像_多维数据检测_全能查', + description: '全能查个人风险报告为您提供全方位的信用健康度参考。基于公开数据深度解析综合风险指数、司法关联风险、历史履约趋势及潜在的负面标签。数据客观中立,帮助用户建立良好的个人履约记录管理意识。', + keywords: '个人风险检测,履约能力分析,综合风险指数,信用健康度,个人数据画像', + url: `${BASE_URL}/inquire/riskassessment` + }, + 'inquire-companyinfo.html': { + title: '企业工商信用画像_经营异常与商业风险透视_全能查', + description: '全能查企业版深度透视商业真相。聚合工商、司法及税务公开数据,核验企业经营异常名录、行政处罚、法律诉讼及股权穿透信息。全方位评估合作伙伴的商业健康度,规避合同违约风险。', + keywords: '企业信用评估,工商背景核验,商业风险评估,公司经营异常,合作方背景核实', + url: `${BASE_URL}/inquire/companyinfo` + }, + 'inquire-preloanbackgroundcheck.html': { + title: '综合履约评分检测_多平台履约记录分析_个人财务履约报告_全能查', + description: '全能查提供专业的个人履约健康度体检服务。基于多维大数据分析,检测您的综合评分波动、历史履约记录及潜在的风险标签。本服务旨在帮助用户优化个人数据画像,提升信用管理意识,不提供任何信贷金融服务。', + keywords: '综合评分检测,多重履约压力分析,履约能力评估,综合评分优化,个人数据画像', + url: `${BASE_URL}/inquire/preloanbackgroundcheck` + }, + 'inquire-marriage.html': { + title: '婚前综合背景了解_情感安全风险评估_家庭履约分析_全能查', + description: '全能查婚恋风险报告基于合法公开数据,辅助评估对象的婚前背景。核心核验司法涉诉记录、失信被执行历史、多重履约能力及不良社会标签。拒绝情感盲区,用数据守护您的家庭与财产安全。', + keywords: '婚前背景报告,恋爱对象风险,情感安全评估,司法记录核验,家庭风险防范', + url: `${BASE_URL}/inquire/marriage` + }, + 'inquire-backgroundcheck.html': { + title: '职场背景核验报告_候选人职业风险与竞业核验_全能查', + description: '全能查为企业提供专业的入职背调服务。一键筛查候选人的学历背景、涉及的商业利益冲突、劳动仲裁记录及社会不良风险。数据实时合规,降低企业用工试错成本,提升招聘决策效率。', + keywords: '员工入职背调,职业背景核实,竞业限制评估,职场信用报告,候选人风险筛查', + url: `${BASE_URL}/inquire/backgroundcheck` + }, + 'inquire-homeservice.html': { + title: '家政人员背景核实_保姆月嫂司法安全评估_全能查', + description: '全能查针对家庭用工场景,提供客观的家政人员背景核验服务。重点核验身份信息、司法涉诉记录及失信历史。辅助雇主识别高危人员,让居家养老育儿更安心。', + keywords: '保姆背景核验,家政风险筛查,月嫂司法记录,雇佣安全评估,家政人员核验', + url: `${BASE_URL}/inquire/homeservice` + }, + 'inquire-consumerFinanceReport.html': { + title: '个人履约能力评估_经济风险与收支压力参考_全能查', + description: '全能查履约报告基于大数据算法,提供个人经济稳定性的客观分析。多维度检测综合履约分、经济关联风险及潜在的资金压力指数。本服务仅提供大数据层面的风险参考,助您优化财务管理。', + keywords: '履约能力评估,经济风险指数,综合评分波动,资金压力分析,财务健康度', + url: `${BASE_URL}/inquire/consumerFinanceReport` + }, + 'agent.html': { + title: '全能查代理 - 免费开通代理权限 | 大数据风险报告代理', + description: '全能查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。', + keywords: '全能查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益', + url: `${BASE_URL}/agent` + }, + 'help.html': { + title: '帮助中心 - 全能查使用指南 | 常见问题解答', + description: '全能查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。', + keywords: '全能查帮助, 使用指南, 常见问题, 操作教程, 客服支持', + url: `${BASE_URL}/help` + }, + 'help-guide.html': { + title: '使用指南 - 全能查操作教程 | 功能说明', + description: '全能查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。', + keywords: '使用指南, 操作教程, 功能说明, 快速上手, 全能查教程', + url: `${BASE_URL}/help/guide` + }, + 'example.html': { + title: '示例报告 - 全能查报告展示 | 大数据风险报告样例', + description: '全能查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。', + keywords: '示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告', + url: `${BASE_URL}/example` + }, + 'service.html': { + title: '客服中心 - 全能查在线客服 | 技术支持', + description: '全能查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。', + keywords: '客服中心, 在线客服, 技术支持, 问题反馈, 全能查客服', + url: `${BASE_URL}/service` + }, + 'promote.html': { + title: '全能查合伙人计划_风控平台系统招商_渠道合作平台_全能查', + description: '全能查开放全国渠道合作,提供零门槛的风险评估系统接入服务。一键开通独立后台,支持婚恋、职场、家政及商业风控等多场景报告推广。正规项目,结算透明,赋能流量方实现合规商业价值。', + keywords: '风控系统代理,风险评估平台招商,平台渠道合作,企业服务创业,全能查合伙人', + url: `${BASE_URL}/promote` + } +} + +/** + * 规范化文案:统一为中文标点,避免乱码 + */ +function normalizeText(str) { + if (typeof str !== 'string') return str + return str + .replace(/\uFFFD/g, '') + .replace(/。/g, '。') + .replace(/、/g, '、') +} + +/** + * 转义 HTML 属性值 + */ +function escapeAttr(str) { + if (typeof str !== 'string') return '' + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>') +} + +/** + * 生成单页 HTML 模板 + */ +function generateHTMLTemplate(config) { + const title = normalizeText(config.title) + const description = normalizeText(config.description) + const keywords = normalizeText(config.keywords) + const structuredData = { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: title, + description: description, + url: config.url, + mainEntity: { + '@type': 'Organization', + name: '全能查', + url: 'https://www.quannengcha.com/', + description: '专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用' + } + } + + return ` + + + + + + + ${escapeAttr(title)} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

${escapeAttr(title)}

+
+

正在跳转到完整版网站...

+

如果浏览器没有自动跳转,请 点击这里

+
+

${escapeAttr(description)}

+
+

关于全能查

+

全能查是您的掌上风控工具箱。平台基于合规数据,提供个人婚姻状态分析、职场背调及黑名单筛查服务。无需繁琐流程,客观中立,一键生成包含婚姻涉诉历史与家庭风险的综合报告,助您快速识别潜在隐患。

+
+
+

核心服务

+
    +
  • 个人综合风险分析与履约能力画像
  • +
  • 企业工商信用画像与商业风险透视
  • +
  • 婚前综合背景了解与情感安全风险评估
  • +
  • 职场背景核验与入职背调
  • +
  • 家政人员背景核实与安全评估
  • +
  • 个人履约能力评估与经济风险分析
  • +
+
+
+ +` +} + +function main() { + const outputDir = path.join(__dirname, '../public/seo-templates') + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + console.log(`✓ 创建模板目录: ${outputDir}`) + } + + let successCount = 0 + Object.entries(pageSEOConfigs).forEach(([filename, config]) => { + const htmlContent = generateHTMLTemplate(config) + const filePath = path.join(outputDir, filename) + fs.writeFileSync(filePath, htmlContent, 'utf-8') + console.log(`✓ 生成模板: ${filename}`) + successCount++ + }) + + console.log(`\n✓ 成功生成 ${successCount} 个 SEO 模板文件`) + console.log(`📁 模板目录: ${outputDir}`) + console.log(`💡 配置与 useSEO.js 一致,当前域名: ${BASE_URL}`) +} + +main() diff --git a/server/middleware.js b/server/middleware.js new file mode 100644 index 0000000..ce115ed --- /dev/null +++ b/server/middleware.js @@ -0,0 +1,179 @@ +/** + * SEO中间件 + * 用于在Node.js服务器中检测爬虫并返回静态HTML + */ + +const fs = require('fs') +const path = require('path') +const CrawlerDetector = require('./crawler-detector') + +class SEOMiddleware { + constructor(options = {}) { + this.detector = new CrawlerDetector() + this.templateDir = options.templateDir || path.join(__dirname, '../public/seo-templates') + this.defaultTemplate = options.defaultTemplate || 'index.html' + this.fallbackToSPA = options.fallbackToSPA !== false + this.debug = options.debug || false + + // 路由到模板的映射(与 useSEO.js 及 generate-seo-templates.cjs 保持一致;子路径放前面以优先精确匹配) + this.routeTemplateMap = { + '/': 'index.html', + '/agent/system-guide': 'agent-system-guide.html', + '/inquire/riskassessment': 'inquire-riskassessment.html', + '/inquire/companyinfo': 'inquire-companyinfo.html', + '/inquire/preloanbackgroundcheck': 'inquire-preloanbackgroundcheck.html', + '/inquire/marriage': 'inquire-marriage.html', + '/inquire/backgroundcheck': 'inquire-backgroundcheck.html', + '/inquire/homeservice': 'inquire-homeservice.html', + '/inquire/consumerFinanceReport': 'inquire-consumerFinanceReport.html', + '/agent': 'agent.html', + '/help/guide': 'help-guide.html', + '/help': 'help.html', + '/example': 'example.html', + '/service': 'service.html', + '/promote': 'promote.html' + } + + // 初始化模板缓存 + this.templateCache = new Map() + this.cacheTemplates() + } + + /** + * 缓存所有模板文件 + */ + cacheTemplates() { + try { + if (!fs.existsSync(this.templateDir)) { + console.warn(`[SEOMiddleware] 模板目录不存在: ${this.templateDir}`) + return + } + + const files = fs.readdirSync(this.templateDir) + files.forEach(file => { + const filePath = path.join(this.templateDir, file) + if (fs.statSync(filePath).isFile()) { + this.templateCache.set(file, fs.readFileSync(filePath, 'utf-8')) + if (this.debug) { + console.log(`[SEOMiddleware] 已缓存模板: ${file}`) + } + } + }) + + console.log(`[SEOMiddleware] 已缓存 ${this.templateCache.size} 个模板文件`) + } catch (error) { + console.error('[SEOMiddleware] 缓存模板失败:', error) + } + } + + /** + * 获取对应的模板文件名 + * @param {String} path - 请求路径 + * @returns {String} 模板文件名 + */ + getTemplatePath(requestPath) { + // 完全匹配 + if (this.routeTemplateMap[requestPath]) { + return this.routeTemplateMap[requestPath] + } + + // 模糊匹配(处理动态路由) + const matchedKey = Object.keys(this.routeTemplateMap).find(route => { + return requestPath.startsWith(route) + }) + + return matchedKey ? this.routeTemplateMap[matchedKey] : this.defaultTemplate + } + + /** + * 获取模板内容 + * @param {String} templateName - 模板文件名 + * @returns {String|null} 模板内容 + */ + getTemplate(templateName) { + // 首先尝试缓存 + let content = this.templateCache.get(templateName) + + // 如果缓存中没有,尝试从磁盘读取 + if (!content) { + try { + const filePath = path.join(this.templateDir, templateName) + if (fs.existsSync(filePath)) { + content = fs.readFileSync(filePath, 'utf-8') + this.templateCache.set(templateName, content) + } + } catch (error) { + console.error(`[SEOMiddleware] 读取模板失败: ${templateName}`, error) + } + } + + return content || null + } + + /** + * Express中间件 + */ + express() { + return (req, res, next) => { + // 检测是否为爬虫 + if (this.detector.isCrawler(req)) { + const templateName = this.getTemplatePath(req.path) + const template = this.getTemplate(templateName) + + if (template) { + // 设置响应头 + res.setHeader('Content-Type', 'text/html; charset=utf-8') + res.setHeader('X-SEOMiddleware', 'prerendered') + + // 返回静态HTML + if (this.debug) { + console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${req.path}`) + } + + return res.send(template) + } + } + + // 不是爬虫或模板不存在,继续处理SPA + next() + } + } + + /** + * Koa中间件 + */ + koa() { + return async (ctx, next) => { + // 检测是否为爬虫 + if (this.detector.isCrawler(ctx.req)) { + const templateName = this.getTemplatePath(ctx.path) + const template = this.getTemplate(templateName) + + if (template) { + ctx.type = 'text/html; charset=utf-8' + ctx.set('X-SEOMiddleware', 'prerendered') + + if (this.debug) { + console.log(`[SEOMiddleware] 返回SEO模板: ${templateName} for ${ctx.path}`) + } + + ctx.body = template + return + } + } + + await next() + } + } + + /** + * 重新加载模板缓存 + */ + reloadCache() { + this.templateCache.clear() + this.cacheTemplates() + console.log('[SEOMiddleware] 模板缓存已重新加载') + } +} + +module.exports = SEOMiddleware diff --git a/server/nginx-www.quannengcha.com.conf b/server/nginx-www.quannengcha.com.conf new file mode 100644 index 0000000..3007f88 --- /dev/null +++ b/server/nginx-www.quannengcha.com.conf @@ -0,0 +1,419 @@ +# Nginx 配置 - www.quannengcha.com(全能查) +# 含爬虫检测 + SEO 静态页回退,与 useSEO.js / generate-seo-templates.cjs 路由一致 +# 部署时将 public/seo-templates/ 下所有 .html 拷贝到 /www/sites/www.quannengcha.com/static-pages/ + +server { + listen 80; + listen 443 ssl http2; + server_name www.quannengcha.com quannengcha.com p.quannengcha.com; + + # 网站根目录(SPA) + root /www/sites/www.quannengcha.com/index; + + # SEO 静态页面目录(与 generate-seo-templates.cjs 输出一致) + set $static_root /www/sites/www.quannengcha.com/index/static-pages; + + # 默认首页 + index index.php index.html index.htm default.php default.htm default.html; + + # ======================================== + # 爬虫检测(核心 SEO 逻辑) + # ======================================== + set $is_bot 0; + + # Google 爬虫 + if ($http_user_agent ~* (googlebot|googlebot-image|googlebot-news|googlebot-video|mediapartners-google|adsbot-google)) { + set $is_bot 1; + } + + # 百度爬虫 + if ($http_user_agent ~* (baiduspider|baiduspider-mobile|baiduspider-image|baiduspider-video|baiduspider-news)) { + set $is_bot 1; + } + + # 必应爬虫 + if ($http_user_agent ~* (bingbot|msnbot|bingpreview)) { + set $is_bot 1; + } + + # 360 爬虫 + if ($http_user_agent ~* "360spider|360Spider") { + set $is_bot 1; + } + + # 搜狗爬虫 + if ($http_user_agent ~* "(sogou spider|sogou-orion|Sogou web spider)") { + set $is_bot 1; + } + + # 头条爬虫 + if ($http_user_agent ~* "bytespider|Bytespider") { + set $is_bot 1; + } + + # 神马爬虫 + if ($http_user_agent ~* "yisouspider|YisouSpider") { + set $is_bot 1; + } + + # 其他常见爬虫 + if ($http_user_agent ~* "(spider|crawl|bot|slurp|yandex|duckduckbot|facebookexternalhit|twitterbot|linkedinbot|pinterest|applebot)") { + set $is_bot 1; + } + + # ======================================== + # 通用代理头设置 + # ======================================== + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + + # ======================================== + # 日志配置 + # ======================================== + access_log /www/sites/www.quannengcha.com/log/access.log main; + error_log /www/sites/www.quannengcha.com/log/error.log; + + # ======================================== + # Let's Encrypt 验证 + # ======================================== + location ^~ /.well-known/acme-challenge { + allow all; + root /usr/share/nginx/html; + } + + # ======================================== + # SSL 证书配置 + # ======================================== + ssl_certificate /www/sites/www.quannengcha.com/ssl/fullchain.pem; + ssl_certificate_key /www/sites/www.quannengcha.com/ssl/privkey.pem; + ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1; + ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + error_page 497 https://$host$request_uri; + proxy_set_header X-Forwarded-Proto https; + add_header Strict-Transport-Security "max-age=31536000"; + + # ======================================== + # SEO 静态文件(sitemap.xml, robots.txt) + # ======================================== + location = /sitemap.xml { + root /www/sites/www.quannengcha.com/static-pages; + default_type application/xml; + } + + location = /robots.txt { + root /www/sites/www.quannengcha.com/static-pages; + default_type text/plain; + } + + # ======================================== + # 首页(爬虫 → 静态页,用户 → SPA) + # ======================================== + location = / { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /index.html break; + } + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"; + add_header Pragma "no-cache"; + add_header Expires 0; + } + + # ======================================== + # SEO 关键页面(与 useSEO.js / middleware 一致;子路径在前) + # ======================================== + + # 合作政策指南(子路径须在 /agent 之前) + location = /agent/system-guide { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /agent-system-guide.html break; + } + try_files $uri $uri/ /index.html; + } + + # 个人综合风险分析 + location = /inquire/riskassessment { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-riskassessment.html break; + } + try_files $uri $uri/ /index.html; + } + + # 企业工商信用画像 + location = /inquire/companyinfo { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-companyinfo.html break; + } + try_files $uri $uri/ /index.html; + } + + # 综合履约评分检测 + location = /inquire/preloanbackgroundcheck { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-preloanbackgroundcheck.html break; + } + try_files $uri $uri/ /index.html; + } + + # 婚前综合背景了解 + location = /inquire/marriage { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-marriage.html break; + } + try_files $uri $uri/ /index.html; + } + + # 职场背景核验报告 + location = /inquire/backgroundcheck { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-backgroundcheck.html break; + } + try_files $uri $uri/ /index.html; + } + + # 家政人员背景核实 + location = /inquire/homeservice { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-homeservice.html break; + } + try_files $uri $uri/ /index.html; + } + + # 个人履约能力评估 + location = /inquire/consumerFinanceReport { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /inquire-consumerFinanceReport.html break; + } + try_files $uri $uri/ /index.html; + } + + # 代理中心 + location = /agent { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /agent.html break; + } + try_files $uri $uri/ /index.html; + } + + # 使用指南(子路径须在 /help 之前) + location = /help/guide { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /help-guide.html break; + } + try_files $uri $uri/ /index.html; + } + + # 帮助中心 + location = /help { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /help.html break; + } + try_files $uri $uri/ /index.html; + } + + # 示例报告 + location = /example { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /example.html break; + } + try_files $uri $uri/ /index.html; + } + + # 客服中心 + location = /service { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /service.html break; + } + try_files $uri $uri/ /index.html; + } + + # 合伙人计划 + location = /promote { + if ($is_bot = 1) { + root /www/sites/www.quannengcha.com/static-pages; + rewrite ^ /promote.html break; + } + try_files $uri $uri/ /index.html; + } + + # ======================================== + # API 代理配置 + # ======================================== + + # 处理后端API请求 + location /api { + proxy_pass http://127.0.0.1:17990; + proxy_set_header Host 127.0.0.1:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + proxy_set_header X-Host $host:$server_port; + proxy_set_header X-Scheme $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /s/ { + proxy_pass http://127.0.0.1:21204; + proxy_set_header Host 127.0.0.1:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + proxy_set_header X-Host $host:$server_port; + proxy_set_header X-Scheme $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location /api/v1 { + set $target_backend "http://127.0.0.1:21204"; + if ($host ~* ^([a-zA-Z0-9-]+\.)?tianyuancha\.cn$) { + set $target_backend "http://127.0.0.1:20004"; + } + if ($host ~* ^([a-zA-Z0-9-]+\.)?quannengcha\.com$) { + set $target_backend "http://127.0.0.1:21204"; + } + proxy_pass $target_backend; + proxy_set_header Host 127.0.0.1:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + proxy_set_header X-Host $host:$server_port; + proxy_set_header X-Scheme $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + proxy_send_timeout 30s; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # V2 + location /apiv2 { + proxy_pass http://127.0.0.1:18990; + proxy_set_header Host 127.0.0.1:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + add_header X-Cache $upstream_cache_status; + proxy_set_header X-Host $host:$server_port; + proxy_set_header X-Scheme $scheme; + proxy_connect_timeout 30s; + proxy_read_timeout 86400s; + } + + location ^~ /api/v1/chat { + resolver 8.8.8.8 114.114.114.114 valid=10s; + resolver_timeout 5s; + set $backend "chat.guimiaokeji.com"; + rewrite ^/api/v1/(.*)$ /$1 break; + proxy_pass https://$backend; + proxy_set_header Host $backend; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + add_header X-Cache $upstream_cache_status; + add_header Cache-Control no-cache; + proxy_ssl_server_name off; + proxy_buffering off; + } + + # ======================================== + # 静态资源缓存(图片、字体等) + # ======================================== + location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf|eot|otf)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # ======================================== + # HTML/JS/CSS 无缓存策略 + # ======================================== + location ~* \.(html|htm|js|css|json|xml)$ { + add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # ======================================== + # SPA 路由回退(其他所有路由) + # ======================================== + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"; + add_header Pragma "no-cache"; + add_header Expires 0; + } + + # ======================================== + # 错误页面 + # ======================================== + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + + # ======================================== + # Gzip 压缩 + # ======================================== + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json image/svg+xml; + + # ======================================== + # 安全头 + # ======================================== + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # ======================================== + # HTTP 强制跳转 HTTPS + # ======================================== + if ($scheme = http) { + return 301 https://$host$request_uri; + } + + # ======================================== + # 引入重定向配置 + # ======================================== + include /www/sites/www.quannengcha.com/redirect/*.conf; +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..dbb7c3e --- /dev/null +++ b/server/package.json @@ -0,0 +1,27 @@ +{ + "name": "tydata-seo-server", + "version": "1.0.0", + "description": "SPA SEO 优化 - 爬虫检测与静态 HTML 回退,与 useSEO.js 同步", + "main": "server-example-express.js", + "scripts": { + "start": "node server-example-express.js", + "dev": "node server-example-express.js", + "generate": "node generate-seo-templates.cjs", + "test": "node test-seo.js", + "test:crawler": "node test-crawler-detection.js" + }, + "keywords": [ + "seo", + "crawler", + "spa", + "prerender" + ], + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "compression": "^1.7.4" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/server/server-example-express.js b/server/server-example-express.js new file mode 100644 index 0000000..811d63e --- /dev/null +++ b/server/server-example-express.js @@ -0,0 +1,36 @@ +/** + * Express服务器示例 + * 展示如何集成SEO中间件 + */ + +const express = require('express') +const path = require('path') +const SEOMiddleware = require('./middleware') + +const app = express() +const port = process.env.PORT || 3000 + +// 初始化SEO中间件 +const seoMiddleware = new SEOMiddleware({ + templateDir: path.join(__dirname, '../public/seo-templates'), + debug: process.env.NODE_ENV === 'development' +}) + +// 应用SEO中间件(必须在静态文件服务之前) +app.use(seoMiddleware.express()) + +// 静态文件服务 +app.use(express.static(path.join(__dirname, '../dist'))) + +// SPA路由处理 +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../dist/index.html')) +}) + +// 启动服务器 +app.listen(port, () => { + console.log(`🚀 服务器运行在 http://localhost:${port}`) + console.log(`🔍 SEO中间件已启用`) +}) + +module.exports = app diff --git a/server/test-crawler-detection.js b/server/test-crawler-detection.js new file mode 100644 index 0000000..d494d78 --- /dev/null +++ b/server/test-crawler-detection.js @@ -0,0 +1,112 @@ +/** + * 爬虫检测测试脚本 + * 用于验证爬虫检测功能是否正常工作 + */ + +const CrawlerDetector = require('./crawler-detector') + +const detector = new CrawlerDetector() + +// 测试用例 +const testCases = [ + // 爬虫User-Agent + { userAgent: 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)', expected: true, description: '百度爬虫' }, + { userAgent: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', expected: true, description: 'Google爬虫' }, + { userAgent: 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', expected: true, description: '必应爬虫' }, + { userAgent: 'Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)', expected: true, description: '搜狗爬虫' }, + { userAgent: '360Spider', expected: true, description: '360爬虫' }, + { userAgent: 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)', expected: true, description: 'Facebook爬虫' }, + { userAgent: 'Twitterbot/1.0', expected: true, description: 'Twitter爬虫' }, + { userAgent: 'LinkedInBot/1.0 (compatible; Mozilla/5.0; +https://www.linkedin.com/help/linkedin/answer/8665)', expected: true, description: 'LinkedIn爬虫' }, + + // 正常浏览器User-Agent + { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', expected: false, description: 'Chrome浏览器' }, + { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0', expected: false, description: 'Firefox浏览器' }, + { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15', expected: false, description: 'Safari浏览器' }, + { userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', expected: false, description: 'iPhone Safari' }, + { userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36', expected: false, description: 'Android Chrome' }, + { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0', expected: false, description: 'Edge浏览器' }, + + // 边界情况 + { userAgent: '', expected: false, description: '空User-Agent' }, + { userAgent: 'Mozilla/5.0 (compatible; MyBot/1.0)', expected: true, description: '包含bot关键词' }, + { userAgent: 'Mozilla/5.0 (compatible; Spider/1.0)', expected: true, description: '包含spider关键词' }, + { userAgent: 'Mozilla/5.0 (compatible; Crawler/1.0)', expected: true, description: '包含crawler关键词' } +] + +console.log('='.repeat(70)) +console.log('爬虫检测测试') +console.log('='.repeat(70)) +console.log() + +let passed = 0 +let failed = 0 + +testCases.forEach((testCase, index) => { + const req = { + headers: { + 'user-agent': testCase.userAgent + } + } + + const result = detector.isCrawler(req) + const success = result === testCase.expected + const status = success ? '✓ 通过' : '✗ 失败' + const crawlerType = result ? detector.getCrawlerType(testCase.userAgent) : 'N/A' + + if (success) { + passed++ + console.log(`${status} 测试 ${index + 1}: ${testCase.description}`) + } else { + failed++ + console.error(`${status} 测试 ${index + 1}: ${testCase.description}`) + console.error(` User-Agent: ${testCase.userAgent.substring(0, 80)}...`) + console.error(` 预期: ${testCase.expected}, 实际: ${result}`) + } + + if (result) { + console.log(` 识别为: ${crawlerType} 爬虫`) + } +}) + +console.log() +console.log('='.repeat(70)) +console.log(`测试结果: ${passed} 通过, ${failed} 失败, 共 ${testCases.length} 个测试`) +console.log('='.repeat(70)) +console.log() + +// 测试爬虫类型识别 +console.log('爬虫类型识别测试:') +console.log('-'.repeat(70)) + +const crawlerTypes = [ + { userAgent: 'Baiduspider', expected: 'baidu', description: '百度爬虫' }, + { userAgent: 'Googlebot', expected: 'google', description: 'Google爬虫' }, + { userAgent: 'bingbot', expected: 'bing', description: '必应爬虫' }, + { userAgent: '360spider', expected: '360', description: '360爬虫' }, + { userAgent: 'sogou spider', expected: 'sogou', description: '搜狗爬虫' }, + { userAgent: 'facebookexternalhit', expected: 'facebook', description: 'Facebook爬虫' }, + { userAgent: 'Twitterbot', expected: 'twitter', description: 'Twitter爬虫' }, + { userAgent: 'linkedinbot', expected: 'linkedin', description: 'LinkedIn爬虫' } +] + +let typePassed = 0 +crawlerTypes.forEach(test => { + const result = detector.getCrawlerType(test.userAgent) + const success = result === test.expected + + if (success) { + typePassed++ + console.log(`✓ ${test.description}: ${result}`) + } else { + console.error(`✗ ${test.description}: 预期 ${test.expected}, 实际 ${result}`) + } +}) + +console.log() +console.log('='.repeat(70)) +console.log(`爬虫类型识别: ${typePassed}/${crawlerTypes.length} 正确`) +console.log('='.repeat(70)) + +// 退出码 +process.exit(failed === 0 ? 0 : 1) diff --git a/server/test-seo.js b/server/test-seo.js new file mode 100644 index 0000000..209c1d7 --- /dev/null +++ b/server/test-seo.js @@ -0,0 +1,178 @@ +/** + * SEO 端到端检测脚本 + * 模拟爬虫与普通用户请求,验证是否返回正确的页面 + * + * 使用前请先启动服务器: npm run start + * 然后运行: npm run test 或 node test-seo.js + */ + +const http = require('http') +const https = require('https') + +const BASE_URL = process.env.SEO_TEST_URL || 'http://localhost:3000' + +// 要检测的路由及期望的 SEO 标题关键词(与 useSEO.js 一致,天远数据) +const ROUTES = [ + { path: '/', titleKeyword: '天远数据' }, + { path: '/agent', titleKeyword: '天远数据代理' }, + { path: '/help', titleKeyword: '天远数据帮助中心' }, + { path: '/inquire/personalData', titleKeyword: '个人综合风险报告' }, + { path: '/agent/promote', titleKeyword: '推广码' }, + { path: '/historyQuery', titleKeyword: '我的报告' } +] + +function request(url, userAgent) { + return new Promise((resolve, reject) => { + const lib = url.startsWith('https') ? https : http + const req = lib.get(url, { + headers: { 'User-Agent': userAgent }, + timeout: 10000 + }, res => { + const chunks = [] + res.on('data', chunk => chunks.push(chunk)) + res.on('end', () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: Buffer.concat(chunks).toString('utf-8') + }) + }) + }) + req.on('error', reject) + req.on('timeout', () => { + req.destroy() + reject(new Error('请求超时')) + }) + }) +} + +function extractTitle(html) { + const match = html.match(/]*>([^<]+)<\/title>/i) + return match ? match[1].trim() : null +} + +function hasMetaDescription(html) { + return /`、``、``、`` +- 用 **普通浏览器 UA** 再请求一遍,确认仍是 200(SPA 正常) + +全部通过即说明:爬虫拿到的是 SEO 模板,普通用户拿到的是 SPA。 + +--- + +## 二、用 curl 手动检测 + +在服务器已启动的前提下,在终端执行: + +### 爬虫应拿到“带 TDK 的 HTML” + +```bash +# 模拟百度爬虫请求首页 +curl -s -A "Baiduspider/2.0" http://localhost:3000/ | findstr /i "title description keywords canonical" +``` + +应能看到包含「天远数据」的 title,以及 description、keywords、canonical 等标签。 + +**Windows 下中文乱码说明**:服务器返回的是 UTF-8,CMD 默认是 GBK,直接 `curl … | findstr` 会看到乱码(如 `澶╄繙鏁版嵁`)或出现 “FINDSTR: 写入错误”。可任选一种方式解决: + +```cmd +:: 方式 1:先切到 UTF-8 再执行(CMD) +chcp 65001 +curl -s -A "Baiduspider/2.0" https://www.tianyuandb.com/ | findstr /i "title description" +``` + +```powershell +# 方式 2:PowerShell 下指定输出编码 +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +curl -s -A "Baiduspider/2.0" https://www.tianyuandb.com/ | Select-String -Pattern "title|description" +``` + +```cmd +:: 方式 3:保存到文件后用编辑器打开(任意编码都行) +curl -s -A "Baiduspider/2.0" https://www.tianyuandb.com/ -o seo-test.html +:: 用记事本/VSCode 打开 seo-test.html,选 UTF-8 即可看到正确中文 +``` + +```bash +# 看完整 HTML 前几行(含 ) +curl -s -A "Baiduspider/2.0" http://localhost:3000/ | more +``` + +### 普通用户应拿到 SPA(一般是带 script 的 index.html) + +```bash +# 不带爬虫 UA,相当于普通浏览器 +curl -s http://localhost:3000/ | findstr /i "script root app" +``` + +通常会有 `id="app"` 或大量 ` diff --git a/微信授权流程设计文档.md b/微信授权流程设计文档.md deleted file mode 100644 index 84a0051..0000000 --- a/微信授权流程设计文档.md +++ /dev/null @@ -1,256 +0,0 @@ -# 微信 H5 授权流程重构 - 设计文档 - -## 📋 核心业务需求 - -在微信 H5 环境中,**整个应用在没有有效 token 时,都需要进行微信授权登录**。授权完成后,用户应该被重定向回他们尝试访问的原始页面。 - ---- - -## 🔄 流程设计 - -### **完整的授权流程图** - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 用户在微信中访问任意页面(无 token) │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 路由守卫检测:微信 + 无 token │ -│ → 保存目标路由到 authStore.pendingRoute │ -│ → 生成微信授权 URL │ -│ → window.location.href = 授权 URL (不调用 next()) │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 跳转到微信授权页面 │ -│ https://open.weixin.qq.com/connect/oauth2/authorize?... │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ┌─────────┴─────────┐ - │ 用户点击"同意" │ - └────────┬──────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 微信回调:redirectUri?code=xxx&state=yyy │ -│ 浏览器重新加载应用,URL 中包含 code/state 参数 │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ App.vue onMounted 检测到 code + state 参数 │ -│ → 调用 handleWeixinAuthCallback(code) │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 1. 调用后端接口 /user/wxh5Auth 交换 token │ -│ 2. 保存 token 到 localStorage │ -│ 3. 清理 URL 中的 code/state 参数 │ -│ 4. 获取用户信息 │ -│ 5. 标记授权完成 authStore.completeWeixinAuth() │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 获取 pendingRoute,重定向回原始页面 │ -│ router.replace(pendingRoute) │ -│ 如果没有 pendingRoute,重定向到首页 "/" │ -└──────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ ✅ 授权完成,用户看到原始页面 │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📁 关键文件修改 - -### **1. `src/router/index.js` - 路由守卫** - -**关键变化:** -- 检测到 **微信 + 无 token** 的情况 -- **直接发起授权**(调用 `window.location.href`) -- **不调用 `next()`**,完全停止导航 -- 授权 URL 中的 redirectUri 指向当前页面(清理旧的 code/state 参数) - -**代码片段:** -```javascript -if (isWeChat.value && !isAuthenticated && !isTokenExpired) { - // 保存目标路由 - authStore.startWeixinAuth(to); - - // 生成授权 URL - const appId = import.meta.env.VITE_WECHAT_APP_ID; - const url = new URL(window.location.href); - const params = new URLSearchParams(url.search); - params.delete("code"); - params.delete("state"); - const cleanUrl = `${url.origin}${url.pathname}${params.toString() ? "?" + params.toString() : ""}`; - const redirectUri = encodeURIComponent(cleanUrl); - const weixinAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=snsapi_base#wechat_redirect`; - - // 直接跳转,不调用 next() - window.location.href = weixinAuthUrl; - return; -} -``` - -### **2. `src/App.vue` - 授权回调处理** - -**关键职责:** -1. **检测回调**:读取 URL 中的 code/state 参数 -2. **处理回调**:调用 `/user/wxh5Auth` 交换 token -3. **恢复导航**:使用保存的 pendingRoute 重定向用户 - -**核心函数:`handleWeixinAuthCallback(code)`** -```javascript -const handleWeixinAuthCallback = async (code) => { - // 1. 交换 token - const { data, error } = await useApiFetch("/user/wxh5Auth").post({ code }).json(); - - // 2. 保存 token - localStorage.setItem("token", data.value.data.accessToken); - localStorage.setItem("refreshAfter", data.value.data.refreshAfter); - localStorage.setItem("accessExpire", data.value.data.accessExpire); - - // 3. 清理 URL - const url = new URL(window.location.href); - const params = new URLSearchParams(url.search); - params.delete("code"); - params.delete("state"); - window.history.replaceState({}, "", newUrl); - - // 4. 获取用户信息 - await userStore.fetchUserInfo(); - - // 5. 标记授权完成 - authStore.completeWeixinAuth(); - - // 6. 重定向回 pendingRoute - const pendingRoute = authStore.pendingRoute; - await router.replace(pendingRoute || "/"); -}; -``` - -### **3. `src/stores/authStore.js` - 授权状态管理** - -**关键方法:** - -| 方法 | 作用 | -|------|------| -| `startWeixinAuth(targetRoute)` | 开始授权,保存目标路由 | -| `completeWeixinAuth()` | 标记授权完成 | -| `clearPendingRoute()` | 清除待处理路由 | -| `resetAuthState()` | 重置所有授权状态 | -| `restoreFromStorage()` | 页面刷新后恢复状态 | - ---- - -## 🎯 核心特性 - -### **1. 同步流程控制** -- ❌ **不使用** `watch` 监听 state 的变化 -- ❌ **不使用** 异步的 route guard + next() 来触发授权 -- ✅ **直接** 在 route guard 中调用 `window.location.href` 发起授权 - -**为什么?** -- Watch 是异步的,可能被路由完成打断 -- next() 后可能路由已经切换,导致授权流程混乱 -- 直接跳转 URL 更加可靠和同步 - -### **2. 状态持久化** -- 授权开始时保存到 localStorage -- 页面刷新时自动恢复状态 -- 防止超时(30秒)导致的无限授权循环 - -### **3. URL 参数清理** -- 每次发起授权前,都清理 URL 中旧的 code/state 参数 -- 防止参数被重复编码进 redirectUri -- 微信回调时才注入新的 code/state - -### **4. 目标页面恢复** -- 路由守卫保存用户尝试访问的页面 -- 授权完成后自动重定向到该页面 -- 用户无感知的完整授权体验 - ---- - -## 🔐 场景处理 - -### **场景 1:无 token + 访问开放页面** -``` -用户访问 "/" → 微信检测无 token → 发起授权 → 授权完成后回到首页 -``` - -### **场景 2:无 token + 访问推广链接** -``` -用户访问 "/agent/promotionInquire/abc123" - → 微信检测无 token - → 保存 pendingRoute = "/agent/promotionInquire/abc123" - → 发起授权 - → 授权完成后回到 "/agent/promotionInquire/abc123" -``` - -### **场景 3:有效 token + 访问任意页面** -``` -用户有 token 且未过期 → 路由守卫放行 → 直接加载页面,无授权 -``` - -### **场景 4:Token 过期 + 访问需登录页面** -``` -用户在非微信环境 → 检测到 token 过期 → 跳转登录页面 -``` - -### **场景 5:授权超时** -``` -用户授权流程中,30 秒内未完成 → 自动重置状态 → 页面刷新时重新授权 -``` - ---- - -## 🐛 常见问题排查 - -### **Q1:为什么授权后没有跳到原页面?** -**A:** 检查 authStore 中是否有 pendingRoute -```javascript -console.log('pendingRoute:', authStore.pendingRoute); -``` - -### **Q2:为什么一直在授权页面循环?** -**A:** 可能是授权回调处理失败。检查: -1. 后端 `/user/wxh5Auth` 接口是否正确返回 token -2. Token 是否正确保存到 localStorage -3. URL 中的 code/state 是否正确清理 - -### **Q3:为什么刷新页面后授权状态丢失?** -**A:** authStore.restoreFromStorage() 应该在 onMounted 中被调用。检查: -```javascript -authStore.restoreFromStorage(); // 这行很重要! -``` - -### **Q4:在 PC 上测试,为什么无法触发授权?** -**A:** 授权只在微信环境中触发(isWeChat.value === true)。在 PC 上: -- 访问需登录的页面 → 跳转登录页 -- 访问开放页面 → 正常加载 - ---- - -## 📝 总结 - -这个重构的核心思想是: -1. **路由守卫 = 决策者**:检测到需要授权就立即发起 -2. **App.vue = 处理器**:处理授权回调和状态恢复 -3. **AuthStore = 状态管理**:保存和恢复授权相关的状态 -4. **同步流程 = 高可靠性**:避免异步竞态条件 - -通过这个设计,微信授权流程变得: -- ✅ 清晰易懂 -- ✅ 可靠稳定 -- ✅ 易于测试和调试 -- ✅ 完整的用户体验 diff --git a/微信授权重定向问题分析与修复.md b/微信授权重定向问题分析与修复.md deleted file mode 100644 index 8227821..0000000 --- a/微信授权重定向问题分析与修复.md +++ /dev/null @@ -1,121 +0,0 @@ -# 微信授权重定向问题分析与修复 - -## 问题描述 -微信授权回调成功后,虽然 `pendingRoute` 有正确的值,回调成功也能跳转到应该到达的页面(如 `promotionInquire`),但在加载该页面后,又会自动跳转到首页。 - -## 根本原因分析 - -### 问题 1: pendingRoute 清除时序错误 ❌ -**位置**: `src/App.vue` 第 69-72 行 - -**原始代码**: -```javascript -if (pendingRoute) { - authStore.clearPendingRoute(); // ❌ 先清除 - await router.replace(pendingRoute); // 用已清空的 null 值跳转 -} -``` - -**问题**: -- `clearPendingRoute()` 将 `authStore.pendingRoute` 设为 `null` -- 之后 `router.replace(pendingRoute)` 使用的是已清空的 `null` 值 -- 导致实际跳转到 `undefined` 而非目标路由 - -**修复**: 先跳转再清除 ✅ - -### 问题 2: 路由守卫重复授权防护缺失 ❌ -**位置**: `src/router/index.js` 第 428-444 行 - -**问题**: -- 当用户在授权完成后,路由守卫再次执行时 -- 如果 `isWeChat.value && !isAuthenticated` 条件满足 -- 会重新触发授权流程,导致页面跳转 - -**原因**: -- `authStore.isWeixinAuthing` 和 `authStore.weixinAuthComplete` 状态未被路由守卫检查 -- 虽然 token 已保存,但守卫仍可能因某些时序问题再次触发授权 - -**修复**: 在路由守卫中增加授权状态检查 ✅ - -### 问题 3: localStorage 同步问题 -**位置**: `src/stores/authStore.js` - -**问题**: -- 清除 `pendingRoute` 时,可能只清除内存状态,localStorage 可能残留数据 -- 页面刷新时恢复的数据可能不完整 - -**修复**: 确保内存和 localStorage 同步清除 ✅ - -## 修复清单 - -### ✅ 修复 1: App.vue 中的时序问题 -```javascript -if (pendingRoute) { - // ✅ 先跳转 - await router.replace(pendingRoute); - // ✅ 再清除 - authStore.clearPendingRoute(); -} -``` - -### ✅ 修复 2: 路由守卫的防护检查 -在路由守卫中增加: -```javascript -if (isWeChat.value && !isAuthenticated && !isTokenExpired) { - // ✨ 新增:检查是否正在授权或已完成授权 - if (authStore.isWeixinAuthing || authStore.weixinAuthComplete) { - console.warn("⚠️ WeChat auth already in progress or completed"); - NProgress.done(); - next(); - return; - } - // ... 继续原有逻辑 -} -``` - -### ✅ 修复 3: authStore 中的日志和同步 -增强 `clearPendingRoute()` 和 `restoreFromStorage()` 方法的日志记录和错误处理。 - -### ✅ 修复 4: PromotionInquire.vue 中的延迟处理 -添加延迟以确保页面完全加载后再进行任何路由跳转: -```javascript -function isFinishPayment() { - const query = new URLSearchParams(window.location.search); - let orderNo = query.get("out_trade_no"); - if (orderNo) { - // ✨ 延迟 100ms 确保页面加载完成 - setTimeout(() => { - router.push({ path: "/report", query: { orderNo } }); - }, 100); - } -} -``` - -## 调试建议 - -### 观察日志的关键点: -1. **微信授权开始**: 看是否出现 "Triggering WeChat auth from route guard" -2. **Token 保存**: 看是否出现 "✅ Token saved successfully" -3. **用户信息加载**: 看 "✅ User info fetched" 是否成功 -4. **pendingRoute 获取**: 看 "🎯 pendingRoute:" 后面的值 -5. **导航执行**: 看 "🚀 Navigating to pendingRoute:" 和 "✅ Navigated to pendingRoute" -6. **是否重复授权**: 看是否出现 "⚠️ WeChat auth already in progress" - -### 测试步骤: -1. 在微信中打开推广链接: `https://xxx.com/agent/promotionInquire/abc123` -2. 观察控制台日志,确保看到上述所有成功日志 -3. 验证最终页面是 `promotionInquire` 而非首页 - -## 修复文件列表 -- ✅ `src/App.vue` - 修复 pendingRoute 时序问题 -- ✅ `src/router/index.js` - 增强路由守卫防护 -- ✅ `src/stores/authStore.js` - 改进日志和同步 -- ✅ `src/views/PromotionInquire.vue` - 添加延迟处理 - -## 总结 -这个问题是由多个时序和状态管理问题联合造成的: -1. pendingRoute 被过早清除导致跳转失败 -2. 路由守卫缺少防护措施可能重复授权 -3. 状态同步不完整可能导致恢复失败 - -通过修复这些问题,应该能够确保微信授权后正确重定向到目标页面。 diff --git a/阿里云验证码接入.md b/阿里云验证码接入.md deleted file mode 100644 index 92e27d9..0000000 --- a/阿里云验证码接入.md +++ /dev/null @@ -1,456 +0,0 @@ -## 阿里云滑块验证码接入说明(含加密模式) - - -- 前端需要做什么 -- 后端需要做什么 -- 非加密模式 vs 加密模式(`EncryptedSceneId`) -- 本项目修改了哪些文件,可以作为参考实现 - -下面所有内容以当前项目(`tyc-webview-v2` + `tyc-server-v2`)为例,按步骤说明。 - ---- - -配置项 - -go get github.com/alibabacloud-go/captcha-20230305/client - -Captcha: - # 建议与短信相同的 AccessKey,或单独为验证码创建子账号 - AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9" - AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65" - # 验证码服务 Endpoint,国内一般为 captcha.cn-shanghai.aliyuncs.com - EndpointURL: "captcha.cn-shanghai.aliyuncs.com" - # 阿里云控制台中该场景的 SceneId,请替换为真实值 - SceneID: "wynt39to" - # 验证码控制台中的 ekey(通常为 Base64 字符串),用于生成 EncryptedSceneId - EKey: "" - -index.html - - -## 一、整体流程概览 - -### 1.1 场景说明 - -我们使用的是 **阿里云验证码 2.0 / V3 架构** 的滑块验证码,前后端配合流程如下: - -1. 前端在「获取短信验证码」/「查询前无短信码的产品」时,先弹出阿里云滑块验证码。 -2. 用户拖动成功后,前端拿到 `captchaVerifyParam`,携带到后端业务接口。 -3. 后端使用阿里云官方 Go SDK(`captcha-20230305`)+ 我们的封装,对 `captchaVerifyParam` 做服务端校验: - - 校验通过:继续后续业务逻辑(发短信、查询等)。 - - 校验失败:直接返回业务错误,例如「图形验证码校验失败」。 -4. 对于 **加密模式** 场景,前端还需要在初始化时传入 `EncryptedSceneId`,而 `EncryptedSceneId` 由后端用控制台的 `ekey` 生成。 - -### 1.2 使用场景(当前项目) - -- 登录页: - - **获取短信验证码**:必须先通过滑块验证。 - - **提交登录**:只校验短信验证码,不再做滑块。 -- Inquire 查询页: - - 有「短信验证码」字段的产品:点击「获取验证码」前滑块;点击「查询」时不需要再次滑块。 - - 无「短信验证码」的产品:点击「查询」前滑块。 - ---- - -## 二、前端接入说明(以 Vue 3 / Vite 为例) - -### 2.1 全局基础集成(`index.html`) - -在入口 HTML 中引入阿里云验证码脚本,并预留一个容器: - -- 文件:`tyc-webview-v2/index.html` - -关键片段: - -```html - - -... - -
-
- -``` - -注意: - -- `#captcha-element` 是验证码挂载容器,必须存在。 -- `AliyunCaptcha.js` 必须在前端业务代码(`/src/main.js`)之前加载。 - -### 2.2 通用封装:`useAliyunCaptcha` - -- 文件:`tyc-webview-v2/src/composables/useAliyunCaptcha.js` - -该 composable 封装了: - -- **初始化阿里云验证码实例**(含加密 / 非加密模式); -- 提供一个通用方法 `runWithCaptcha(bizVerify, onSuccess)`: - - `bizVerify(captchaVerifyParam)`:前端回调,内部调用后端业务接口(发短信、查询等),返回 `{ data, error }`(`useApiFetch` 结果)。 - - `onSuccess(res)`:当业务 `code === 200` 时调用,`res` 为后端返回的数据。 - -使用方式示例: - -```js -import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha"; - -const { runWithCaptcha } = useAliyunCaptcha(); - -function sendLoginSms() { - if (!isPhoneValid.value) return; - - runWithCaptcha( - (captchaVerifyParam) => - useApiFetch("auth/sendSms") - .post({ - mobile: phoneNumber.value, - actionType: "login", - captchaVerifyParam, - }) - .json(), - (res) => { - if (res.code === 200) { - // 成功:toast + 开始倒计时 + 聚焦输入框 - } else { - // 失败:toast 提示 - } - }, - ); -} -``` - -### 2.3 加密模式开关(前端) - -在 `useAliyunCaptcha.js` 顶部: - -```js -// 是否启用加密模式(通过环境变量控制) -const ENABLE_ENCRYPTED = - import.meta.env.VITE_ALIYUN_CAPTCHA_ENCRYPTED === "true"; -``` - -在 `ensureCaptchaInit()` 中: - -- **非加密模式**(`ENABLE_ENCRYPTED === false`): - - ```js - if (!ENABLE_ENCRYPTED) { - window.initAliyunCaptcha({ - SceneId: ALIYUN_CAPTCHA_SCENE_ID, - mode: "popup", - element: "#captcha-element", - getInstance(instance) { window.captcha = instance; ... }, - captchaVerifyCallback(param) { ... }, - onBizResultCallback(bizResult) { ... }, - slideStyle: { width: 360, height: 40 }, - language: "cn", - }); - return; - } - ``` - - 前端不会请求后端获取 `EncryptedSceneId`,只用 `SceneId` 初始化。 - -- **加密模式**(`ENABLE_ENCRYPTED === true`): - - ```js - const { data, error } = await useApiFetch("/captcha/encryptedSceneId") - .post() - .json(); - const resp = data?.value; - const encryptedSceneId = resp?.data?.encryptedSceneId; - if (error?.value || !encryptedSceneId) { - showToast({ message: "获取验证码参数失败,请稍后重试" }); - // 重置状态 - captchaInitialised = false; - captchaReadyPromise = null; - captchaReadyResolve = null; - return; - } - - window.initAliyunCaptcha({ - SceneId: ALIYUN_CAPTCHA_SCENE_ID, - EncryptedSceneId: encryptedSceneId, - mode: "popup", - element: "#captcha-element", - ... - }); - ``` - -> 在其它前端平台(React、原生 H5 等),可以复用同样的思路: -> - 抽一个 `runWithCaptcha` 工具; -> - 初始化逻辑中根据配置决定是否去后端拿 `EncryptedSceneId`,有则带上。 - -### 2.4 用户体验:加载提示 - -在 `runWithCaptcha` 中增加全局 Loading: - -```js -const loading = showLoadingToast({ - message: "安全验证加载中...", - forbidClick: true, - duration: 0, - loadingType: "spinner", -}); - -try { - // 设置 __captchaVerifyCallback / __onBizResultCallback - // await ensureCaptchaInit() - // await captchaReadyPromise - // window.captcha.show() -} finally { - closeToast(); -} -``` - -这样用户点击按钮后能看到“安全验证加载中”,避免误以为没反应。 - ---- - -## 三、后端接入说明(Go / go-zero) - -### 3.1 基本配置(`config.go` + `main.yaml`) - -- 文件:`app/main/api/internal/config/config.go` - -```go -type CaptchaConfig struct { - AccessKeyID string - AccessKeySecret string - EndpointURL string - SceneID string - EKey string // 加密模式用的 ekey(Base64) -} -``` - -- 文件:`app/main/api/etc/main.yaml` / `main.dev.yaml` - -```yaml -Captcha: - AccessKeyID: "你的AccessKeyId" - AccessKeySecret: "你的AccessKeySecret" - EndpointURL: "captcha.cn-shanghai.aliyuncs.com" - SceneID: "控制台场景ID" - EKey: "控制台上看到的 ekey(Base64 字符串)" -``` - -> 其它平台(Java/Spring、.NET 等)也需要同样的配置: -> - 一个 Captcha 配置块,包含 AK/SK、Endpoint、SceneId、EKey。 - -### 3.2 EncryptedSceneId 生成接口(加密模式) - -#### 3.2.1 生成函数 - -- 文件:`pkg/captcha/encrypt_scene.go` - -```go -package captcha - -import ( - "encoding/base64" - "fmt" - "time" - - lzcrypto "tyc-server/pkg/lzkit/crypto" -) - -// GenerateEncryptedSceneID: sceneId×tamp&expireTime -> AES-256-CBC + PKCS7 -> Base64(IV + ciphertext) -func GenerateEncryptedSceneID(sceneId, ekey string, expireSeconds int) (string, error) { - if expireSeconds <= 0 || expireSeconds > 86400 { - expireSeconds = 3600 - } - - ts := time.Now().Unix() - plaintext := fmt.Sprintf("%s&%d&%d", sceneId, ts, expireSeconds) - - keyBytes, err := base64.StdEncoding.DecodeString(ekey) - if err != nil { - return "", fmt.Errorf("decode ekey error: %w", err) - } - if len(keyBytes) != 32 { - return "", fmt.Errorf("invalid ekey length, need 32 bytes after base64 decode, got %d", len(keyBytes)) - } - - return lzcrypto.AesEncrypt([]byte(plaintext), keyBytes) -} -``` - -> 在其它语言上,只要完全按文档实现同样的算法即可。 - -#### 3.2.2 API 声明 - -- 文件:`app/main/api/desc/front/user.api` - -```go -@server ( - prefix: api/v1 - group: captcha -) -service main { - @doc "get encrypted scene id for aliyun captcha" - @handler getEncryptedSceneId - post /captcha/encryptedSceneId returns (GetEncryptedSceneIdResp) -} - -type ( - GetEncryptedSceneIdResp { - EncryptedSceneId string `json:"encryptedSceneId"` - } -) -``` - -#### 3.2.3 逻辑实现 - -- 文件:`app/main/api/internal/logic/captcha/getencryptedsceneidlogic.go` - -```go -func (l *GetEncryptedSceneIdLogic) GetEncryptedSceneId() (*types.GetEncryptedSceneIdResp, error) { - cfg := l.svcCtx.Config.Captcha - encrypted, err := captcha.GenerateEncryptedSceneID(cfg.SceneID, cfg.EKey, 3600) - if err != nil { - l.Errorf("generate encrypted scene id error: %+v", err) - return nil, err - } - return &types.GetEncryptedSceneIdResp{ - EncryptedSceneId: encrypted, - }, nil -} -``` - -> 其它平台只需要提供一个类似的 HTTP 接口即可: -> `POST /captcha/encryptedSceneId -> { encryptedSceneId: "xxx" }` - -### 3.3 验证 `captchaVerifyParam`(服务端验签) - -- 文件:`pkg/captcha/aliyun.go` - -使用阿里云官方 Go SDK 验证: - -```go -func Verify(cfg Config, captchaVerifyParam string) error { - if os.Getenv("ENV") == "development" { - return nil - } - if captchaVerifyParam == "" { - return errors.Wrapf(xerr.NewErrMsg("图形验证码校验失败"), "empty captchaVerifyParam") - } - - clientCfg := &openapi.Config{ - AccessKeyId: tea.String(cfg.AccessKeyID), - AccessKeySecret: tea.String(cfg.AccessKeySecret), - } - clientCfg.Endpoint = tea.String(cfg.EndpointURL) - client, err := captcha20230305.NewClient(clientCfg) - ... - - req := &captcha20230305.VerifyIntelligentCaptchaRequest{ - SceneId: tea.String(cfg.SceneID), - CaptchaVerifyParam: tea.String(captchaVerifyParam), - } - resp, err := client.VerifyIntelligentCaptcha(req) - ... - if tea.BoolValue(resp.Body.Result.VerifyResult) { - return nil - } - // 否则返回 "图形验证码校验失败" -} -``` - -在需要滑块的业务逻辑里(发送短信、查询),调用: - -```go -cfg := l.svcCtx.Config.Captcha -if err := captcha.Verify(captcha.Config{ - AccessKeyID: cfg.AccessKeyID, - AccessKeySecret: cfg.AccessKeySecret, - EndpointURL: cfg.EndpointURL, - SceneID: cfg.SceneID, -}, req.CaptchaVerifyParam); err != nil { - return nil, err -} -``` - -> 其它语言平台(Java、Node.js 等)可以用对应的阿里云 SDK 实现相同的服务端校验。 - -### 3.4 sendSms 接口示例 - -- 文件:`app/main/api/internal/logic/auth/sendsmslogic.go` - -```go -func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error { - // 1. 图形验证码校验 - cfg := l.svcCtx.Config.Captcha - if err := captcha.Verify(captcha.Config{...}, req.CaptchaVerifyParam); err != nil { - return err - } - - // 2. 原来的手机号加密、频率限制、阿里短信发送、Redis 存验证码等逻辑 -} -``` - ---- - -## 四、关键文件清单 - -- 前端: - - `tyc-webview-v2/index.html`:引入 `AliyunCaptcha.js`、提供 `#captcha-element`。 - - `tyc-webview-v2/src/composables/useAliyunCaptcha.js`:通用封装,支持加密/非加密模式 + Loading 提示。 - - `tyc-webview-v2/src/views/Login.vue`:获取短信前通过 `runWithCaptcha` 调用 `/auth/sendSms`。 - - `tyc-webview-v2/src/components/InquireForm.vue`:查询前根据是否有短信验证码字段决定是否通过 `runWithCaptcha`。 - -- 后端: - - `app/main/api/internal/config/config.go`:`CaptchaConfig` 增加 `EKey`。 - - `app/main/api/etc/main.yaml` / `main.dev.yaml`:补充 `Captcha` 配置(SceneID + EKey)。 - - `app/main/api/desc/front/user.api`:声明 `/captcha/encryptedSceneId` 接口。 - - `app/main/api/internal/logic/captcha/getencryptedsceneidlogic.go`:生成 `EncryptedSceneId`。 - - `pkg/captcha/encrypt_scene.go`:`GenerateEncryptedSceneID` 实现。 - - `pkg/captcha/aliyun.go`:`Verify` 实现(调用阿里云 SDK)。 - - `app/main/api/internal/logic/auth/sendsmslogic.go`:发送短信前调用 `captcha.Verify`。 - - `app/main/api/internal/logic/query/queryservicelogic.go`:对无短信验证码的产品,在查询前调用 `captcha.Verify`。 - ---- - -## 五、接入其它平台时的注意事项 - -1. **SceneId / ekey 必须一一对应** - - 控制台“场景管理”里的 SceneId 和 ekey 必须和配置里完全一致。 - - 多个场景要分别管理 SceneId / ekey。 - -2. **时间同步** - - 加密模式依赖 `timestamp` 和 `expireTime`,服务器时间要尽量准确(建议 NTP 同步)。 - -3. **前后端模式一致** - - 如果控制台开启了加密模式,前端必须**带上 `EncryptedSceneId`**; - - 如果前端只传 `SceneId`,在加密模式下会被阿里云直接拒绝(出现类似 F022 错误码)。 - -4. **错误处理** - - 服务端 `Verify` 出错(网络、阿里云故障)时,我们当前策略是**记录日志但视为通过**,防止影响业务可用性——这一点可根据各平台风险偏好调整。 - -5. **复用策略** - - 不同前端技术栈(Vue/React/小程序等),只要能做到: - 1. 初始化时根据配置决定是否从后端拿 `EncryptedSceneId`; - 2. 在业务请求前通过验证码拿到 `captchaVerifyParam` 并传给后端; - - 后端则统一在与风控相关的接口上调用相同的 `Verify` 封装即可。 - ---- - -## 六、推荐的接入步骤 - -1. 在阿里云验证码控制台创建场景,记下 **SceneId** 和 **ekey**,并根据需要打开「加密模式」。 -2. 在你自己的后端项目里: - - 增加 Captcha 配置块(AccessKeyID、AccessKeySecret、EndpointURL、SceneID、EKey); - - 实现 EncryptedSceneId 生成接口 `/captcha/encryptedSceneId`(可参考本项目的 `GenerateEncryptedSceneID`); - - 在需要滑块的业务接口(发短信、查询)前调用阿里云 SDK 做 `captchaVerifyParam` 校验。 -3. 在你自己的前端项目里: - - 页面引入 `AliyunCaptcha.js` 并预留验证码容器; - - 抽一个类似 `runWithCaptcha(bizVerify, onSuccess)` 的封装; - - 业务按钮点击时,不直接调接口,而是先触发 `runWithCaptcha`: - - 前端拉取 `EncryptedSceneId`(若启用加密模式); - - 初始化 `initAliyunCaptcha` 并弹出滑块; - - 滑块通过后才真正调用后端业务接口。