Compare commits

..

22 Commits

Author SHA1 Message Date
Mrx
c33167fe6b f 2026-06-09 21:26:43 +08:00
Mrx
a07cbbc607 f 2026-06-08 12:47:26 +08:00
Mrx
ed06e9c367 Merge branch 'main' of http://1.117.67.95:3000/team/tyc-webview-v2 2026-05-25 10:55:10 +08:00
Mrx
d3b7c0a3f6 f 2026-05-25 10:55:08 +08:00
4cedecb9ab f 2026-05-18 18:05:39 +08:00
Mrx
da64a678f5 f 2026-05-16 18:28:38 +08:00
Mrx
bbcc22071e f 2026-05-16 18:28:24 +08:00
Mrx
4a563f08d5 f 2026-05-16 17:42:48 +08:00
Mrx
1e9630c5ab f 2026-05-09 22:05:31 +08:00
Mrx
66ca9ad993 f 2026-05-09 18:16:43 +08:00
Mrx
f06b0b068a f 2026-05-09 17:04:55 +08:00
Mrx
a67094f90a f 2026-05-09 15:28:23 +08:00
Mrx
e48a8ee4ed f 2026-04-24 11:48:27 +08:00
1c67d7fbc9 f 2026-04-24 11:20:27 +08:00
6d5ce26d65 f 2026-04-24 11:20:25 +08:00
Mrx
955069693a Merge branch 'main' of http://1.117.67.95:3000/team/tyc-webview-v2 2026-03-21 11:04:07 +08:00
Mrx
ccf1e47bee f 2026-03-21 11:04:05 +08:00
038a8f7453 f 2026-03-02 16:50:53 +08:00
56ccf8a28d f 2026-03-02 16:50:34 +08:00
Mrx
a419940d88 f 2026-02-28 13:01:51 +08:00
Mrx
72bb706023 up seo 2026-02-28 12:45:13 +08:00
Mrx
1934fb1789 f 2026-02-28 10:52:35 +08:00
69 changed files with 25332 additions and 178 deletions

View File

@@ -6,7 +6,10 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"build:seo": "node scripts/seo-static-generator/run.js",
"build:all": "pnpm run build && pnpm run build:seo",
"config:nginx": "node scripts/seo-static-generator/generateNginxConfig.js"
},
"dependencies": {
"@vant/area-data": "^2.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>天远查代理 - 免费开通代理权限 | 大数据风险报告代理</title>
<meta name="description" content="天远查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。">
<meta name="keywords" content="天远查代理, 免费代理, 大数据风险报告代理, 代理权限, 代理收益">
<meta property="og:title" content="天远查代理 - 免费开通代理权限 | 大数据风险报告代理">
<meta property="og:description" content="天远查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。">
<meta property="og:url" content="https://www.tianyuancha.cn/agent">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="天远查代理 - 免费开通代理权限 | 大数据风险报告代理">
<meta name="twitter:description" content="天远查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/agent">
<link rel="canonical" href="https://www.tianyuancha.cn/agent">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "天远查代理 - 免费开通代理权限 | 大数据风险报告代理",
"description": "天远查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。",
"url": "https://www.tianyuancha.cn/agent",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>天远查代理 - 免费开通代理权限 | 大数据风险报告代理</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/agent">点击这里</a></p>
</div>
<p>天远查代理平台,免费开通代理权限,享受大数据风险报告查询服务代理收益。专业的大数据风险报告、婚姻查询、个人信用评估等服务的代理合作。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>示例报告 - 天远查报告展示 | 大数据风险报告样例</title>
<meta name="description" content="天远查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。">
<meta name="keywords" content="示例报告, 报告展示, 报告样例, 大数据风险报告, 婚姻查询报告">
<meta property="og:title" content="示例报告 - 天远查报告展示 | 大数据风险报告样例">
<meta property="og:description" content="天远查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。">
<meta property="og:url" content="https://www.tianyuancha.cn/example">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="示例报告 - 天远查报告展示 | 大数据风险报告样例">
<meta name="twitter:description" content="天远查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/example">
<link rel="canonical" href="https://www.tianyuancha.cn/example">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "示例报告 - 天远查报告展示 | 大数据风险报告样例",
"description": "天远查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。",
"url": "https://www.tianyuancha.cn/example",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>示例报告 - 天远查报告展示 | 大数据风险报告样例</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/example">点击这里</a></p>
</div>
<p>天远查示例报告展示,包含大数据风险报告、婚姻状况查询、个人信用评估等服务的报告样例,让用户了解报告内容和格式。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>使用指南 - 天远查操作教程 | 功能说明</title>
<meta name="description" content="天远查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。">
<meta name="keywords" content="使用指南, 操作教程, 功能说明, 快速上手, 天远查教程">
<meta property="og:title" content="使用指南 - 天远查操作教程 | 功能说明">
<meta property="og:description" content="天远查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。">
<meta property="og:url" content="https://www.tianyuancha.cn/help/guide">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="使用指南 - 天远查操作教程 | 功能说明">
<meta name="twitter:description" content="天远查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/help/guide">
<link rel="canonical" href="https://www.tianyuancha.cn/help/guide">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "使用指南 - 天远查操作教程 | 功能说明",
"description": "天远查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。",
"url": "https://www.tianyuancha.cn/help/guide",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>使用指南 - 天远查操作教程 | 功能说明</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/help/guide">点击这里</a></p>
</div>
<p>天远查详细使用指南,包含各功能模块的操作教程、功能说明、注意事项等,让用户快速上手使用。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>帮助中心 - 天远查使用指南 | 常见问题解答</title>
<meta name="description" content="天远查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。">
<meta name="keywords" content="天远查帮助, 使用指南, 常见问题, 操作教程, 客服支持">
<meta property="og:title" content="帮助中心 - 天远查使用指南 | 常见问题解答">
<meta property="og:description" content="天远查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。">
<meta property="og:url" content="https://www.tianyuancha.cn/help">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="帮助中心 - 天远查使用指南 | 常见问题解答">
<meta name="twitter:description" content="天远查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/help">
<link rel="canonical" href="https://www.tianyuancha.cn/help">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "帮助中心 - 天远查使用指南 | 常见问题解答",
"description": "天远查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。",
"url": "https://www.tianyuancha.cn/help",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>帮助中心 - 天远查使用指南 | 常见问题解答</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/help">点击这里</a></p>
</div>
<p>天远查帮助中心,提供详细的使用指南、常见问题解答、操作教程等,帮助用户更好地使用大数据风险报告查询服务。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>天远查官网_企业与婚姻关联风险核验_综合履约背景核验</title>
<meta name="description" content="天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。数据实时同步,助您精准规避投资、交易及家庭结合中的经济与法律风险。">
<meta name="keywords" content="天远查,婚姻状态风险, 配偶背景核验,企业信用查询,司法诉讼记录,资产风险评估">
<meta property="og:title" content="天远查官网_企业与婚姻关联风险核验_综合履约背景核验">
<meta property="og:description" content="天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。数据实时同步,助您精准规避投资、交易及家庭结合中的经济与法律风险。">
<meta property="og:url" content="https://www.tianyuancha.cn">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="天远查官网_企业与婚姻关联风险核验_综合履约背景核验">
<meta name="twitter:description" content="天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。数据实时同步,助您精准规避投资、交易及家庭结合中的经济与法律风险。">
<meta name="twitter:url" content="https://www.tianyuancha.cn">
<link rel="canonical" href="https://www.tianyuancha.cn">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "天远查官网_企业与婚姻关联风险核验_综合履约背景核验",
"description": "天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。数据实时同步,助您精准规避投资、交易及家庭结合中的经济与法律风险。",
"url": "https://www.tianyuancha.cn",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>天远查官网_企业与婚姻关联风险核验_综合履约背景核验</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn">点击这里</a></p>
</div>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。数据实时同步,助您精准规避投资、交易及家庭结合中的经济与法律风险。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>司法涉诉核验_个人及企业法律诉讼记录_履约风险评估_天远查</title>
<meta name="description" content="天远查司法风险检测中心,聚合全国法院公开公示数据。一键筛查开庭公告、裁判文书、立案信息及执行记录。帮助用户快速识别法律纠纷隐患,全方位扫除合作盲区。">
<meta name="keywords" content="司法案件核验,法律诉讼记录,个人涉诉详情,法院公告查询,案件执行状态">
<meta property="og:title" content="司法涉诉核验_个人及企业法律诉讼记录_履约风险评估_天远查">
<meta property="og:description" content="天远查司法风险检测中心,聚合全国法院公开公示数据。一键筛查开庭公告、裁判文书、立案信息及执行记录。帮助用户快速识别法律纠纷隐患,全方位扫除合作盲区。">
<meta property="og:url" content="https://www.tianyuancha.cn/inquire/category/lawsuit">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="司法涉诉核验_个人及企业法律诉讼记录_履约风险评估_天远查">
<meta name="twitter:description" content="天远查司法风险检测中心,聚合全国法院公开公示数据。一键筛查开庭公告、裁判文书、立案信息及执行记录。帮助用户快速识别法律纠纷隐患,全方位扫除合作盲区。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/inquire/category/lawsuit">
<link rel="canonical" href="https://www.tianyuancha.cn/inquire/category/lawsuit">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "司法涉诉核验_个人及企业法律诉讼记录_履约风险评估_天远查",
"description": "天远查司法风险检测中心,聚合全国法院公开公示数据。一键筛查开庭公告、裁判文书、立案信息及执行记录。帮助用户快速识别法律纠纷隐患,全方位扫除合作盲区。",
"url": "https://www.tianyuancha.cn/inquire/category/lawsuit",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>司法涉诉核验_个人及企业法律诉讼记录_履约风险评估_天远查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/inquire/category/lawsuit">点击这里</a></p>
</div>
<p>天远查司法风险检测中心,聚合全国法院公开公示数据。一键筛查开庭公告、裁判文书、立案信息及执行记录。帮助用户快速识别法律纠纷隐患,全方位扫除合作盲区。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>个人婚姻关联风险核验_家庭背景合规报告_天远查</title>
<meta name="description" content="天远查提供基于大数据的婚姻关联风险评估。通过分析司法文书及公开社会关系,辅助判断目标的真实家庭状况与情感履历。合法合规,保障知情权。">
<meta name="keywords" content="婚史风险排查,家庭背景核实,婚姻诚信评估,情感状态评估,涉婚法律记录">
<meta property="og:title" content="个人婚姻关联风险核验_家庭背景合规报告_天远查">
<meta property="og:description" content="天远查提供基于大数据的婚姻关联风险评估。通过分析司法文书及公开社会关系,辅助判断目标的真实家庭状况与情感履历。合法合规,保障知情权。">
<meta property="og:url" content="https://www.tianyuancha.cn/inquire/category/marriageStatus">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="个人婚姻关联风险核验_家庭背景合规报告_天远查">
<meta name="twitter:description" content="天远查提供基于大数据的婚姻关联风险评估。通过分析司法文书及公开社会关系,辅助判断目标的真实家庭状况与情感履历。合法合规,保障知情权。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/inquire/category/marriageStatus">
<link rel="canonical" href="https://www.tianyuancha.cn/inquire/category/marriageStatus">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "个人婚姻关联风险核验_家庭背景合规报告_天远查",
"description": "天远查提供基于大数据的婚姻关联风险评估。通过分析司法文书及公开社会关系,辅助判断目标的真实家庭状况与情感履历。合法合规,保障知情权。",
"url": "https://www.tianyuancha.cn/inquire/category/marriageStatus",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>个人婚姻关联风险核验_家庭背景合规报告_天远查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/inquire/category/marriageStatus">点击这里</a></p>
</div>
<p>天远查提供基于大数据的婚姻关联风险评估。通过分析司法文书及公开社会关系,辅助判断目标的真实家庭状况与情感履历。合法合规,保障知情权。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>车辆档案报告_二手车车况与产权风险检测_机动车报告_天远查</title>
<meta name="description" content="天远查车辆数据中心,让车辆交易更透明。支持通过车牌号或VIN码,核验车辆的初次登记信息、抵押查封状态、事故维修记录及产权属性。数据同步权威行业系统,精准识别问题车。">
<meta name="keywords" content="车辆维修记录,二手车出险报告,车辆抵押报告,车况报告,机动车档案">
<meta property="og:title" content="车辆档案报告_二手车车况与产权风险检测_机动车报告_天远查">
<meta property="og:description" content="天远查车辆数据中心,让车辆交易更透明。支持通过车牌号或VIN码,核验车辆的初次登记信息、抵押查封状态、事故维修记录及产权属性。数据同步权威行业系统,精准识别问题车。">
<meta property="og:url" content="https://www.tianyuancha.cn/inquire/category/vehicle">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="车辆档案报告_二手车车况与产权风险检测_机动车报告_天远查">
<meta name="twitter:description" content="天远查车辆数据中心,让车辆交易更透明。支持通过车牌号或VIN码,核验车辆的初次登记信息、抵押查封状态、事故维修记录及产权属性。数据同步权威行业系统,精准识别问题车。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/inquire/category/vehicle">
<link rel="canonical" href="https://www.tianyuancha.cn/inquire/category/vehicle">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "车辆档案报告_二手车车况与产权风险检测_机动车报告_天远查",
"description": "天远查车辆数据中心,让车辆交易更透明。支持通过车牌号或VIN码,核验车辆的初次登记信息、抵押查封状态、事故维修记录及产权属性。数据同步权威行业系统,精准识别问题车。",
"url": "https://www.tianyuancha.cn/inquire/category/vehicle",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>车辆档案报告_二手车车况与产权风险检测_机动车报告_天远查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/inquire/category/vehicle">点击这里</a></p>
</div>
<p>天远查车辆数据中心,让车辆交易更透明。支持通过车牌号或VIN码,核验车辆的初次登记信息、抵押查封状态、事故维修记录及产权属性。数据同步权威行业系统,精准识别问题车。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>婚前背景核验_婚姻关联司法风险筛查_情感综合保障_天远查</title>
<meta name="description" content="天远查婚恋风险报告为您提供深度的背景核实服务。基于合法公开数据,排查对象的重婚司法记录、家庭暴力涉诉历史、潜在债务风险及不良嗜好风险。拒绝盲目信任,用数据守护您的情感与财产安全。">
<meta name="keywords" content="婚前背景核实,婚恋对象评估,婚姻司法风险,个人情感风险,婚前背调工具">
<meta property="og:title" content="婚前背景核验_婚姻关联司法风险筛查_情感综合保障_天远查">
<meta property="og:description" content="天远查婚恋风险报告为您提供深度的背景核实服务。基于合法公开数据,排查对象的重婚司法记录、家庭暴力涉诉历史、潜在债务风险及不良嗜好风险。拒绝盲目信任,用数据守护您的情感与财产安全。">
<meta property="og:url" content="https://www.tianyuancha.cn/inquire/marriage">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="婚前背景核验_婚姻关联司法风险筛查_情感综合保障_天远查">
<meta name="twitter:description" content="天远查婚恋风险报告为您提供深度的背景核实服务。基于合法公开数据,排查对象的重婚司法记录、家庭暴力涉诉历史、潜在债务风险及不良嗜好风险。拒绝盲目信任,用数据守护您的情感与财产安全。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/inquire/marriage">
<link rel="canonical" href="https://www.tianyuancha.cn/inquire/marriage">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "婚前背景核验_婚姻关联司法风险筛查_情感综合保障_天远查",
"description": "天远查婚恋风险报告为您提供深度的背景核实服务。基于合法公开数据,排查对象的重婚司法记录、家庭暴力涉诉历史、潜在债务风险及不良嗜好风险。拒绝盲目信任,用数据守护您的情感与财产安全。",
"url": "https://www.tianyuancha.cn/inquire/marriage",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>婚前背景核验_婚姻关联司法风险筛查_情感综合保障_天远查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/inquire/marriage">点击这里</a></p>
</div>
<p>天远查婚恋风险报告为您提供深度的背景核实服务。基于合法公开数据,排查对象的重婚司法记录、家庭暴力涉诉历史、潜在债务风险及不良嗜好风险。拒绝盲目信任,用数据守护您的情感与财产安全。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>核验工具多场景数据核验服务天远查</title>
<meta name="description" content="提供车辆、企业、个人等多场景核验,包括状态、信用、身份等查询,权威高效,保护隐私。">
<meta name="keywords" content="核验工具,数据核验服务,车辆核验,企业核验,天远查">
<meta property="og:title" content="核验工具多场景数据核验服务天远查">
<meta property="og:description" content="提供车辆、企业、个人等多场景核验,包括状态、信用、身份等查询,权威高效,保护隐私。">
<meta property="og:url" content="https://www.tianyuancha.cn/inquire">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="核验工具多场景数据核验服务天远查">
<meta name="twitter:description" content="提供车辆、企业、个人等多场景核验,包括状态、信用、身份等查询,权威高效,保护隐私。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/inquire">
<link rel="canonical" href="https://www.tianyuancha.cn/inquire">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "核验工具多场景数据核验服务天远查",
"description": "提供车辆、企业、个人等多场景核验,包括状态、信用、身份等查询,权威高效,保护隐私。",
"url": "https://www.tianyuancha.cn/inquire",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>核验工具多场景数据核验服务天远查</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/inquire">点击这里</a></p>
</div>
<p>提供车辆、企业、个人等多场景核验,包括状态、信用、身份等查询,权威高效,保护隐私。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>客服中心 - 天远查在线客服 | 技术支持</title>
<meta name="description" content="天远查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。">
<meta name="keywords" content="客服中心, 在线客服, 技术支持, 问题反馈, 天远查客服">
<meta property="og:title" content="客服中心 - 天远查在线客服 | 技术支持">
<meta property="og:description" content="天远查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。">
<meta property="og:url" content="https://www.tianyuancha.cn/service">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="客服中心 - 天远查在线客服 | 技术支持">
<meta name="twitter:description" content="天远查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。">
<meta name="twitter:url" content="https://www.tianyuancha.cn/service">
<link rel="canonical" href="https://www.tianyuancha.cn/service">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "客服中心 - 天远查在线客服 | 技术支持",
"description": "天远查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。",
"url": "https://www.tianyuancha.cn/service",
"mainEntity": {
"@type": "Organization",
"name": "天远查",
"url": "https://www.tianyuancha.cn/",
"description": "专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用"
}
}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>客服中心 - 天远查在线客服 | 技术支持</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="https://www.tianyuancha.cn/service">点击这里</a></p>
</div>
<p>天远查客服中心,提供在线客服支持、技术咨询、问题反馈等服务,确保用户获得及时有效的帮助。</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>

View File

@@ -0,0 +1,310 @@
# ========================================
# 天远查 Nginx 配置 - 带 SEO 爬虫检测
# 生成时间: 2026-02-25
# ========================================
server {
listen 80;
listen 443 ssl http2;
server_name www.tianyuancha.cn tianyuancha.cn;
# 网站根目录SPA
root /www/sites/www.tianyuancha.cn/index;
# SEO 静态页面目录
set $static_root /www/sites/www.tianyuancha.cn/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.tianyuancha.cn/log/access.log main;
error_log /www/sites/www.tianyuancha.cn/log/error.log;
# ========================================
# SSL 证书配置
# ========================================
ssl_certificate /www/sites/www.tianyuancha.cn/ssl/fullchain.pem;
ssl_certificate_key /www/sites/www.tianyuancha.cn/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";
# ========================================
# Let's Encrypt 验证
# ========================================
location ^~ /.well-known/acme-challenge {
allow all;
root /usr/share/nginx/html;
}
# ========================================
# SEO 静态文件sitemap.xml, robots.txt
# ========================================
location = /sitemap.xml {
root /www/sites/www.tianyuancha.cn/static-pages;
default_type application/xml;
}
location = /robots.txt {
root /www/sites/www.tianyuancha.cn/static-pages;
default_type text/plain;
}
# ========================================
# 首页处理(爬虫检测)
# ========================================
location = / {
# 爬虫访问静态页面
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /index.html break;
}
# 正常用户访问 SPA
try_files $uri $uri/ /index.html;
}
# ========================================
# SEO 关键页面(爬虫检测)
# ========================================
# 司法涉诉核验页
location = /inquire/category/lawsuit {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-category-lawsuit.html break;
}
try_files $uri $uri/ /index.html;
}
# 婚前背景核验页
location = /inquire/marriage {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-marriage.html break;
}
try_files $uri $uri/ /index.html;
}
# 车辆档案报告页
location = /inquire/category/vehicle {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-category-vehicle.html break;
}
try_files $uri $uri/ /index.html;
}
# 婚姻关联风险核验页
location = /inquire/category/marriageStatus {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-category-marriageStatus.html break;
}
try_files $uri $uri/ /index.html;
}
# 代理中心页
location = /agent {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /agent.html break;
}
try_files $uri $uri/ /index.html;
}
# 帮助中心页
location = /help {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /help.html break;
}
try_files $uri $uri/ /index.html;
}
# 示例报告页
location = /example {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /example.html break;
}
try_files $uri $uri/ /index.html;
}
# 客服中心页
location = /service {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /service.html break;
}
try_files $uri $uri/ /index.html;
}
# 核验工具页
location = /inquire {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire.html break;
}
try_files $uri $uri/ /index.html;
}
# ========================================
# API 代理 - 主服务
# ========================================
location /api/v1 {
proxy_pass http://127.0.0.1:21004;
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";
}
# ========================================
# API 代理 - Chat 服务
# ========================================
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;
}
# ========================================
# 错误页面
# ========================================
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;
# ========================================
# 引入重定向配置
# ========================================
include /www/sites/www.tianyuancha.cn/redirect/*.conf;
}

351
server/README-SEO.md Normal file
View File

@@ -0,0 +1,351 @@
# SPA SEO 优化解决方案
## 📋 方案概述
针对 SPA 应用 SEO 问题,采用**爬虫检测 + 静态 HTML 回退**方案:
1. **爬虫检测**识别搜索引擎爬虫百度、Google、必应、搜狗等
2. **静态 HTML**:为爬虫提供预渲染的 HTML 模板,包含完整 TDK、OG、canonical、结构化数据
3. **正常用户**:继续使用 SPA体验不受影响
**配置统一**:服务端 SEO 模板内容与前端 `src/composables/useSEO.js` 保持一致(标题、描述、关键词、域名),域名默认为 `https://www.tianyuancha.cn`(天远查)。可通过环境变量 `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.tianyuandb.com.conf # 天远数据 Nginx 配置tianyuandb.com
├── nginx-www.xingfucha.cn.conf # 幸福查 Nginx 配置示例
├── nginx-seo-location-替换片段.conf # Nginx SEO 片段
├── test-seo.js # SEO 端到端检测脚本
└── README-SEO.md # 本文档
public/
└── seo-templates/ # SEO 静态模板目录(运行 generate 后生成)
├── index.html
├── inquire.html
├── inquire-category-lawsuit.html
├── inquire-category-vehicle.html
├── inquire-category-marriageStatus.html
├── inquire-marriage.html
├── agent.html
├── help.html
├── help-guide.html
├── example.html
├── service.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.tianyuandb.com.conf`,将 `root``server_name`、证书路径改为你的服务器路径。
- 幸福查站点:参考 `server/nginx-www.xingfucha.cn.conf`
- 部署时把 `public/seo-templates/` 整目录上传到服务器 `root` 下的 `seo-templates/`
```bash
# 复制并修改配置
cp server/nginx-www.tianyuandb.com.conf /etc/nginx/sites-available/zhinengcha
nano /etc/nginx/sites-available/zhinengcha # 修改 root、证书等
# 启用并重载
ln -s /etc/nginx/sites-available/zhinengcha /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 `<!DOCTYPE html>
<html>
<head>
<!-- 头部信息 -->
<style>
/* 自定义样式 */
body { ... }
</style>
</head>
<body>
<!-- 自定义内容 -->
</body>
</html>`
}
```
### 添加结构化数据
模板已包含基本的结构化数据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 '<title>.*</title>'
# 检查 meta 与 canonical
curl -s -A "Googlebot" https://www.tianyuandb.com/ | grep -E '<meta name="description"|<meta name="keywords"|<link rel="canonical"'
# 检查结构化数据
curl -s -A "Baiduspider" https://www.tianyuandb.com/ | grep -A 20 'application/ld+json'
```
## 📊 维护建议
### 定期更新爬虫列表
搜索引擎爬虫的User-Agent可能会更新建议定期检查并更新
- 百度https://ziyuan.baidu.com/spider/
- Googlehttps://support.google.com/webmasters/answer/1061943
### 保持模板内容同步
`useSEO.js` 中页面 SEO 更新时,重新生成模板并部署:
```bash
cd server
node generate-seo-templates.cjs
# 将 public/seo-templates/ 上传到服务器对应目录
```
可选定时任务(例如每小时同步一次):
```bash
0 * * * * cd /path/to/tydata-webview-v2/server && node generate-seo-templates.cjs
```
### 监控爬虫访问
在服务器日志中监控爬虫访问情况:
```bash
# 查看百度爬虫访问
grep "Baiduspider" /var/log/nginx/access.log
# 查看Google爬虫访问
grep "Googlebot" /var/log/nginx/access.log
```
## 🐛 常见问题
### Q1: 爬虫访问的是SPA还是静态HTML
检查响应头:
```bash
curl -I -A "Baiduspider" http://www.xingfucha.cn/
```
若看到 `X-SEOMiddleware: prerendered`Node 中间件)或 `X-SEOMiddleware: nginx-prerendered`Nginx说明返回的是 SEO 静态 HTML。
### Q2: 如何调试爬虫检测?
启用调试模式:
```javascript
const seoMiddleware = new SEOMiddleware({
templateDir: 'public/seo-templates',
debug: true // 查看详细日志
})
```
### Q3: 模板内容如何更新?
`src/composables/useSEO.js` 为准修改标题/描述/关键词,再在 `server/generate-seo-templates.cjs` 中同步 `pageSEOConfigs`,然后执行:
```bash
node generate-seo-templates.cjs
```
### Q4: 如何处理动态内容?
静态模板只包含静态内容。如果需要包含动态数据,可以考虑:
1. **预渲染服务**:如 Prerender.io
2. **服务端渲染**:迁移到 Nuxt.js
3. **混合方案**:静态模板 + AJAX加载动态内容
### Q5: 爬虫列表是否会误判?
如果某个用户被误判为爬虫,可以:
1. 检查User-Agent是否包含爬虫关键词
2. 在检测器中添加白名单
3. 使用IP地址作为辅助判断
## 📚 参考资源
- [百度搜索蜘蛛协议](https://ziyuan.baidu.com/spider/)
- [Google 爬虫文档](https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers)
- [结构化数据规范](https://schema.org/)
- [Open Graph 协议](https://ogp.me/)
## 🆘 支持
如有问题,请检查:
1. 模板目录是否存在:`public/seo-templates/`
2. 模板文件是否生成:运行 `node generate-seo-templates.js`
3. 服务器配置是否正确检查中间件或Nginx配置
4. 爬虫User-Agent是否匹配查看检测器日志
## 📄 许可
本方案可自由使用和修改。

170
server/crawler-detector.js Normal file
View File

@@ -0,0 +1,170 @@
/**
* 爬虫检测模块
* 用于识别搜索引擎爬虫和社交媒体爬虫
*/
class CrawlerDetector {
constructor() {
// 常见搜索引擎爬虫User-Agent列表
this.crawlerPatterns = [
// 百度爬虫
'baiduspider',
'baiduspider-mobile',
'baiduspider-image',
'baiduspider-video',
'baiduspider-news',
'baiduboxapp',
// Google爬虫
'googlebot',
'googlebot-image',
'googlebot-news',
'googlebot-mobile',
'googlebot-video',
'google-web-snippet',
// 360搜索
'360spider',
'soha-agent',
'haosouspider',
// 搜狗搜索
'sogou spider',
'sogou news spider',
'sogou orion spider',
'sogou-blog',
// 必应
'bingbot',
'msnbot',
// 雅虎
'slurp',
// 搜搜
'sosospider',
'sosoimagespider',
// 有道
'youdaobot',
'yodaobot',
// 头条搜索
'bytedance-spider',
'toutiaospider',
// 社交媒体爬虫
'facebookexternalhit',
'facebookcatalog',
'twitterbot',
'linkedinbot',
'whatsapp',
'telegrambot',
'viber',
'line',
// 其他常见爬虫
'applebot',
'semrushbot',
'ahrefsbot',
'mj12bot',
'dotbot',
'crawler',
'spider',
'bot'
]
// 需要检测的头部字段
this.crawlerHeaders = ['x-bot', 'x-crawler', 'x-forwarded-for']
}
/**
* 检测请求是否来自爬虫
* @param {Object} req - HTTP请求对象
* @returns {Boolean} 是否为爬虫
*/
isCrawler(req) {
const userAgent = req.headers['user-agent']?.toLowerCase() || ''
const headers = req.headers
// 1. 通过User-Agent检测
if (this.checkUserAgent(userAgent)) {
console.log(`[CrawlerDetector] 检测到爬虫 UA: ${userAgent}`)
return true
}
// 2. 通过特定头部检测
if (this.checkHeaders(headers)) {
console.log(`[CrawlerDetector] 检测到爬虫 Headers`)
return true
}
// 3. 通过IP地址检测可选
// if (this.checkIP(req.connection.remoteAddress)) {
// return true
// }
return false
}
/**
* 检查User-Agent
* @param {String} userAgent
* @returns {Boolean}
*/
checkUserAgent(userAgent) {
if (!userAgent) return false
return this.crawlerPatterns.some(pattern => {
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

View File

@@ -0,0 +1,220 @@
/**
* SEO模板生成器
* 根据 useSEO.js 的页面配置自动生成静态 HTML 模板,供爬虫访问时返回
* 配置与 src/composables/useSEO.js 保持一致
*
* 多站点:通过环境变量 SEO_BASE_URL 指定 canonical/og:url 域名后生成
* 例SEO_BASE_URL=https://www.tianyuandb.com node generate-seo-templates.cjs
*/
const fs = require('fs')
const path = require('path')
const BASE_URL = process.env.SEO_BASE_URL || 'https://www.tianyuancha.cn'
// 页面 SEO 配置(与 src/composables/useSEO.js 的 routeConfigs 保持一致)
const pageSEOConfigs = {
'index.html': {
title: '天远查官网_企业与婚姻关联风险核验_综合履约背景核验',
description: '天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。数据实时同步,助您精准规避投资、交易及家庭结合中的经济与法律风险。',
keywords: '天远查,婚姻状态风险, 配偶背景核验,企业信用查询,司法诉讼记录,资产风险评估',
url: BASE_URL
},
'inquire-category-lawsuit.html': {
title: '司法涉诉核验_个人及企业法律诉讼记录_履约风险评估_天远查',
description: '天远查司法风险检测中心,聚合全国法院公开公示数据。一键筛查开庭公告、裁判文书、立案信息及执行记录。帮助用户快速识别法律纠纷隐患,全方位扫除合作盲区。',
keywords: '司法案件核验,法律诉讼记录,个人涉诉详情,法院公告查询,案件执行状态',
url: `${BASE_URL}/inquire/category/lawsuit`
},
'inquire-marriage.html': {
title: '婚前背景核验_婚姻关联司法风险筛查_情感综合保障_天远查',
description: '天远查婚恋风险报告为您提供深度的背景核实服务。基于合法公开数据,排查对象的重婚司法记录、家庭暴力涉诉历史、潜在债务风险及不良嗜好风险。拒绝盲目信任,用数据守护您的情感与财产安全。',
keywords: '婚前背景核实,婚恋对象评估,婚姻司法风险,个人情感风险,婚前背调工具',
url: `${BASE_URL}/inquire/marriage`
},
'inquire-category-vehicle.html': {
title: '车辆档案报告_二手车车况与产权风险检测_机动车报告_天远查',
description: '天远查车辆数据中心,让车辆交易更透明。支持通过车牌号或VIN码,核验车辆的初次登记信息、抵押查封状态、事故维修记录及产权属性。数据同步权威行业系统,精准识别问题车。',
keywords: '车辆维修记录,二手车出险报告,车辆抵押报告,车况报告,机动车档案',
url: `${BASE_URL}/inquire/category/vehicle`
},
'inquire-category-marriageStatus.html': {
title: '个人婚姻关联风险核验_家庭背景合规报告_天远查',
description: '天远查提供基于大数据的婚姻关联风险评估。通过分析司法文书及公开社会关系,辅助判断目标的真实家庭状况与情感履历。合法合规,保障知情权。',
keywords: '婚史风险排查,家庭背景核实,婚姻诚信评估,情感状态评估,涉婚法律记录',
url: `${BASE_URL}/inquire/category/marriageStatus`
},
'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`
},
'inquire.html': {
title: '核验工具多场景数据核验服务天远查',
description: '提供车辆、企业、个人等多场景核验,包括状态、信用、身份等查询,权威高效,保护隐私。',
keywords: '核验工具,数据核验服务,车辆核验,企业核验,天远查',
url: `${BASE_URL}/inquire`
}
}
/**
* 规范化文案:统一为中文标点,避免乱码
*/
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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
/**
* 生成单页 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.tianyuancha.cn/',
description: '专业大数据风险报告查询与代理平台,支持个人和企业多场景风控应用'
}
}
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>${escapeAttr(title)}</title>
<meta name="description" content="${escapeAttr(description)}">
<meta name="keywords" content="${escapeAttr(keywords)}">
<meta property="og:title" content="${escapeAttr(title)}">
<meta property="og:description" content="${escapeAttr(description)}">
<meta property="og:url" content="${escapeAttr(config.url)}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="天远查">
<meta property="og:locale" content="zh_CN">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="${escapeAttr(title)}">
<meta name="twitter:description" content="${escapeAttr(description)}">
<meta name="twitter:url" content="${escapeAttr(config.url)}">
<link rel="canonical" href="${escapeAttr(config.url)}">
<script type="application/ld+json">
${JSON.stringify(structuredData, null, 8)}
</script>
<meta name="robots" content="index, follow">
<meta name="googlebot" content="index, follow">
<meta name="baiduspider" content="index, follow">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 0; line-height: 1.6; }
.seo-content { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #333; }
p { color: #666; }
.redirect-notice { background: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 10px; margin: 20px 0; border-radius: 4px; }
</style>
</head>
<body>
<div class="seo-content">
<h1>${escapeAttr(title)}</h1>
<div class="redirect-notice">
<p>正在跳转到完整版网站...</p>
<p>如果浏览器没有自动跳转,请 <a href="${escapeAttr(config.url)}">点击这里</a></p>
</div>
<p>${escapeAttr(description)}</p>
<section>
<h2>关于天远查</h2>
<p>天远查官网(TianYuanCha)聚合官方公示数据,专注于商业安全与资产背调。提供企业工商画像、婚姻状态关联风险、司法涉诉筛查及配偶债务核验。</p>
</section>
<section>
<h2>核心服务</h2>
<ul>
<li>司法涉诉核验与法律诉讼记录</li>
<li>婚前背景核验与婚姻关联风险筛查</li>
<li>车辆档案报告与二手车车况检测</li>
<li>个人婚姻关联风险核验</li>
<li>多场景数据核验服务</li>
</ul>
</section>
</div>
</body>
</html>`
}
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()

175
server/middleware.js Normal file
View File

@@ -0,0 +1,175 @@
/**
* 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',
'/inquire/category/lawsuit': 'inquire-category-lawsuit.html',
'/inquire/category/vehicle': 'inquire-category-vehicle.html',
'/inquire/category/marriageStatus': 'inquire-category-marriageStatus.html',
'/inquire/marriage': 'inquire-marriage.html',
'/inquire': 'inquire.html',
'/agent': 'agent.html',
'/help/guide': 'help-guide.html',
'/help': 'help.html',
'/example': 'example.html',
'/service': 'service.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

View File

@@ -0,0 +1,316 @@
# Nginx 配置 - www.tianyuancha.cn天远查
# 含爬虫检测 + SEO 静态页回退,与 useSEO.js / generate-seo-templates.cjs 路由一致
# 部署时将 public/seo-templates/ 下所有 .html 拷贝到 /www/sites/www.tianyuancha.cn/static-pages/
server {
listen 80;
listen 443 ssl http2;
server_name www.tianyuancha.cn tianyuancha.cn;
# 网站根目录SPA
root /www/sites/www.tianyuancha.cn/index;
# SEO 静态页面目录(与 generate-seo-templates.cjs 输出一致)
set $static_root /www/sites/www.tianyuancha.cn/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.tianyuancha.cn/log/access.log main;
error_log /www/sites/www.tianyuancha.cn/log/error.log;
# ========================================
# SSL 证书配置
# ========================================
ssl_certificate /www/sites/www.tianyuancha.cn/ssl/fullchain.pem;
ssl_certificate_key /www/sites/www.tianyuancha.cn/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";
# ========================================
# Let's Encrypt 验证
# ========================================
location ^~ /.well-known/acme-challenge {
allow all;
root /usr/share/nginx/html;
}
# ========================================
# SEO 静态文件sitemap.xml, robots.txt
# ========================================
location = /sitemap.xml {
root /www/sites/www.tianyuancha.cn/static-pages;
default_type application/xml;
}
location = /robots.txt {
root /www/sites/www.tianyuancha.cn/static-pages;
default_type text/plain;
}
# ========================================
# 首页(爬虫 → 静态页,用户 → SPA
# ========================================
location = / {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /index.html break;
}
try_files $uri $uri/ /index.html;
}
# ========================================
# SEO 关键页面(与 useSEO.js / middleware 一致;子路径在前)
# ========================================
# 司法涉诉核验
location = /inquire/category/lawsuit {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-category-lawsuit.html break;
}
try_files $uri $uri/ /index.html;
}
# 车辆档案报告
location = /inquire/category/vehicle {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-category-vehicle.html break;
}
try_files $uri $uri/ /index.html;
}
# 婚姻关联风险核验
location = /inquire/category/marriageStatus {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-category-marriageStatus.html break;
}
try_files $uri $uri/ /index.html;
}
# 婚前背景核验
location = /inquire/marriage {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire-marriage.html break;
}
try_files $uri $uri/ /index.html;
}
# 核验工具(/inquire 须在 /inquire/xxx 之后,由 location 精确匹配保证)
location = /inquire {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /inquire.html break;
}
try_files $uri $uri/ /index.html;
}
# 代理中心
location = /agent {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /agent.html break;
}
try_files $uri $uri/ /index.html;
}
# 使用指南(子路径须在 /help 之前)
location = /help/guide {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /help-guide.html break;
}
try_files $uri $uri/ /index.html;
}
# 帮助中心
location = /help {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /help.html break;
}
try_files $uri $uri/ /index.html;
}
# 示例报告
location = /example {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /example.html break;
}
try_files $uri $uri/ /index.html;
}
# 客服中心
location = /service {
if ($is_bot = 1) {
root /www/sites/www.tianyuancha.cn/static-pages;
rewrite ^ /service.html break;
}
try_files $uri $uri/ /index.html;
}
# ========================================
# API 代理 - 主服务
# ========================================
location /api/v1 {
proxy_pass http://127.0.0.1:21004;
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";
}
# ========================================
# API 代理 - Chat 服务
# ========================================
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;
}
# ========================================
# 错误页面
# ========================================
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;
# ========================================
# 引入重定向配置
# ========================================
include /www/sites/www.tianyuancha.cn/redirect/*.conf;
}

View File

@@ -0,0 +1,141 @@
# 天远数据 - www.tianyuandb.com 完整配置(含 SEO 爬虫检测)
# 使用前请将 public/seo-templates 上传到服务器:/www/sites/www.tianyuandb.com/index/seo-templates/
# 生成 tianyuandb 用模板SEO_BASE_URL=https://www.tianyuandb.com node generate-seo-templates.cjs
server {
listen 80;
listen 443 ssl http2;
server_name www.tianyuandb.com tianyuandb.com;
index index.php index.html index.htm default.php default.htm default.html;
root /www/sites/www.tianyuandb.com/index;
# ========== SEO 爬虫检测与模板映射(请勿删除) ==========
set $is_crawler 0;
if ($http_user_agent ~* "baiduspider|baiduspider-mobile|baiduspider-image|baiduspider-video|baiduspider-news|baiduboxapp") {
set $is_crawler 1;
}
if ($http_user_agent ~* "googlebot|googlebot-image|googlebot-news|googlebot-mobile|googlebot-video|google-web-snippet") {
set $is_crawler 1;
}
if ($http_user_agent ~* "bingbot|msnbot") {
set $is_crawler 1;
}
if ($http_user_agent ~* "360spider|soha-agent|haosouspider") {
set $is_crawler 1;
}
if ($http_user_agent ~* "sogou spider|sogou news spider|sogou orion spider|sogou-blog") {
set $is_crawler 1;
}
if ($http_user_agent ~* "slurp|sosospider|sosoimagespider|youdaobot|yodaobot") {
set $is_crawler 1;
}
if ($http_user_agent ~* "bytedance-spider|toutiaospider") {
set $is_crawler 1;
}
if ($http_user_agent ~* "facebookexternalhit|facebookcatalog|twitterbot|linkedinbot|whatsapp|telegrambot|viber|line") {
set $is_crawler 1;
}
set $seo_file index.html;
if ($uri = '/') { set $seo_file index.html; }
if ($uri = '/historyQuery') { set $seo_file historyQuery.html; }
if ($uri = '/agent') { set $seo_file agent.html; }
if ($uri = '/agent/promote') { set $seo_file agent-promote.html; }
if ($uri = '/agent/invitation') { set $seo_file agent-invitation.html; }
if ($uri = '/help') { set $seo_file help.html; }
if ($uri = '/help/guide') { set $seo_file help-guide.html; }
if ($uri = '/example') { set $seo_file example.html; }
if ($uri = '/service') { set $seo_file service.html; }
if ($uri = '/inquire/personalData') { set $seo_file inquire-personalData.html; }
if ($uri = '/inquire/companyinfo') { set $seo_file inquire-companyinfo.html; }
if ($uri = '/inquire/preloanbackgroundcheck') { set $seo_file inquire-preloanbackgroundcheck.html; }
if ($uri = '/inquire/marriage') { set $seo_file inquire-marriage.html; }
if ($uri = '/inquire/backgroundcheck') { set $seo_file inquire-backgroundcheck.html; }
if ($uri = '/inquire/homeservice') { set $seo_file inquire-homeservice.html; }
# ========== SEO 配置结束 ==========
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.tianyuandb.com/log/access.log main;
error_log /www/sites/www.tianyuandb.com/log/error.log;
location ^~ /.well-known/acme-challenge {
allow all;
root /usr/share/nginx/html;
}
# SEO爬虫访问时返回静态 HTML 模板
location /seo-templates/ {
internal;
add_header Content-Type "text/html; charset=utf-8";
add_header X-SEOMiddleware "nginx-prerendered";
}
location / {
if ($is_crawler = 1) {
rewrite ^ /seo-templates/$seo_file break;
}
try_files $uri $uri/ /index.html;
}
location /api/v1 {
proxy_pass http://127.0.0.1:21004;
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/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;
}
error_page 404 /404.html;
add_header Strict-Transport-Security "max-age=31536000";
include /www/sites/www.tianyuandb.com/redirect/*.conf;
if ($scheme = http) {
return 301 https://$host$request_uri;
}
ssl_certificate /www/sites/www.tianyuandb.com/ssl/fullchain.pem;
ssl_certificate_key /www/sites/www.tianyuandb.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;
}

27
server/package.json Normal file
View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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)

178
server/test-seo.js Normal file
View File

@@ -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[^>]*>([^<]+)<\/title>/i)
return match ? match[1].trim() : null
}
function hasMetaDescription(html) {
return /<meta\s+name=["']description["']\s+content=["']/i.test(html)
}
function isSEOTemplate(html) {
return (
/<meta\s+name=["']description["']/i.test(html) &&
/<meta\s+name=["']keywords["']/i.test(html) &&
/<link\s+rel=["']canonical["']/i.test(html)
)
}
async function runTest(route, titleKeyword) {
const url = BASE_URL + route
const results = { route, crawler: null, normal: null }
// 1. 爬虫请求
try {
const crawlerRes = await request(url, 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)')
const title = extractTitle(crawlerRes.body)
const seoHeader = crawlerRes.headers['x-seomiddleware']
results.crawler = {
status: crawlerRes.statusCode,
title,
hasSEOMeta: hasMetaDescription(crawlerRes.body),
isSEOTemplate: isSEOTemplate(crawlerRes.body),
seoHeader: seoHeader || '(无)'
}
if (crawlerRes.statusCode !== 200) {
results.crawler.error = `HTTP ${crawlerRes.statusCode}`
} else if (!title || !title.includes(titleKeyword)) {
results.crawler.error = `标题不匹配,期望含「${titleKeyword}」,实际: ${title || '未找到'}`
} else if (!results.crawler.isSEOTemplate) {
results.crawler.error = '响应中缺少完整 SEO 标签description/keywords/canonical'
}
} catch (e) {
results.crawler = { error: e.message || String(e) }
}
// 2. 普通用户请求(仅验证能正常返回)
try {
const normalRes = await request(url, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
results.normal = {
status: normalRes.statusCode,
bodyLength: normalRes.body.length
}
if (normalRes.statusCode !== 200) {
results.normal.error = `HTTP ${normalRes.statusCode}`
}
} catch (e) {
results.normal = { error: e.message || String(e) }
}
return results
}
function printResult(results) {
const { route, crawler, normal } = results
console.log(`\n📍 路由: ${route}`)
console.log('─'.repeat(60))
const crawlerOk = crawler && !crawler.error && crawler.status === 200 && crawler.isSEOTemplate
if (crawlerOk) {
console.log(' 爬虫请求: ✓ 通过')
console.log(` 标题: ${crawler.title}`)
console.log(` 响应头: X-SEOMiddleware = ${crawler.seoHeader}`)
} else {
console.log(' 爬虫请求: ✗ 未通过')
if (crawler && crawler.error) console.log(` 原因: ${crawler.error}`)
else if (crawler) console.log(` 状态: ${crawler.status}, 标题: ${crawler.title || '无'}`)
else console.log(' 请求失败')
}
const normalOk = normal && !normal.error && normal.status === 200
if (normalOk) {
console.log(' 普通用户: ✓ 正常 (SPA)')
} else {
console.log(' 普通用户: ✗ 异常')
if (normal && normal.error) console.log(` 原因: ${normal.error}`)
}
}
async function main() {
console.log('='.repeat(60))
console.log('SEO 端到端检测')
console.log('='.repeat(60))
console.log(`目标地址: ${BASE_URL}`)
console.log('若服务器未启动,请先执行: npm run start')
console.log('')
let allPass = true
for (const r of ROUTES) {
try {
const results = await runTest(r.path, r.titleKeyword)
printResult(results)
const crawlerOk = results.crawler && !results.crawler.error && results.crawler.isSEOTemplate
const normalOk = results.normal && !results.normal.error && results.normal.status === 200
if (!crawlerOk || !normalOk) allPass = false
} catch (e) {
console.log(`\n📍 路由: ${r.path}`)
console.log(' 错误:', e.message)
allPass = false
}
}
console.log('\n' + '='.repeat(60))
if (allPass) {
console.log('✓ 全部检测通过:爬虫获得 SEO 模板,普通用户获得 SPA')
} else {
console.log('✗ 部分检测未通过,请检查服务器与模板配置')
}
console.log('='.repeat(60))
process.exit(allPass ? 0 : 1)
}
main().catch(err => {
console.error('检测失败:', err)
process.exit(1)
})

211
server/如何检测.md Normal file
View File

@@ -0,0 +1,211 @@
# SEO 检测指南
## 一、本地快速检测
### 1. 只测爬虫识别逻辑(不启动服务)
```bash
cd server
node test-crawler-detection.js
```
会跑多组 User-Agent看是否把百度/Google 等识别为爬虫、普通浏览器识别为非爬虫。
### 2. 端到端检测(需先启动服务)
**步骤 1启动 SEO 服务器**
```bash
cd server
npm install
npm run start
```
**步骤 2另开一个终端运行检测**
```bash
cd server
npm run test
```
或指定地址:
```bash
# Windows PowerShell
$env:SEO_TEST_URL="http://localhost:3000"; node test-seo.js
# 若部署在线上,可测线上
$env:SEO_TEST_URL="https://www.xingfucha.cn"; node test-seo.js
```
脚本会:
-**百度爬虫 UA** 请求首页、代理页、帮助中心、个人查询等
- 检查响应里是否有:`<title>``<meta name="description">``<meta name="keywords">``<link rel="canonical">`
-**普通浏览器 UA** 再请求一遍,确认仍是 200SPA 正常)
全部通过即说明:爬虫拿到的是 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-8CMD 默认是 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
# 方式 2PowerShell 下指定输出编码
[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 前几行(含 <head>
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"` 或大量 `<script>`,说明是前端 SPA。
### 看响应头(若用了 Node 中间件)
```bash
curl -I -A "Baiduspider/2.0" http://localhost:3000/
```
若返回里有 `X-SEOMiddleware: prerendered`,说明走的是 SEO 中间件、返回的是预渲染 HTML。
---
## 三、用浏览器“伪装”爬虫(可选)
1. 打开 Chrome DevTools (F12) → Network。
2. 右键列表头 → 勾选 “User-Agent”或先在 More tools 里加自定义请求头)。
3. 使用扩展(如 “User-Agent Switcher”把 UA 改成:
- `Baiduspider/2.0`
4. 刷新页面,在 Elements 里看 `<head>`
- 应看到正确的 `<title>``<meta name="description">``<meta name="keywords">` 等。
注意:若前端是 SPA同一 URL 在浏览器里仍可能先加载 SPA 再改 title用 curl 用爬虫 UA 请求,更能代表爬虫真实看到的内容。
---
## 四、线上/生产环境检测
### 1. 用本机脚本测线上
```bash
cd server
$env:SEO_TEST_URL="https://www.xingfucha.cn"; node test-seo.js
```
Linux/Mac 用 `export SEO_TEST_URL=https://www.xingfucha.cn` 再运行 `node test-seo.js`。)
### 2. 用 curl 测线上
```bash
curl -s -A "Baiduspider/2.0" https://www.xingfucha.cn/ | findstr /i "title description"
curl -I -A "Baiduspider/2.0" https://www.xingfucha.cn/
```
### 3. 使用搜索引擎工具
- **百度** [百度搜索资源平台](https://ziyuan.baidu.com/) → 抓取诊断 / 抓取频次,看抓取是否成功。
- **Google** [Google Search Console](https://search.google.com/search-console) → URL 检查,输入首页或内页 URL查看“已编入索引”的页面快照。
---
## 五、检测结果对照
| 检测项 | 预期结果 |
|------------------|----------|
| 爬虫 UA 请求 | 返回 200HTML 内含完整 title、description、keywords、canonical |
| 爬虫响应头 | 若用 Node 中间件,有 `X-SEOMiddleware: prerendered` |
| 普通浏览器请求 | 返回 200为 SPA 的 index.html#app 或大量 script |
| test-crawler-detection.js | 全部用例通过 |
| test-seo.js | 全部路由“爬虫拿到 SEO 模板 + 普通用户正常” |
若某一项不符合,按下面排查:
- **爬虫也拿到 SPA**:检查 Nginx/Node 里爬虫判断和 SEO 模板路径、路由映射是否正确。
- **普通用户报错或空白**:检查 SPA 静态资源和 fallback 的 index.html 是否正常。
- **标题/描述不对**:检查 `public/seo-templates/` 下对应模板是否已重新生成(`node generate-seo-templates.cjs`)。
---
## 六、爬虫请求返回 404 时排查
当执行 `curl -s -A "Baiduspider/2.0" https://www.xingfucha.cn/` 得到 **404 Not Found** 时,按下面顺序在**服务器上**检查。
### 1. 确认已接入 SEO 配置
- 若你**还没**在宝塔里替换/合并过 `server/nginx-www.xingfucha.cn.conf` 里的 SEO 相关配置,爬虫仍会走原来的 `location /`,不会返回 SEO 模板。
- **操作**:在宝塔 → 网站 → www.xingfucha.cn → 配置文件,确认存在:
- 上面的 `set $is_crawler``set $seo_file` 以及一堆 `if ($http_user_agent ...)``if ($uri = '/') ...`
- `location ~ ^/__seo__/(.+)$ { ... internal; }`
-`location /` 里有 `if ($is_crawler = 1) { rewrite ^ /__seo__/$seo_file break; }`
- 修改后执行 `nginx -t`,再重载 Nginx。
### 2. 确认 SEO 模板目录和文件存在
404 常见原因是 Nginx 去读 SEO 模板时路径不对或文件不存在。
在**服务器**上执行(路径按你实际部署的来,一般为宝塔默认):
```bash
# Linux 服务器上
ls -la /www/wwwroot/www.xingfucha.cn/seo-templates/
cat /www/wwwroot/www.xingfucha.cn/seo-templates/index.html | head -5
```
-`seo-templates` 不存在或没有 `index.html`,需要把本地的 `public/seo-templates/` 整个目录上传到服务器的 `/www/wwwroot/www.xingfucha.cn/seo-templates/`(与配置里的 `alias` 路径一致)。
- 若目录名或路径和 Nginx 里 `alias` 不一致(例如多写了 `public` 或少了一层目录),改成与配置一致或改配置里的 `alias`
### 3. 看 Nginx 错误日志
在服务器上:
```bash
tail -20 /www/wwwlogs/www.xingfucha.cn.error.log
```
再发一次爬虫请求,看是否有 `open() ".../seo-templates/xxx.html" failed (2: No such file or directory)` 之类,根据路径修正目录或配置。
### 4. 快速对照
| 现象 | 可能原因 | 处理 |
|------|----------|------|
| 爬虫返回 404 | 未接入 SEO 配置 | 按 1 合并/替换 Nginx 配置并重载 |
| 爬虫返回 404 | 模板目录或文件不存在 | 按 2 上传/创建 `seo-templates` 并保证有 `index.html` |
| 爬虫返回 404 | alias 路径错误 | 对照 error.log 修正 `alias` 或目录结构 |
| 爬虫返回 200 但仍是 SPA | 爬虫未命中 SEO 逻辑 | 检查 `$is_crawler``$seo_file``rewrite` 是否生效 |
按上述步骤即可完成从本地到线上的 SEO 检测。

View File

@@ -0,0 +1,56 @@
# 宝塔环境 Nginx SEO 配置说明
## 1. 已生成的配置文件
- **`server/nginx-www.tianyuandb.com.conf`**天远数据站点tianyuandb.com完整 server 配置,含 SEO 爬虫检测与静态模板,与 **`src/composables/useSEO.js`** 路由一致。
- **`server/nginx-www.xingfucha.cn.conf`**:幸福查站点示例,结构相同,含 `historyQuery``/agent/promote``/agent/invitation` 等路由。
按实际域名二选一或复制后改 `server_name``root`、证书路径。
## 2. 部署前准备:上传 SEO 模板
在服务器上要有 SEO 静态 HTML 目录,路径与站点 `root` 一致,例如:
- 天远数据:`/www/wwwroot/www.tianyuandb.com/seo-templates/`
- 幸福查:`/www/wwwroot/www.xingfucha.cn/seo-templates/`
操作步骤:
1. 本地生成模板(与 useSEO.js 同步):
```bash
cd server
npm run generate
```
2. 将本地的 **`tydata-webview-v2/public/seo-templates/`** 整个目录上传到服务器对应站点的 `seo-templates/` 目录。
目录下应有:`index.html`、`historyQuery.html`、`agent.html`、`agent-promote.html`、`agent-invitation.html`、`help.html`、`help-guide.html`、`example.html`、`service.html`、`inquire-*.html` 等。
## 3. 在宝塔里修改 Nginx 配置
1. 登录宝塔 → **网站** → 找到对应站点(如 www.tianyuandb.com→ **设置** → **配置文件**。
2. 应用 SEO
- **方式 A**:打开 **`server/nginx-www.tianyuandb.com.conf`**(或 xingfucha 版),复制从 `# ========== SEO 爬虫检测` 到 `# ========== SEO 配置结束` 的整段,以及 **`location /seo-templates/`** 和 **`location /`** 两块,合并进当前 server保留 SSL、include、api、日志等
- **方式 B**:用示例的整个 server 块替换当前站点 server 块(先备份),并修改 `server_name`、`root`、证书路径。
3. 保存后 **重载 Nginx**`nginx -t && nginx -s reload`。
## 4. 配置要点说明
- **爬虫判断**:按 `User-Agent` 识别百度、Google、必应、360、搜狗、头条、社交媒体等爬虫爬虫访问时走 SEO 逻辑。
- **路由与模板**`$uri` 与 `$seo_file` 对应关系已在配置中写好(如 `/`→index.html`/historyQuery`→historyQuery.html`/agent/promote`→agent-promote.html 等),与 `public/seo-templates` 下文件名一致。
- **内部 location**`/seo-templates/` 为 internal仅由 `location /` 内 rewrite 使用,爬虫看到的仍是正常 URL内容来自 `seo-templates` 下的静态 HTML。
## 5. 验证
将下面域名换成你的站点后执行:
```bash
# 天远数据示例
curl -sI -A "Baiduspider/2.0" https://www.tianyuandb.com/
curl -s -A "Baiduspider/2.0" https://www.tianyuandb.com/ | grep -o '<title>.*</title>'
```
若看到正确标题和响应头 `X-SEOMiddleware: nginx-prerendered`,说明 SEO 已生效。
**若得到 404**:按 **`server/如何检测.md`** 中「爬虫请求返回 404 时排查」检查:是否已接入 SEO 配置、`seo-templates` 目录与文件是否存在、error 日志。
更多检测:**`server/如何检测.md`**、**`npm run test`**(需先启动本地服务)。

View File

@@ -0,0 +1,50 @@
# 天远数据 tianyuandb.com SEO 部署说明
## 1. 生成 tianyuandb 用 SEO 模板
在项目根目录或 `server` 目录执行(使用当前站点域名生成 canonical/og:url
```bash
cd server
SEO_BASE_URL=https://www.tianyuandb.com node generate-seo-templates.cjs
```
会在 `public/seo-templates/` 下生成 15 个 HTML 文件,内容里的链接均为 `https://www.tianyuandb.com`
## 2. 上传模板到服务器
**`tydata-webview-v2/public/seo-templates/`** 整个目录上传到服务器,目标路径:
```text
/www/sites/www.tianyuandb.com/index/seo-templates/
```
即与站点 `root` 同级,保证存在:
`/www/sites/www.tianyuandb.com/index/seo-templates/index.html`
`historyQuery.html``agent.html``agent-promote.html` 等。
## 3. 使用 Nginx 配置
- 配置文件:**`server/nginx-www.tianyuandb.com.conf`**
- 复制该文件内容到线上 Nginx 对应站点的 server 配置(或直接替换该站点的 server 块,注意先备份)。
- 确认以下路径与你的环境一致,按需修改:
- `root /www/sites/www.tianyuandb.com/index`
- `ssl_certificate` / `ssl_certificate_key`
- `include /www/sites/www.tianyuandb.com/redirect/*.conf`
- `proxy_pass http://127.0.0.1:21004`API 端口)
重载 Nginx
```bash
nginx -t && nginx -s reload
```
## 4. 验证
```bash
# 应返回带 title/description 的 HTML且响应头含 X-SEOMiddleware
curl -sI -A "Baiduspider/2.0" https://www.tianyuandb.com/
curl -s -A "Baiduspider/2.0" https://www.tianyuandb.com/ | grep -o '<title>.*</title>'
```
看到正确标题和 `X-SEOMiddleware: nginx-prerendered` 即表示 SEO 已生效。

View File

@@ -1,5 +1,6 @@
<script setup>
import { RouterLink, RouterView, useRouter } from "vue-router";
import { computed } from "vue";
import { RouterLink, RouterView, useRouter, useRoute } from "vue-router";
const { isWeChat } = useEnv();
import { useAgentStore } from "@/stores/agentStore";
import { useUserStore } from "@/stores/userStore";
@@ -9,12 +10,26 @@ import { useWeixinShare } from "@/composables/useWeixinShare";
import WechatOverlay from "@/components/WechatOverlay.vue";
const router = useRouter();
const route = useRoute();
/** 仅在代理推广报告查询页展示「浏览器打开」遮罩(组件内仍会校验微信环境) */
const showPromotionWechatOverlay = computed(
() => route.name === "promotionInquire"
);
const agentStore = useAgentStore();
const userStore = useUserStore();
const dialogStore = useDialogStore();
const authStore = useAuthStore();
const { configWeixinShare, setDynamicShare } = useWeixinShare();
/** 微信内已登录用户是否仍需走 snsapi_base 换 code以便后端把 openid 写入 user_auth */
const WXH5_OPENID_BOUND_AT = "wxh5_openid_bound_at";
function needWxh5SilentOAuth() {
const t = localStorage.getItem(WXH5_OPENID_BOUND_AT);
if (!t) return true;
const maxAge = 30 * 86400000;
return Date.now() - Number.parseInt(t, 10) > maxAge;
}
onMounted(() => {
// 检查token版本如果版本不匹配则清除旧token
checkTokenVersion()
@@ -79,6 +94,7 @@ const clearAuthData = () => {
localStorage.removeItem('accessExpire')
localStorage.removeItem('userInfo')
localStorage.removeItem('agentInfo')
localStorage.removeItem(WXH5_OPENID_BOUND_AT)
}
const RefreshToken = async () => {
@@ -135,11 +151,8 @@ const refreshToken = async () => {
const h5WeixinLogin = async () => {
return;
// 获取当前URL
const url = new URL(window.location.href);
// 获取参数
const params = new URLSearchParams(url.search);
// 获取特定参数值
const code = params.get("code");
const state = params.get("state");
@@ -183,11 +196,45 @@ const h5WeixinLogin = async () => {
router.replace(authStore.pendingRoute);
authStore.clearPendingRoute();
}
localStorage.setItem(
WXH5_OPENID_BOUND_AT,
String(Date.now())
);
}
}
} else {
// 没有授权参数,需要开始微信授权
// 保存当前路由作为授权完成后的目标路由
// 已有有效会话则不再跳转 OAuth避免每次打开页面都重定向授权
const token = localStorage.getItem("token");
const accessExpire = localStorage.getItem("accessExpire");
const refreshAfter = localStorage.getItem("refreshAfter");
const currentTime = Date.now();
let accessOk = true;
if (accessExpire) {
accessOk =
currentTime <= parseInt(accessExpire, 10) * 1000;
}
if (token && accessOk) {
if (refreshAfter && currentTime < parseInt(refreshAfter, 10) * 1000) {
authStore.completeWeixinAuth();
if (needWxh5SilentOAuth()) {
const r = router.currentRoute.value;
authStore.startWeixinAuth(r);
h5WeixinGetCode();
}
return;
}
await refreshToken();
authStore.completeWeixinAuth();
if (needWxh5SilentOAuth()) {
const r = router.currentRoute.value;
authStore.startWeixinAuth(r);
h5WeixinGetCode();
}
return;
}
const currentRoute = router.currentRoute.value;
authStore.startWeixinAuth(currentRoute);
h5WeixinGetCode();
@@ -196,7 +243,9 @@ const h5WeixinLogin = async () => {
const h5WeixinGetCode = () => {
const currentUrl = window.location.href;
let redirectUri = encodeURIComponent(currentUrl);
let appId = "wxa581992dc74d860e";
// 必须与后端 WechatH5.AppID 一致,否则拿到的 openid 无法用于 JSAPI 下单
const appId =
import.meta.env.VITE_WECHAT_H5_APPID || "wxd391e40295bd9dfb";
let state = "snsapi_base";
let scope = "snsapi_base";
let authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
@@ -208,6 +257,7 @@ const h5WeixinGetCode = () => {
<template>
<RouterView />
<!-- <WechatOverlay v-if="showPromotionWechatOverlay" /> -->
<WechatOverlay />
<BindPhoneDialog />
</template>

View File

@@ -122,6 +122,9 @@ const router = useRouter();
const show = defineModel("show");
import { useCascaderAreaData } from "@vant/area-data";
import { showToast } from "vant"; // 引入 showToast 方法
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const { runWithCaptcha } = useAliyunCaptcha();
const emit = defineEmits(); // 确保 emit 可以正确使用
const props = defineProps({
ancestor: {
@@ -146,7 +149,6 @@ const form = ref({
const showCascader = ref(false);
const cascaderValue = ref("");
const options = useCascaderAreaData();
const loadingSms = ref(false); // 控制验证码按钮的loading状态
const isCountingDown = ref(false);
const isAgreed = ref(false);
const countdown = ref(60);
@@ -158,7 +160,7 @@ const isPhoneNumberValid = computed(() => {
return /^1[3-9]\d{9}$/.test(form.value.mobile);
});
const getSmsCode = async () => {
const getSmsCode = () => {
if (!form.value.mobile) {
showToast({ message: "请输入手机号" });
return;
@@ -169,22 +171,24 @@ const getSmsCode = async () => {
return;
}
loadingSms.value = true;
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: form.value.mobile, actionType: "agentApply" })
.json();
loadingSms.value = false;
if (data.value && !error.value) {
if (data.value.code === 200) {
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({
mobile: form.value.mobile,
actionType: "agentApply",
captchaVerifyParam,
})
.json(),
(res) => {
if (res?.code === 200) {
showToast({ message: "获取成功" });
startCountdown(); // 启动倒计时
startCountdown();
} else {
showToast(data.value.msg);
showToast(res?.msg || "获取失败");
}
}
);
};
let timer = null;

View File

@@ -361,6 +361,11 @@ const featureMap = {
name: "名下车辆(车牌)",
component: defineAsyncComponent(() => import("@/ui/QCXG5F3A.vue")),
},
FLXGDEA9: {
name: "本人不良",
component: defineAsyncComponent(() => import("@/ui/CFLXGDEA9.vue")),
remark: '本人不良记录查询结果来源于公安部门等权威机构,包括各类违法犯罪前科记录。查询结果仅供参考,具体信息以相关部门官方记录为准。'
},
QCXG4D2E: { name: "名下车辆(数量)", component: defineAsyncComponent(() => import("@/ui/CQCXG4D2E.vue")) },
QCXG5U0Z: { name: "车辆静态信息查询", component: defineAsyncComponent(() => import("@/ui/CQCXG5U0Z.vue")) },
QCXG1U4U: { name: "车辆里程记录(混合查询)", component: defineAsyncComponent(() => import("@/ui/CQCXG1U4U.vue")) },
@@ -470,6 +475,19 @@ const featureMap = {
component: defineAsyncComponent(() => import("@/ui/JRZQ8B3C/index.vue")),
remark: '根据个人的消费能力,评估其消费水平,从而评估其信用风险。'
},
IVYZ0S0D: {
name: "劳动仲裁信息",
component: defineAsyncComponent(() => import("@/ui/IVYZ0S0D.vue")),
remark: '劳动仲裁信息展示被查询人在失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程及通知函触达等方面的风险信息。',
},
IVYZ4Y27: {
name: "学历信息",
component: defineAsyncComponent(() => import("@/ui/IVYZ4Y27/index.vue")),
remark: '学历信息展示专业能力相关的学习经历,包括院校名称、学习类型、专业方向、学习时间、学历等级及核心竞争力等级等字段。',
},
// 司南报告
DWBG6A2C: {
name: "司南报告",
@@ -643,7 +661,8 @@ const featureRiskLevels = {
'FLXG0V4B': 20, // 司法涉诉
'FLXG7E8F': 20, // 司法涉诉
'FLXG3D56': 10, // 违约失信
'JRZQ4AA8': 10, // 还款压力
'JRZQ4AA8': 10, // 还款压力ff
'FLXGDEA9': 20, // 本人不良
// 🟠 中高风险类 - 权重 7
'JRZQ0A03': 7, // 借贷申请记录
@@ -664,11 +683,14 @@ const featureRiskLevels = {
'YYSYS9W1': 5, 'YYSYE7V5': 5, 'YYSYP0T4': 5, 'YYSY6F2B': 5,
'YYSY9E4A': 3, 'QYGL5F6A': 5, 'JRZQACAB': 5, 'JRZQ0B6Y': 5,
'QYGL66SL': 10, 'QYGL2S0W': 10, 'FLXG3A9B': 10,
'IVYZ4Y27': 3, // 学历信息
// 🔵 低风险类 - 权重 3
'IVYZ5733': 3, // 婚姻状态
'IVYZ9A2B': 3, // 学历信息
'IVYZ3P9M': 3, // 学历信息查询(实时版)
'IVYZ0S0D': 10, // 劳动仲裁信息
// 📊 复合报告类 - 按子模块动态计算
'DWBG8B4D': 0, // 谛听多维报告(由子模块计算)
@@ -711,6 +733,9 @@ const featureRiskLevels = {
'CJRZQ5E9F_RiskIndicators': 8,
'CJRZQ5E9F_RiskAdvice': 2,
// 人企关系加强版子模块
'CQYGL3F8E_Investment': 4,
'CQYGL3F8E_SeniorExecutive': 4,

View File

@@ -1,10 +1,12 @@
<script setup>
import { ref, computed, nextTick } from "vue";
import { useDialogStore } from "@/stores/dialogStore";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const emit = defineEmits(['bind-success'])
const router = useRouter();
const dialogStore = useDialogStore();
const { runWithCaptcha } = useAliyunCaptcha();
const agentStore = useAgentStore();
const userStore = useUserStore();
const phoneNumber = ref("");
@@ -30,31 +32,30 @@ const canBind = computed(() => {
);
});
async function sendVerificationCode() {
function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return;
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "bindMobile", captchaVerifyParam })
.json(),
(res) => {
if (res?.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
// 聚焦到验证码输入框
nextTick(() => {
const verificationCodeInput = document.getElementById('verificationCode');
if (verificationCodeInput) {
verificationCodeInput.focus();
}
if (verificationCodeInput) verificationCodeInput.focus();
});
} else {
showToast(data.value.msg);
showToast(res?.msg || "获取失败");
}
}
);
}
function startCountdown() {

View File

@@ -440,7 +440,8 @@
</div>
<!-- 支付组件 -->
<Payment v-model="showPayment" :data="featureData" :id="queryId" type="query" @close="showPayment = false" />
<Payment v-model="showPayment" :data="featureData" :id="queryId" type="query"
:hide-wechat="props.type === 'promotion'" @close="showPayment = false" />
<BindPhoneDialog @bind-success="handleBindSuccess" />
<LoginDialog @login-success="handleLoginSuccess" />
@@ -654,6 +655,7 @@ const getFeatureIcon = (apiId) => {
JRZQ8203: "/inquire_icons/jiedaixingwei.svg", // 借贷行为记录
JRZQ09J8: "/inquire_icons/beijianguanrenyuan.svg", // 收入评估
JRZQ4B6C: "/inquire_icons/fengxianxingwei.svg", // 探针C风险评估
FLXGDEA9: "/inquire_icons/benrenbuliang.png", // 本人不良
};
return iconMap[apiId] || "/inquire_icons/default.svg";
};

View File

@@ -26,10 +26,10 @@
活动价2折优惠
</div>
</div>
<!-- 支付方式选择 -->
<!-- 支付方式微信内仅微信推广 hideWechat 时仅支付宝非微信仅支付宝 -->
<div class="">
<van-cell-group inset>
<van-cell v-if="isWeChat" title="微信支付" clickable @click="selectedPaymentMethod = 'wechat'">
<van-cell v-if="showWechatPay" title="微信支付" clickable @click="selectedPaymentMethod = 'wechat'">
<template #icon>
<van-icon size="24" name="wechat-pay" color="#1AAD19" class="mr-2" />
</template>
@@ -38,28 +38,7 @@
</template>
</van-cell>
<!-- 开发环境测试支付仅开发环境显示 -->
<van-cell v-if="isDev" title="测试支付(开发)" clickable @click="selectedPaymentMethod = 'test'">
<template #icon>
<van-icon size="24" name="passed" color="#07c160" class="mr-2" />
</template>
<template #right-icon>
<van-radio v-model="selectedPaymentMethod" name="test" />
</template>
</van-cell>
<!-- 开发环境测试支付仅开发环境显示 -->
<van-cell v-if="isDev" title="测试支付(开发)" clickable @click="selectedPaymentMethod = 'test'">
<template #icon>
<van-icon size="24" name="passed" color="#07c160" class="mr-2" />
</template>
<template #right-icon>
<van-radio v-model="selectedPaymentMethod" name="test" />
</template>
</van-cell>
<!-- 支付宝支付 -->
<van-cell v-if="!isWeChat" title="支付宝支付" clickable @click="selectedPaymentMethod = 'alipay'">
<van-cell v-if="showAlipayPay" title="支付宝支付" clickable @click="selectedPaymentMethod = 'alipay'">
<template #icon>
<van-icon size="24" name="alipay" color="#00A1E9" class="mr-2" />
</template>
@@ -67,6 +46,16 @@
<van-radio v-model="selectedPaymentMethod" name="alipay" />
</template>
</van-cell>
<!-- 开发环境测试支付 -->
<van-cell v-if="isDev" title="测试支付(开发)" clickable @click="selectedPaymentMethod = 'test'">
<template #icon>
<van-icon size="24" name="passed" color="#07c160" class="mr-2" />
</template>
<template #right-icon>
<van-radio v-model="selectedPaymentMethod" name="test" />
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 确认按钮 -->
@@ -77,9 +66,9 @@
</template>
<script setup>
import { ref, defineProps } from "vue";
import { showConfirmDialog } from "vant";
const { isWeChat } = useEnv();
import { ref, watch, onMounted, computed } from "vue";
import { showToast } from "vant";
const isDev = import.meta.env.DEV;
const props = defineProps({
@@ -95,31 +84,130 @@ const props = defineProps({
type: String,
required: true,
},
/** 为 true 时不展示微信支付(例如 /agent/promotionInquire/ 代理推广查询) */
hideWechat: {
type: Boolean,
default: false,
},
});
const show = defineModel();
const selectedPaymentMethod = ref(
isDev ? "test" : isWeChat.value ? "wechat" : "alipay"
);
onMounted(() => {
if (isDev) {
selectedPaymentMethod.value = "test";
} else if (isWeChat.value) {
selectedPaymentMethod.value = "wechat";
} else {
selectedPaymentMethod.value = "alipay";
function isWechatBrowser() {
if (typeof navigator === "undefined") return false;
return /MicroMessenger/i.test(navigator.userAgent);
}
});
/** 推广强制隐藏微信;非微信环境不展示微信支付 */
const showWechatPay = computed(() => !props.hideWechat && isWechatBrowser());
/** 微信内不展示支付宝;推广 hideWechat 时在微信内只能走支付宝,仍展示 */
const showAlipayPay = computed(() => !isWechatBrowser() || props.hideWechat);
function defaultPaymentMethod() {
if (isDev) return "test";
if (props.hideWechat) return "alipay";
if (isWechatBrowser()) return "wechat";
return "alipay";
}
const selectedPaymentMethod = ref(defaultPaymentMethod());
function syncSelectedPaymentMethod() {
selectedPaymentMethod.value = defaultPaymentMethod();
}
onMounted(syncSelectedPaymentMethod);
watch(
[() => props.hideWechat, showWechatPay, showAlipayPay, show],
() => {
if (show.value) {
syncSelectedPaymentMethod();
}
},
{ flush: "post" }
);
const orderNo = ref("");
const router = useRouter();
const discountPrice = ref(false); // 是否应用折扣
/** 规范化为 WeixinJSBridge 需要的字符串字段 */
function normalizeWechatJsApiPayload(obj) {
if (!obj || typeof obj !== "object") return null;
const appId = obj.appId ?? obj.appid;
const timeStamp = obj.timeStamp ?? obj.timestamp;
const nonceStr = obj.nonceStr ?? obj.nonce_str;
const pkg = obj.package != null ? obj.package : undefined;
const signType = obj.signType ?? obj.sign_type;
const paySign = obj.paySign ?? obj.pay_sign;
const out = {
appId: appId != null && appId !== "" ? String(appId) : undefined,
timeStamp: timeStamp != null && timeStamp !== "" ? String(timeStamp) : undefined,
nonceStr: nonceStr != null && nonceStr !== "" ? String(nonceStr) : undefined,
package: pkg != null && pkg !== "" ? String(pkg) : undefined,
signType: signType != null && signType !== "" ? String(signType) : undefined,
paySign: paySign != null && paySign !== "" ? String(paySign) : undefined,
};
if (Object.values(out).some((v) => v == null)) return null;
return out;
}
/** 从接口 data 中取出 JSAPI 参数(兼容 snake_case、camelCase、扁平 */
function extractWechatJsApiPayload(apiInner) {
if (!apiInner || typeof apiInner !== "object") return null;
const nested =
apiInner.prepay_data ??
apiInner.prepayData ??
apiInner.request_payment ??
apiInner.requestPayment;
if (nested && typeof nested === "object") {
const fromNested = normalizeWechatJsApiPayload(nested);
if (fromNested) return fromNested;
}
return normalizeWechatJsApiPayload(apiInner);
}
/** 微信内 JSAPI 调起支付(等待 WeixinJSBridge 就绪;非微信环境超时后提示) */
function invokeWechatPay(payload, onResult) {
const run = () => {
try {
WeixinJSBridge.invoke("getBrandWCPayRequest", payload, onResult);
} catch (e) {
console.error(e);
showToast("无法调起微信支付,请稍后重试");
show.value = true;
}
};
if (typeof WeixinJSBridge !== "undefined") {
run();
return;
}
const timeoutId = setTimeout(() => {
showToast("请在微信内打开页面后使用微信支付");
show.value = true;
}, 4000);
document.addEventListener(
"WeixinJSBridgeReady",
function onReady() {
clearTimeout(timeoutId);
document.removeEventListener("WeixinJSBridgeReady", onReady);
run();
},
false
);
}
async function getPayment() {
showConfirmDialog({
title: '重要安全声明',
message:
'为保障您的个人信息与资金安全,请您务必知悉以下事项:\n\n关于平台业务:本平台官方服务仅限于大数据报告查询,不涉及也从未开展“央行征信修复”、“贷款办理”或“征信洗白”等相关业务。请注意,本平台出具的报告仅供决策参考,不可作为任何官方征信凭证或贷款依据。\n\n关于诈骗警示任何自称与本平台合作或以“内部渠道”、“百分百包下款”、“修复征信”等为由诱导您进行支付的行为均属欺诈。请您切勿相信谨慎对待任何支付要求。\n\n关于安全提示请您时刻保持警惕妥善保管个人敏感信息。如遇任何索款要求或可疑承诺请务必首先通过我平台官方公布的联系方式进行核实切勿轻信他人。',
})
.then(async () => {
show.value = false;
// 为保障您的个人信息与资金安全,请您务必知悉以下事项:
//
// 关于平台业务:本平台官方服务仅限于大数据报告查询,不涉及也从未开展“央行征信修复”、“贷款办理”或“征信洗白”等相关业务。请注意,本平台出具的报告仅供决策参考,不可作为任何官方征信凭证或贷款依据。
//
// 关于诈骗警示:任何自称与本平台合作,或以“内部渠道”、“百分百包下款”、“修复征信”等为由,诱导您进行支付的行为,均属欺诈。请您切勿相信,谨慎对待任何支付要求。
//
// 关于安全提示:请您时刻保持警惕,妥善保管个人敏感信息。如遇任何索款要求或可疑承诺,请务必首先通过我平台官方公布的联系方式进行核实,切勿轻信他人。
const { data, error } = await useApiFetch("/pay/payment")
.post({
id: props.id,
@@ -128,9 +216,15 @@ async function getPayment() {
})
.json();
if (data.value && !error.value) {
const prepayId = data.value.data.prepay_id ?? data.value.data.prepayId;
const orderNoFromApi = data.value.data.order_no;
// 业务错误时 afterFetch 已 showToast此处仅恢复支付弹窗便于重试
if (error.value || !data.value || data.value.code !== 200) {
show.value = true;
return;
}
const inner = data.value.data ?? {};
const prepayId = inner.prepay_id ?? inner.prepayId;
const orderNoFromApi = inner.order_no ?? inner.orderNo;
orderNo.value = orderNoFromApi;
// 开发环境测试支付:直接跳转结果页
@@ -139,42 +233,44 @@ async function getPayment() {
path: "/payment/result",
query: { orderNo: orderNoFromApi },
});
show.value = false;
return;
}
if (selectedPaymentMethod.value === "alipay") {
// 存储订单ID以便支付宝返回时获取
const prepayUrl = prepayId;
if (!prepayUrl) {
showToast("未获取到支付宝支付链接");
show.value = true;
return;
}
const paymentForm = document.createElement("form");
paymentForm.method = "POST";
paymentForm.action = prepayUrl;
paymentForm.style.display = "none";
document.body.appendChild(paymentForm);
paymentForm.submit();
} else {
const payload = data.value.data.prepay_data;
WeixinJSBridge.invoke(
"getBrandWCPayRequest",
payload,
function (res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 支付成功,直接跳转到结果页面
return;
}
const payload = extractWechatJsApiPayload(inner);
if (!payload) {
showToast(
"未获取到微信支付参数。请确认1后端已部署最新支付接口2请求头含 X-Platform: wxh53用户已绑定微信网页授权 openid。"
);
show.value = true;
return;
}
invokeWechatPay(payload, function (res) {
if (res.err_msg === "get_brand_wcpay_request:ok") {
router.push({
path: "/payment/result",
query: { orderNo: data.value.data.order_no },
query: { orderNo: orderNoFromApi },
});
} else if (res.err_msg) {
showToast(res.err_msg.replace(/^get_brand_wcpay_request:/, "") || "支付未完成");
}
}
);
}
}
show.value = false;
})
.catch(() => {
return;
});
}
</script>

View File

@@ -1,8 +1,11 @@
<script setup>
import { ref, computed } from "vue";
import { useDialogStore } from "@/stores/dialogStore";
import { useAliyunCaptcha } from "@/composables/useAliyunCaptcha";
const router = useRouter();
const dialogStore = useDialogStore();
const { runWithCaptcha } = useAliyunCaptcha();
const agentStore = useAgentStore();
const userStore = useUserStore();
import { showToast } from "vant";
@@ -48,24 +51,26 @@ const canSubmit = computed(() => {
});
// 发送验证码
async function sendVerificationCode() {
function sendVerificationCode() {
if (isCountingDown.value || !isPhoneNumberValid.value) return;
if (!isPhoneNumberValid.value) {
showToast({ message: "请输入有效的手机号" });
return;
}
const { data, error } = await useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "realName" })
.json();
if (data.value && !error.value) {
if (data.value.code === 200) {
runWithCaptcha(
(captchaVerifyParam) =>
useApiFetch("auth/sendSms")
.post({ mobile: phoneNumber.value, actionType: "realName", captchaVerifyParam })
.json(),
(res) => {
if (res?.code === 200) {
showToast({ message: "获取成功" });
startCountdown();
} else {
showToast(data.value.msg);
showToast(res?.msg || "获取失败");
}
}
);
}
function startCountdown() {

View File

View File

@@ -27,14 +27,11 @@ const useApiFetch = createFetch({
if (isWechat) {
platform = "wxh5";
}
options.headers['X-Platform'] = platform
if (token) {
options.headers = {
...options.headers,
Authorization: `${token}`,
...(options.headers || {}),
"X-Platform": platform,
...(token ? { Authorization: `${token}` } : {}),
};
}
return { url, options };
},
async afterFetch({ data, response }) {

View File

@@ -60,6 +60,8 @@ export function useInquireForm(featureRef) {
// 司法涉诉
toc_PersonalLawsuit: ["name", "idCard", "mobile", "verificationCode"],
// 本人不良:姓名 + 身份证(授权由后端默认传 1
toc_PersonalBadRecord: ["name", "idCard"],
// 人企关系加强版:仅身份证号
toc_PersonEnterprisePro: ["idCard"],
// 新企业司法涉诉QYGL66SL仅企业名称其他字段后端自动补齐
@@ -160,6 +162,8 @@ export function useInquireForm(featureRef) {
toc_EnterpriseRelation: ["idCard"],
toc_BankcardFourFactors: ["mobile", "idCard", "bankCard", "name"],
toc_BankcardBlacklist: ["mobile", "idCard", "name", "bankCard"],
backgroundcheck: ["name", "idCard", "mobile"],
};
// 当前 feature 名称

View File

@@ -44,6 +44,13 @@ export const inquireCategoryConfig = {
icon: "sjsys_icon.svg",
iconFrom: "category",
},
{
name: "本人不良",
feature: "toc_PersonalBadRecord",
desc: "本人不良记录风险评估",
icon: "sifasheyu.svg",
iconFrom: "category",
},
],
},
/** 婚恋风险 */
@@ -290,6 +297,49 @@ export const inquireCategoryConfig = {
},
],
},
/** 更多报告:与 InquireForm loadProductBackground 中已配查询页背景的产品一致 */
moreReport: {
title: "更多报告",
banner: "fxpg_banner.png",
items: [
{
name: "小微企业",
feature: "companyinfo",
desc: "小微企业相关风险评估",
icon: "renqiguanxi.svg",
},
{
name: "贷前风险",
feature: "preloanbackgroundcheck",
desc: "贷前背景与信用筛查",
icon: "jiedaishenqing.svg",
},
{
name: "个人大数据",
feature: "personalData",
desc: "个人综合与谛听多维等风险报告",
icon: "fengxianxingwei.svg",
},
{
name: "婚恋风险",
feature: "marriage",
desc: "多维度婚恋对象风险分析",
icon: "hunyinzhuangtai.svg",
},
{
name: "家政风险",
feature: "homeservice",
desc: "家政服务人员背景与风险核查",
icon: "beijianguanrenyuan.svg",
},
{
name: "入职风险",
feature: "backgroundcheck",
desc: "入职背景综合核查",
icon: "sifasheyu.svg",
},
],
},
/** 婚姻状况 */
marriageStatus: {
title: "婚姻状况",

668
src/ui/CFLXGDEA9.vue Normal file
View File

@@ -0,0 +1,668 @@
<template>
<div class="personal-bad-record card">
<!-- 空数据提示 -->
<div v-if="!hasData" class="py-8 text-center text-gray-500">
<div class="flex flex-col items-center">
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-3">
<span class="text-2xl text-gray-400">📋</span>
</div>
<p>暂无本人不良数据</p>
</div>
</div>
<div v-else class="space-y-4">
<!-- 风险总览 -->
<div class="">
<div class="flex items-center mb-3">
<div class="w-12 h-12">
<img src="/inquire_icons/benrenbuliang.png" alt="本人不良记录" class="w-8 h-8 object-contain" />
</div>
<div>
<h2 class="text-lg font-bold text-gray-800">风险总览</h2>
<p class="text-sm text-[#999999]">本人不良记录风险评估</p>
</div>
</div>
<!-- 风险统计 -->
<div class="bg-[#FFF0F0] border border-red-200 rounded-lg p-4" v-if="!isNormalPerson">
<div class="grid grid-cols-3 gap-4">
<div class="text-center">
<div class="text-sm text-[#666666] mb-1">总风险点</div>
<div class="text-lg font-bold text-[#E53935]">{{ hitRiskTypes.length }}</div>
</div>
<div class="text-center">
<div class="text-sm text-[#666666] mb-1">高风险</div>
<div class="text-lg font-bold text-[#E53935]">{{ getHighRiskCount() }}</div>
</div>
<div class="text-center">
<div class="text-sm text-[#666666] mb-1">中风险</div>
<div class="text-lg font-bold text-[#FFC107]">{{ getMiddleRiskCount() }}</div>
</div>
</div>
</div>
<!-- 正常人员显示 -->
<div class="bg-[#F0FFF0] border border-green-200 rounded-lg p-4" v-else>
<div class="flex items-center justify-center">
<div class="w-10 h-10 mr-4">
<img src="@/assets/images/report/zq.png" alt="暂无风险" class="w-10 h-10 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-[#333333] text-center">正常人员</div>
<div class="text-sm text-[#999999] text-center">无不良记录属于正常人员</div>
</div>
</div>
</div>
</div>
<!-- 所有风险类型列表 -->
<div class="space-y-3">
<!-- 人员状态 -->
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('0')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('0')]">
{{ getNormalPersonBadgeText() }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('0')" :alt="isNormalPerson ? '正常人员' : '存在风险'"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('0')">
{{ isNormalPerson ? '正常人员' : '人员状态' }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ isNormalPerson ? '无不良记录,属于正常人员' : '存在不良记录风险'
}}</div>
</div>
</div>
</div>
<!-- A类侵犯公民人身权利民主权利 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">A类侵犯公民人身权利民主权利</div>
<div class="space-y-2">
<div v-for="code in ['A', 'A1', 'A2', 'A3', 'A4', 'A5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- B类经济类前科 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">B类经济类前科</div>
<div class="space-y-2">
<div v-for="code in ['B', 'B1', 'B2', 'B3', 'B4', 'B5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- C类妨害社会管理秩序 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">C类妨害社会管理秩序</div>
<div class="space-y-2">
<div v-for="code in ['C', 'C1', 'C2', 'C3', 'C4', 'C5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- D类重点 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">D类重点</div>
<div class="space-y-2">
<div v-for="code in ['D', 'D1', 'D2', 'D3', 'D4', 'D5']" :key="code"
class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass(code)">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass(code)]">
{{ isHit(code) ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon(code)" :alt="getRiskTypeInfo(code).text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor(code)">
{{ getRiskTypeInfo(code).text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo(code).description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- E类涉交通案件 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">E类涉交通案件</div>
<div class="space-y-2">
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('E')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('E')]">
{{ isHit('E') ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('E')" :alt="getRiskTypeInfo('E').text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('E')">
{{ getRiskTypeInfo('E').text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('E').description }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- F类法院文书 -->
<div class="risk-group">
<div class="text-sm font-semibold text-gray-700 mb-2 px-2">F类法院文书</div>
<div class="space-y-2">
<div class="rounded-lg p-4 border-2 relative" :class="getRiskItemClass('F')">
<div
:class="['absolute top-0 right-0 px-1.5 py-0.5 text-sm font-bold text-white rounded-bl-lg rounded-tr-lg', getRiskBadgeClass('F')]">
{{ isHit('F') ? '异常' : '正常' }}
</div>
<div class="flex items-center pr-12">
<div class="w-8 h-8 mr-3 flex-shrink-0 flex items-center justify-center">
<img :src="getRiskItemIcon('F')" :alt="getRiskTypeInfo('F').text"
class="w-8 h-8 object-contain" />
</div>
<div class="flex-1">
<div class="font-bold text-sm" :class="getRiskItemTextColor('F')">
{{ getRiskTypeInfo('F').text }}
</div>
<div class="text-sm text-[#999999] mt-0.5">{{ getRiskTypeInfo('F').description }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import LRemark from '@/components/LRemark.vue'
import { useRiskNotifier } from '@/composables/useRiskNotifier'
// 导入风险类型图标
import iconZfx from '@/assets/images/report/zfx.png'
import iconGfx from '@/assets/images/report/gfx.png'
import iconSafe from '@/assets/images/report/zq.png'
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
params: {
type: Object,
default: () => ({}),
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
})
// 风险类型代码映射
const riskTypeMap = {
'0': {
text: '正常人员',
description: '无不良记录,属于正常人员',
level: 'normal',
riskLevel: '正常'
},
'A': {
text: '前科:侵犯公民人身权利,民主权利',
description: '存在侵犯公民人身权利、民主权利的前科记录(在逃,盗窃、诈骗、抢劫、故意伤害、强奸等在刑或前科等)',
level: 'high',
riskLevel: '高风险'
},
'A1': {
text: '盗窃',
description: '存在盗窃前科记录',
level: 'high',
riskLevel: '高风险'
},
'A2': {
text: '诈骗',
description: '存在诈骗前科记录',
level: 'high',
riskLevel: '高风险'
},
'A3': {
text: '抢劫/夺',
description: '存在抢劫、抢夺前科记录',
level: 'high',
riskLevel: '高风险'
},
'A4': {
text: '故意伤害/杀人',
description: '存在故意伤害、杀人前科记录',
level: 'high',
riskLevel: '高风险'
},
'A5': {
text: '强奸/性侵/猥亵',
description: '存在强奸、性侵、猥亵前科记录',
level: 'high',
riskLevel: '高风险'
},
'B': {
text: '经济类前科',
description: '存在破坏金融秩序、非法吸存、违发贷款、金融诈骗、集资诈骗、保险诈骗、假币等在刑或前科等',
level: 'medium',
riskLevel: '中风险'
},
'B1': {
text: '走私',
description: '存在走私前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B2': {
text: '破坏金融管理秩序',
description: '存在破坏金融管理秩序前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B3': {
text: '金融诈骗',
description: '存在金融诈骗前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B4': {
text: '洗钱',
description: '存在洗钱前科记录',
level: 'medium',
riskLevel: '中风险'
},
'B5': {
text: '偷漏税',
description: '存在偷漏税前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C': {
text: '妨害社会管理秩序',
description: '存在扰乱公共秩序、妨害司法、妨害国境管理、妨害文物管理、涉毒、涉黄等在刑或前科等',
level: 'medium',
riskLevel: '中风险'
},
'C1': {
text: '扰乱公共秩序',
description: '存在扰乱公共秩序前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C2': {
text: '妨害司法',
description: '存在妨害司法前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C3': {
text: '涉毒',
description: '存在涉毒前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C4': {
text: '涉黄刑案',
description: '存在涉黄刑案前科记录',
level: 'medium',
riskLevel: '中风险'
},
'C5': {
text: '帮信/掩隐/侵公',
description: '存在帮助信息网络犯罪活动、掩饰隐瞒犯罪所得、侵犯公民个人信息前科记录',
level: 'medium',
riskLevel: '中风险'
},
'D': {
text: '重点',
description: '存在危害国家、公共安全,涉恐、疆藏,涉稳、涉黑、涉及境外等',
level: 'critical',
riskLevel: '高风险'
},
'D1': {
text: '危害国家、公共安全',
description: '存在危害国家、公共安全前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D2': {
text: '涉稳',
description: '存在涉稳前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D3': {
text: '涉及境外',
description: '存在涉及境外前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D4': {
text: '涉恐、疆藏',
description: '存在涉恐、疆藏前科记录',
level: 'critical',
riskLevel: '高风险'
},
'D5': {
text: '涉黑',
description: '存在涉黑前科记录',
level: 'critical',
riskLevel: '高风险'
},
'E': {
text: '涉交通案件',
description: '存在危险驾驶、交通肇事等交通案件前科记录',
level: 'low',
riskLevel: '低风险'
},
'F': {
text: '法院文书',
description: '存在法院文书记录',
level: 'low',
riskLevel: '低风险'
}
}
// 获取风险类型信息
const getRiskTypeInfo = (type) => {
return riskTypeMap[type] || riskTypeMap['F']
}
// 解析命中的风险代码
const hitRiskCodes = computed(() => {
const levelData = props.data?.data?.level || props.data?.level
if (!levelData) return []
const levelStr = levelData.toString()
return levelStr.split(',').map(code => code.trim()).filter(code => code)
})
// 判断是否命中某个风险代码
const isHit = (code) => {
if (code === '0') {
// 如果level是'0',则正常人员命中
return hitRiskCodes.value.includes('0')
}
// 如果直接包含该代码,则命中
if (hitRiskCodes.value.includes(code)) {
return true
}
// 对于父级类型A、B、C、D如果子类型命中父类型也算命中
if (code === 'A') {
// 如果 A1、A2、A3、A4、A5 任何一个命中A 也算命中
return ['A1', 'A2', 'A3', 'A4', 'A5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
if (code === 'B') {
// 如果 B1、B2、B3、B4、B5 任何一个命中B 也算命中
return ['B1', 'B2', 'B3', 'B4', 'B5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
if (code === 'C') {
// 如果 C1、C2、C3、C4、C5 任何一个命中C 也算命中
return ['C1', 'C2', 'C3', 'C4', 'C5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
if (code === 'D') {
// 如果 D1、D2、D3、D4、D5 任何一个命中D 也算命中
return ['D1', 'D2', 'D3', 'D4', 'D5'].some(subCode => hitRiskCodes.value.includes(subCode))
}
return false
}
// 获取命中的风险类型列表
const hitRiskTypes = computed(() => {
return hitRiskCodes.value.filter(code => code !== '0').map(code => ({
code,
...getRiskTypeInfo(code)
}))
})
// 判断是否有数据
const hasData = computed(() => {
const levelData = props.data?.data?.level || props.data?.level
return levelData && Object.keys(props.data || {}).length > 0
})
// 判断是否为正常人员
const isNormalPerson = computed(() => {
return hitRiskCodes.value.includes('0') && hitRiskCodes.value.length === 1
})
// 获取高风险数量
const getHighRiskCount = () => {
return hitRiskTypes.value.filter(risk =>
risk.level === 'high' || risk.level === 'critical'
).length
}
// 获取中风险数量
const getMiddleRiskCount = () => {
return hitRiskTypes.value.filter(risk => risk.level === 'medium').length
}
// 获取风险项样式类
const getRiskItemClass = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示绿色,否则显示红色(存在异常)
return isNormalPerson.value ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return 'bg-red-50 border-red-200'
} else if (riskInfo.level === 'medium') {
return 'bg-orange-50 border-orange-200'
} else {
return 'bg-yellow-50 border-yellow-200'
}
}
return 'bg-gray-50 border-gray-200'
}
// 获取风险项文本颜色
const getRiskItemTextColor = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示绿色,否则显示红色(存在异常)
return isNormalPerson.value ? 'text-green-600' : 'text-red-600'
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return 'text-red-600'
} else if (riskInfo.level === 'medium') {
return 'text-orange-600'
} else {
return 'text-yellow-600'
}
}
return 'text-gray-600'
}
// 获取风险项图标
const getRiskItemIcon = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示安全图标,否则显示风险图标(存在异常)
return isNormalPerson.value ? iconSafe : iconGfx
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return iconGfx
} else {
return iconZfx
}
}
return iconSafe
}
// 获取正常人员标签文本
const getNormalPersonBadgeText = () => {
// 只有当 level 只有 '0' 时才显示"正常人员"
if (isNormalPerson.value) {
return '正常'
}
// 否则显示"存在异常"
return '存在异常'
}
// 获取风险标签样式类
const getRiskBadgeClass = (code) => {
const hit = isHit(code)
const riskInfo = getRiskTypeInfo(code)
if (code === '0') {
// 正常人员:如果是正常人员显示绿色,否则显示红色(存在异常)
return isNormalPerson.value ? 'bg-green-500' : 'bg-[#E53935]'
}
if (hit) {
if (riskInfo.level === 'critical' || riskInfo.level === 'high') {
return 'bg-[#E53935]'
} else if (riskInfo.level === 'medium') {
return 'bg-[#D6943E]'
} else {
return 'bg-yellow-500'
}
}
// 未命中显示绿色(正常)
return 'bg-green-500'
}
// 计算风险评分0-100分分数越高越安全
const riskScore = computed(() => {
if (isNormalPerson.value) {
return 100 // 正常人员,最安全
}
if (hitRiskTypes.value.length === 0) {
return 100 // 无风险数据,最安全
}
// 获取高风险和中风险数量
const highRiskCount = getHighRiskCount()
const middleRiskCount = getMiddleRiskCount()
// 高风险数量越多,分数越低
let score = 100
// 高风险扣分每个高风险扣15分
score -= highRiskCount * 15
// 中风险扣分每个中风险扣8分
score -= middleRiskCount * 8
// 确保分数在合理范围内最低20分
return Math.max(20, Math.min(100, score))
})
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore)
// 暴露给父组件
defineExpose({
riskScore
})
</script>
<style lang="scss" scoped>
.personal-bad-record {
@apply space-y-4;
}
.risk-group {
@apply mb-4;
}
</style>

View File

@@ -82,6 +82,7 @@ defineExpose({
});
</script>
<template>
<div class="card">
<div class="status-info flex flex-col items-center">

View File

@@ -69,7 +69,6 @@ const currentStatus = !actualData
</p>
</div>
<p v-html="currentStatus.description" class="status-description mt-3 text-sm text-gray-600"></p>
</div>
</div>
</template>

View File

@@ -51,10 +51,13 @@ const maskedName = computed(() => {
return name.length > 1 ? name[0] + '*'.repeat(name.length - 1) : '*';
});
// status: 0 一致, -1 不一致, -2 非法姓名, -4 无记录
// status: 0 一致, -1 不一致, -2 非法姓名, -4 无记录(接口多为 result.status
const status = computed(() => {
const s = props.data?.status;
if (s === 0 || s === -1 || s === -2 || s === -4) return s;
const d = props.data;
const direct = d?.status;
if (direct === 0 || direct === -1 || direct === -2 || direct === -4) return direct;
const nested = d?.result?.status;
if (nested === 0 || nested === -1 || nested === -2 || nested === -4) return nested;
return null;
});

524
src/ui/IVYZ0S0D.vue Normal file
View File

@@ -0,0 +1,524 @@
<script setup>
import { computed, ref } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
const props = defineProps({
data: { type: Object, default: () => ({}) },
params: { type: Object, default: () => ({}) },
apiId: { type: String, default: '' },
index: { type: Number, default: 0 },
notifyRiskStatus: { type: Function, default: () => { } },
});
const periodTab = ref('threeYears'); // 近三年 | 近五年
// 支持 data 或 data.result接口返回的 result 对象)
const result = computed(() => props.data?.result ?? props.data ?? {});
const getStatusText = (value) => {
if (value === 1) return '未命中';
if (value === 2) return '命中';
return '—';
};
// 获取通知函期间描述文本(支持数字或字符串如 "2"
const getNoticeLetterPeriodText = (period) => {
const p = Number(period);
const periodMap = { 0: '没有被发送通知函', 1: '近2年内', 2: '2-4年', 3: '5年以上' };
return periodMap[p] ?? '—';
};
// 检查是否至少有一个数据类别有内容
const hasAnyData = computed(() => {
const r = result.value;
return Object.keys(r).length > 0;
});
// 汇总数据 - 按分类分组 { key, title, rows }
const summaryGroups = computed(() => {
const groups = [];
const basic = result.value.basic_info;
if (basic?.risk_flag !== undefined) {
groups.push({ key: 'basic', title: '基础风险', rows: [{ label: '该人员是否有风险', value: basic.risk_flag }] });
}
const dishonesty = result.value.dishonesty?.dishonesty;
const highConsumption = result.value.high_consumption?.high_consumption;
if (dishonesty !== undefined || highConsumption !== undefined) {
const rows = [];
if (dishonesty !== undefined) rows.push({ label: '失信人员风险', value: dishonesty });
if (highConsumption !== undefined) rows.push({ label: '限制高消费人员风险', value: highConsumption });
groups.push({ key: 'credit', title: '失信限高', rows });
}
const labor = result.value.labor_disputes;
if (labor) {
const items = [['劳动争议', labor.labor_disputes], ['劳动合同纠纷', labor.labor_contract], ['劳动关系纠纷', labor.labor_relation], ['追索劳动报酬纠纷', labor.wage_claim], ['经济补偿金纠纷', labor.compensation], ['集体合同纠纷', labor.collective_contract], ['劳务派遣合同纠纷', labor.dispatch_contract], ['非全日制用工纠纷', labor.part_time], ['竞业限制纠纷', labor.non_compete]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'labor', title: '劳动争议', rows });
}
const social = result.value.social_insurance;
if (social) {
const items = [['社会保险纠纷', social.social_insurance], ['养老保险待遇纠纷', social.pension], ['工伤保险待遇纠纷', social.injury_insurance], ['医疗保险待遇纠纷', social.medical_insurance], ['生育保险待遇纠纷', social.maternity_insurance], ['商业保险待遇纠纷', social.commercial_insurance]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'social', title: '社会保险', rows });
}
if (result.value.welfare_disputes?.welfare !== undefined) {
groups.push({ key: 'welfare', title: '福利待遇', rows: [{ label: '福利待遇纠纷', value: result.value.welfare_disputes.welfare }] });
}
const personnel = result.value.personnel_disputes;
if (personnel) {
const items = [['人事争议类纠纷', personnel.personnel_dispute], ['辞职争议纠纷', personnel.resignation_dispute], ['辞退争议纠纷', personnel.dismissal_dispute], ['聘用合同争议纠纷', personnel.employment_contract]];
const rows = items.filter((item) => item[1] !== undefined).map((item) => ({ label: item[0], value: item[1] }));
if (rows.length) groups.push({ key: 'personnel', title: '人事争议', rows });
}
const arb = result.value.arbitration;
if (arb && (arb.arbitration_confirmation !== undefined || arb.arbitration_revocation !== undefined)) {
const rows = [];
if (arb.arbitration_confirmation !== undefined) rows.push({ label: '申请仲裁确认', value: arb.arbitration_confirmation });
if (arb.arbitration_revocation !== undefined) rows.push({ label: '撤销仲裁裁决', value: arb.arbitration_revocation });
groups.push({ key: 'arbitration', title: '仲裁流程', rows });
}
const notice = result.value.notice_letter;
if (notice?.notice_letter !== undefined) {
const rows = [{ label: '通知函触达', value: notice.notice_letter }];
if (notice.notice_letter_period !== undefined && notice.notice_letter === 2) rows.push({ label: '通知函发送时间', value: null, period: notice.notice_letter_period });
groups.push({ key: 'notice', title: '通知函触达', rows });
}
return groups;
});
const summaryRows = computed(() => summaryGroups.value.flatMap((g) => g.rows));
// 真正的风险项(文档:失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程、通知函触达)
// 排除 basic_info.risk_flag汇总结论和 notice_letter_period非风险项
const riskItemRows = computed(() =>
summaryGroups.value
.filter((g) => g.key !== 'basic')
.flatMap((g) => g.rows)
.filter((r) => r.value === 1 || r.value === 2)
);
// 近三年/近五年 - 按分类分组
const periodGroups = computed(() => {
const suffix = periodTab.value === 'threeYears' ? '_3y' : '_5y';
const groups = [];
const labor = result.value.labor_disputes;
if (labor) {
const keys = ['labor_disputes', 'labor_relation', 'wage_claim', 'compensation', 'collective_contract', 'dispatch_contract', 'part_time', 'non_compete'];
const labels = ['劳动争议', '劳动关系', '追索劳动报酬', '经济补偿金', '集体合同', '劳务派遣', '非全日制用工', '竞业限制'];
const rows = keys.map((k, i) => ({ label: labels[i], value: labor[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'labor', title: '劳动争议', rows });
}
const social = result.value.social_insurance;
if (social) {
const keys = ['pension', 'injury_insurance', 'medical_insurance', 'maternity_insurance', 'commercial_insurance'];
const labels = ['养老保险', '工伤保险', '医疗保险', '生育保险', '商业保险'];
const rows = keys.map((k, i) => ({ label: labels[i], value: social[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'social', title: '社会保险', rows });
}
const personnel = result.value.personnel_disputes;
if (personnel) {
const keys = ['resignation_dispute', 'dismissal_dispute', 'employment_contract'];
const labels = ['辞职争议', '辞退争议', '聘用合同'];
const rows = keys.map((k, i) => ({ label: labels[i], value: personnel[k + suffix] })).filter((r) => r.value === 2).map((r) => ({ label: r.label, value: 2 }));
if (rows.length) groups.push({ key: 'personnel', title: '人事争议', rows });
}
const arb = result.value.arbitration;
if (arb) {
const rows = [];
if (arb[`arbitration_confirmation${suffix}`] === 2) rows.push({ label: '申请仲裁确认', value: 2 });
if (arb[`arbitration_revocation${suffix}`] === 2) rows.push({ label: '撤销仲裁裁决', value: 2 });
if (rows.length) groups.push({ key: 'arbitration', title: '仲裁流程', rows });
}
return groups;
});
const periodRows = computed(() => periodGroups.value.flatMap((g) => g.rows));
// 用于 riskScore需要近三年+近五年全部数据
const recentThreeYearsRows = computed(() => {
const r = result.value;
const rows = [];
const labor = r.labor_disputes;
if (labor) {
[['labor_disputes_3y'], ['labor_relation_3y'], ['wage_claim_3y'], ['compensation_3y'], ['collective_contract_3y'], ['dispatch_contract_3y'], ['part_time_3y'], ['non_compete_3y']].forEach(([k]) => { if (labor[k] === 2) rows.push({ value: 2 }); });
}
const social = r.social_insurance;
if (social) {
['pension_3y', 'injury_insurance_3y', 'medical_insurance_3y', 'maternity_insurance_3y', 'commercial_insurance_3y'].forEach((k) => { if (social[k] === 2) rows.push({ value: 2 }); });
}
const personnel = r.personnel_disputes;
if (personnel) {
['resignation_dispute_3y', 'dismissal_dispute_3y', 'employment_contract_3y'].forEach((k) => { if (personnel[k] === 2) rows.push({ value: 2 }); });
}
const arb = r.arbitration;
if (arb) {
if (arb.arbitration_confirmation_3y === 2) rows.push({ value: 2 });
if (arb.arbitration_revocation_3y === 2) rows.push({ value: 2 });
}
return rows;
});
const recentFiveYearsRows = computed(() => {
const r = result.value;
const rows = [];
const labor = r.labor_disputes;
if (labor) {
[['labor_disputes_5y'], ['labor_relation_5y'], ['wage_claim_5y'], ['compensation_5y'], ['collective_contract_5y'], ['dispatch_contract_5y'], ['part_time_5y'], ['non_compete_5y']].forEach(([k]) => { if (labor[k] === 2) rows.push({ value: 2 }); });
}
const social = r.social_insurance;
if (social) {
['pension_5y', 'injury_insurance_5y', 'medical_insurance_5y', 'maternity_insurance_5y', 'commercial_insurance_5y'].forEach((k) => { if (social[k] === 2) rows.push({ value: 2 }); });
}
const personnel = r.personnel_disputes;
if (personnel) {
['resignation_dispute_5y', 'dismissal_dispute_5y', 'employment_contract_5y'].forEach((k) => { if (personnel[k] === 2) rows.push({ value: 2 }); });
}
const arb = r.arbitration;
if (arb) {
if (arb.arbitration_confirmation_5y === 2) rows.push({ value: 2 });
if (arb.arbitration_revocation_5y === 2) rows.push({ value: 2 });
}
return rows;
});
// 头部风险总结:风险类型、建议、命中统计(仅真正的风险项)
const riskSummary = computed(() => {
const basic = result.value.basic_info;
const hasRisk = basic?.risk_flag === 2;
// 真正的风险项:总项数、命中数(文档:失信限高、劳动争议、社会保险、福利待遇、人事争议、仲裁流程、通知函触达)
const totalItems = riskItemRows.value.length;
const hitItems = riskItemRows.value.filter((r) => r.value === 2).length;
// 命中的风险分类(汇总中 value=2 的 group
const riskCategories = summaryGroups.value
.filter((g) => g.key !== 'basic' && g.rows.some((r) => r.value === 2))
.map((g) => g.title);
// 精简建议(取主要类型合并为一句)
const suggestionMap = {
'失信限高': '征信修复与限高事项',
'劳动争议': '劳动纠纷与薪酬离职',
'社会保险': '社保缴纳与补缴',
'福利待遇': '福利待遇合规',
'人事争议': '辞职辞退与聘用合同',
'仲裁流程': '仲裁案件进展',
'通知函触达': '仲裁调解涉诉通知',
};
const suggestionParts = riskCategories.map((c) => suggestionMap[c]).filter(Boolean);
const suggestion = suggestionParts.length ? `建议关注${suggestionParts.slice(0, 3).join('、')}` : '';
return {
hasRisk,
label: hasRisk ? '有风险' : '无风险',
riskCategories,
suggestion,
totalItems,
hitItems,
};
});
// 风险评分 0-100越高越安全供 BaseReport 分析指数)
// 基于真正的风险项riskItemRows与展示逻辑一致通过 useRiskNotifier 递交 BaseReport
const riskScore = computed(() => {
const basic = result.value.basic_info;
if (!basic || basic.risk_flag === 1) return 100;
const hitItems = riskItemRows.value.filter((r) => r.value === 2).length;
const score = 100 - hitItems * 2; // 每命中 1 项扣 2 分
return Math.max(0, Math.min(100, score));
});
useRiskNotifier(props, riskScore);
defineExpose({ riskScore });
// 借鉴司法涉诉概览:风险图标与背景样式
const getRiskIcon = () => {
if (riskSummary.value.hasRisk) return new URL('@/assets/images/report/gfx.png', import.meta.url).href;
return new URL('@/assets/images/report/zq.png', import.meta.url).href;
};
</script>
<template>
<div class="report-wrap">
<!-- 头部风险总结底色固定白色命中项用内嵌 card 展现 -->
<div v-if="hasAnyData" class="risk-summary card">
<div class="flex items-center mb-4">
<div class="w-12 h-12 mr-3 flex-shrink-0">
<img :src="getRiskIcon()" alt="风险" class="w-12 h-12 object-contain" />
</div>
<div class="text-gray-700 text-[15px] leading-relaxed">
<template v-if="riskSummary.hasRisk">
<span v-if="riskSummary.riskCategories.length">涉及{{ riskSummary.riskCategories.join('')
}}</span>
<span v-if="riskSummary.suggestion">{{ riskSummary.suggestion }}</span>
</template>
<template v-else>未检测到相关风险</template>
</div>
</div>
<!-- 命中项仅显示总项数 / 命中数真正的风险项失信限高劳动争议社会保险福利待遇人事争议仲裁流程通知函触达 -->
<div v-if="riskSummary.totalItems > 0" class="inner-card p-4 rounded-xl text-center"
:class="riskSummary.hitItems > 0 ? 'inner-card-risk' : 'inner-card-safe'">
<div class="text-2xl font-bold mb-1"
:class="riskSummary.hitItems > 0 ? 'text-[#EB3C3C]' : 'text-[#10b981]'">
{{ riskSummary.hitItems }}/{{ riskSummary.totalItems }}
</div>
<div class="text-sm font-medium text-gray-800">命中项</div>
</div>
</div>
<!-- Card 1: 汇总 -->
<section class="card">
<header class="card-header">
<span class="card-title">风险概览</span>
<span class="card-subtitle">综合评估结果</span>
</header>
<div v-if="hasAnyData" class="group-list">
<div v-for="(group, gi) in summaryGroups" :key="group.key" class="group-box">
<div class="group-header">
<span class="group-title">{{ group.title }}</span>
</div>
<div class="group-body">
<div v-for="(row, ri) in group.rows" :key="ri" class="data-row">
<span class="data-label">{{ row.label }}</span>
<span v-if="row.period !== undefined" class="data-value data-value-text">{{
getNoticeLetterPeriodText(row.period) }}</span>
<span v-else
:class="['data-value', 'data-badge', row.value === 2 ? 'badge-risk' : 'badge-safe']">
<span class="badge-dot" :class="row.value === 2 ? 'dot-risk' : 'dot-safe'" />
{{ getStatusText(row.value) }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="empty-icon"></span>
<span class="empty-text">暂无相关风险数据</span>
</div>
</section>
<!-- Card 2: 近三年 / 近五年 -->
<section class="card">
<header class="card-header">
<span class="card-title">时间维度</span>
<span class="card-subtitle">按周期查看命中情况</span>
</header>
<div class="period-tabs">
<button type="button" :class="['period-tab', periodTab === 'threeYears' && 'active']"
@click="periodTab = 'threeYears'">近三年</button>
<button type="button" :class="['period-tab', periodTab === 'fiveYears' && 'active']"
@click="periodTab = 'fiveYears'">近五年</button>
</div>
<div v-if="periodGroups.length" class="group-list">
<div v-for="(group, gi) in periodGroups" :key="group.key" class="group-box">
<div class="group-header">
<span class="group-title">{{ group.title }}</span>
</div>
<div class="group-body">
<div v-for="(row, ri) in group.rows" :key="ri" class="data-row">
<span class="data-label">{{ row.label }}</span>
<span class="data-value data-badge badge-risk">
<span class="badge-dot dot-risk" />
{{ getStatusText(row.value) }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<span class="empty-icon"></span>
<span class="empty-text">暂无{{ periodTab === 'threeYears' ? '近三年' : '近五年' }}命中数据</span>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
.report-wrap {
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.06);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.inner-card {
border: 1px solid #e2e8f0;
}
.inner-card-risk {
background: rgba(235, 60, 60, 0.1);
border-color: rgba(235, 60, 60, 0.3);
}
.inner-card-safe {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
}
.card-header {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid #f1f5f9;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
letter-spacing: 0.02em;
}
.card-subtitle {
font-size: 13px;
color: #94a3b8;
}
.period-tabs {
display: flex;
gap: 8px;
margin-bottom: 14px;
padding: 4px;
background: #f8fafc;
border-radius: 10px;
}
.period-tab {
flex: 1;
padding: 10px 16px;
font-size: 15px;
color: #64748b;
background: transparent;
border: none;
border-radius: 8px;
cursor: pointer;
}
.period-tab.active {
color: #1e293b;
font-weight: 500;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.group-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.group-box {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
}
.group-header {
padding: 12px 16px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.group-title {
font-size: 16px;
font-weight: 600;
color: #475569;
letter-spacing: 0.03em;
}
.group-body {
display: flex;
flex-direction: column;
padding: 0 16px;
}
.data-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
font-size: 16px;
}
.data-row:not(:last-child) {
border-bottom: 1px solid #f1f5f9;
}
.data-label {
color: #475569;
flex: 1;
margin-right: 12px;
font-size: 16px;
line-height: 1.5;
}
.data-value {
flex-shrink: 0;
}
.data-value-text {
color: #64748b;
font-size: 15px;
}
.data-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
}
.badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.badge-risk {
background: #fff1f2;
color: #be123c;
}
.dot-risk {
background: #e11d48;
}
.badge-safe {
background: #f0fdf4;
color: #047857;
}
.dot-safe {
background: #10b981;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 28px 16px;
}
.empty-icon {
font-size: 20px;
color: #cbd5e1;
font-weight: 300;
}
.empty-text {
font-size: 15px;
color: #94a3b8;
}
</style>

View File

@@ -0,0 +1,34 @@
[
{
"value": "A",
"label": "985院校"
},
{
"value": "B",
"label": "双一流"
},
{
"value": "C",
"label": "211院校"
},
{
"value": "D",
"label": "一本院校"
},
{
"value": "E",
"label": "二本院校"
},
{
"value": "F",
"label": "大专院校"
},
{
"value": "G",
"label": "成人本科"
},
{
"value": "H",
"label": "其他"
}
]

View File

@@ -0,0 +1,22 @@
[
{
"value": "9",
"label": "博士研究生"
},
{
"value": "8",
"label": "硕士研究生"
},
{
"value": "7",
"label": "本科"
},
{
"value": "6",
"label": "大专"
},
{
"value": "5",
"label": "其他"
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

282
src/ui/IVYZ4Y27/index.vue Normal file
View File

@@ -0,0 +1,282 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useRiskNotifier } from '@/composables/useRiskNotifier';
import abilityCompetitive from './abilityCompetitive.json';
import abilityCompetitiveDegree from './abilityCompetitiveDegree.json';
import abilityName from './abilityName.json';
import abilityField from './abilityField.json';
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
apiId: {
type: String,
default: '',
},
index: {
type: Number,
default: 0,
},
notifyRiskStatus: {
type: Function,
default: () => { },
},
});
// 导入图片图标
import rkpmIcon from '@/assets/images/report/rkpm.png';
import zymcIcon from '@/assets/images/report/zymc.png';
import xxxsIcon from '@/assets/images/report/xxxs.png';
import xxlxIcon from '@/assets/images/report/xxlx.png';
import bysjIcon from '@/assets/images/report/bysj.png';
import xlIcon from '@/assets/images/report/xl.png';
// 计算风险评分
const riskScore = computed(() => {
return 100;
});
// 使用 composable 通知父组件风险评分
useRiskNotifier(props, riskScore);
// 暴露给父组件
defineExpose({
riskScore
});
// 获取列表数据
const abilityList = computed(() => {
return props.data?.abilityInfo || [];
});
// 字典映射
const competitiveMap = {};
abilityCompetitive.forEach(item => {
competitiveMap[item.value] = item.label;
});
const competitiveDegreeMap = {};
abilityCompetitiveDegree.forEach(item => {
competitiveDegreeMap[item.value] = item.label;
});
const nameMap = {};
abilityName.forEach(item => {
nameMap[item.value] = item.label;
});
const fieldMap = {};
abilityField.forEach(item => {
fieldMap[item.value] = item.label;
});
// 翻译函数
const getCompetitiveLabel = (val) => competitiveMap[val] || val || '未知';
const getCompetitiveDegreeLabel = (val) => competitiveDegreeMap[val] || val || '未知';
const getNameLabel = (val) => nameMap[val] || val || '未知';
// 专业方向翻译C99999/B99999 等以 99999 结尾的表示未匹配到相关专业
const getFieldLabel = (val) => {
if (!val) return '未知';
if (val.endsWith('99999')) {
const prefix = val.charAt(0);
const prefixMap = { 'A': '硕士研究生/博士研究生', 'B': '本科', 'C': '专科' };
return `${prefixMap[prefix] || ''}(未匹配到相关专业)`;
}
return fieldMap[val] || val || '未知';
};
// 格式化日期 yyyy-MM-dd -> yyyy年MM月dd日
const formatDate = (dateStr) => {
if (!dateStr) return '未知';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[0]}${parts[1]}${parts[2]}`;
}
if (parts.length === 2) {
return `${parts[0]}${parts[1]}`;
}
return dateStr;
};
// 获取核心竞争力等级对应的颜色样式
const getCompetitiveBadgeClass = (val) => {
const classMap = {
'A': 'bg-amber-500 text-white',
'B': 'bg-green-500 text-white',
'C': 'bg-blue-500 text-white',
'D': 'bg-indigo-500 text-white',
'E': 'bg-cyan-500 text-white',
'F': 'bg-gray-500 text-white',
'G': 'bg-orange-500 text-white',
'H': 'bg-gray-400 text-white',
};
return classMap[val] || 'bg-gray-400 text-white';
};
// 获取学历等级对应的颜色样式
const getDegreeBadgeClass = (val) => {
const classMap = {
'9': 'bg-purple-500 text-white',
'8': 'bg-blue-600 text-white',
'7': 'bg-blue-500 text-white',
'6': 'bg-cyan-600 text-white',
'5': 'bg-gray-500 text-white',
};
return classMap[val] || 'bg-gray-400 text-white';
};
// 判断是否有数据
const hasData = computed(() => {
return abilityList.value.length > 0;
});
</script>
<template>
<div v-if="hasData" class="card max-w-4xl mx-auto">
<!-- 头部区域 -->
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 flex items-center justify-center">
<img :src="xlIcon" alt="学历图标" class="w-12 h-12" />
</div>
<div>
<h2 class="text-2xl font-bold text-gray-900">学历信息</h2>
<p class="text-sm text-gray-500">Education Information</p>
</div>
</div>
<div class="text-sm text-gray-500">
{{ abilityList.length }} 条记录
</div>
</div>
</div>
<!-- 学历列表 -->
<div class="space-y-6">
<div v-for="(item, idx) in abilityList" :key="idx"
class="bg-white border border-gray-200 rounded-xl overflow-hidden hover:shadow-md transition-shadow">
<!-- 顶部学历等级与核心竞争力 -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 px-5 py-4 border-b border-blue-100">
<div class="flex items-center justify-between flex-wrap gap-2">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center">
<span class="text-white text-lg font-bold">{{ idx + 1 }}</span>
</div>
<div>
<div class="flex items-center gap-2 flex-wrap">
<span v-if="item.abilityCompetitiveDegree"
:class="['inline-flex items-center px-3 py-1 rounded-full text-sm font-medium', getDegreeBadgeClass(item.abilityCompetitiveDegree)]">
{{ getCompetitiveDegreeLabel(item.abilityCompetitiveDegree) }}
</span>
<span v-if="item.abilityCompetitive"
:class="['inline-flex items-center px-3 py-1 rounded-full text-xs font-medium', getCompetitiveBadgeClass(item.abilityCompetitive)]">
{{ getCompetitiveLabel(item.abilityCompetitive) }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 详细信息 -->
<div class="p-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 院校名称 -->
<div v-if="item.abilityName"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="xxlxIcon" alt="院校名称" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">院校名称</div>
<div class="text-base font-medium text-gray-900">{{ getNameLabel(item.abilityName)
}}</div>
</div>
</div>
</div>
<!-- 学习类型 -->
<div v-if="item.abilityType"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="xxxsIcon" alt="学习类型" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">学习类型</div>
<div class="text-base font-medium text-gray-900">{{ item.abilityType }}</div>
</div>
</div>
</div>
<!-- 专业方向 -->
<div v-if="item.abilityField"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="zymcIcon" alt="专业方向" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">专业方向</div>
<div class="text-base font-medium text-gray-900">{{ getFieldLabel(item.abilityField)
}}</div>
</div>
</div>
</div>
<!-- 学习时间 -->
<div v-if="item.abilityStartDate || item.abilityEndDate"
class="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 transition-colors">
<div class="flex items-start gap-3">
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0 mt-1">
<img :src="bysjIcon" alt="学习时间" class="w-8 h-8" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-600 mb-1">学习时间</div>
<div class="text-base font-medium text-gray-900">
{{ formatDate(item.abilityStartDate) }} ~ {{ formatDate(item.abilityEndDate) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div v-else class="card max-w-2xl mx-auto">
<div class="flex flex-col items-center py-12 text-center">
<div class="w-20 h-20 flex items-center justify-center mb-4">
<img :src="xlIcon" alt="学历图标" class="w-20 h-20 opacity-40" />
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无学历信息</h3>
<p class="text-sm text-gray-500 max-w-md">
系统中暂无相关的学历信息记录这可能是因为学历信息未公开或数据正在同步中
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.card {
padding: 1.5rem;
box-shadow: 0px 0px 24px 0px #3F3F3F0F;
border-radius: 12px;
background: white;
}
.bg-gray-50 {
transition: all 0.3s ease;
}
.bg-gray-50:hover {
transform: translateY(-2px);
}
</style>

View File

@@ -108,7 +108,7 @@
在进行税务代扣代缴结算服务时我们有权将必要信息提供给依法合作的第三方税务服务商结算服务商前提是该第三方承担同等信息保护义务
</div>
<div>
您可以对全能查产品及服务的体验问题反馈帮助我们更好地了解您使用我们产品或服务的体验和需求改善我们产品或服务,为此我们会记录您的联系信息反馈的问题或建议以便我们进一步联系您反馈您我们的处理意见
您可以对天远查产品及服务的体验问题反馈帮助我们更好地了解您使用我们产品或服务的体验和需求改善我们产品或服务,为此我们会记录您的联系信息反馈的问题或建议以便我们进一步联系您反馈您我们的处理意见
为向您提供更好的服务例如在不同的服务端或设备上提供体验一致的服务和您需求的客服接待了解产品适配性识别账号异常状态
</div>
</div>

View File

@@ -60,6 +60,8 @@ function toInquire(name) {
router.push(`/inquire/category/vehicle`);
} else if (name === "Verify") {
router.push(`/inquire/category/verify`);
} else if (name === "MoreReport") {
router.push(`/inquire/category/moreReport`);
} else if (name === "Promote") {
router.push(`/agent/promote`);
} else {
@@ -116,17 +118,16 @@ function toPromote() {
</div>
<div class="relative flex flex-col cursor-pointer rounded-bl-[35px] rounded-br-lg rounded-tl-[35px] rounded-tr-lg bg-white px-4 py-6 shadow-lg"
@click="toInquire('VehiclesUnderName')">
@click="toInquire('MoreReport')">
<div class="min-h-18 gap-2 bg-white px-1">
<div class="mb-2 flex justify-around">
<img class="h-12 w-12 flex-shrink-0" src="@/assets/images/icon_index_chacheliang.svg"
alt="车辆查询图标" />
<img class="h-12 w-12 flex-shrink-0" src="@/assets/images/icon_3.png" alt="更多报告图标" />
<div class="mt-1 max-w-max flex-shrink-0 text-left text-lg text-gray-600 font-bold">
车辆查询
更多报告
</div>
</div>
<div class="max-w-max text-left text-xs text-gray-600">
查询个人名下所有车辆信息包括车辆品牌型号车牌号登记时间等详细信息
基于多维度数据整合准确评估潜在风险的全面报告
</div>
</div>
</div>
@@ -159,16 +160,17 @@ function toPromote() {
</div>
</div>
<div class="relative flex flex-col cursor-pointer rounded-bl-lg rounded-br-[35px] rounded-tl-lg rounded-tr-[35px] bg-white px-4 py-6 shadow-lg"
@click="toInquire('Promote')">
@click="toInquire('VehiclesUnderName')">
<div class="min-h-18 gap-2 bg-white px-1">
<div class="mb-2 flex justify-around">
<img class="h-12 w-12 flex-shrink-0" src="@/assets/images/icon_3.png" alt="推广报告图标" />
<div class="mt-1 max-w-max flex-shrink-0 text-left text-lg text-gray-600 font-bold">
推广报告
车辆查询
</div>
<img class="h-12 w-12 flex-shrink-0" src="@/assets/images/icon_index_chacheliang.svg"
alt="车辆查询图标" />
</div>
<div class="max-w-max text-left text-xs text-gray-600">
基于多维度数据整合准确评估潜在风险的全面报告
查询个人名下所有车辆信息包括车辆品牌型号车牌号登记时间等详细信息
</div>
</div>
</div>