b2f5a53dd8
- GET /invitees: 分页 + 名字降级兜底(昵称→微信昵称→脱敏手机号) - 指纹归因: 落地页采集 + (IP,设备型号)反查撞库 + 时间窗口闸 - test_invite: +5 个列表测试(脱敏/倒序/昵称优先/分页/空), 修指纹测试跨用例串味 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Reviewed-on: #31 Co-authored-by: xiebing <xiebing@wonderable.ai> Co-committed-by: xiebing <xiebing@wonderable.ai>
169 lines
8.6 KiB
HTML
169 lines
8.6 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||
<meta name="format-detection" content="telephone=no">
|
||
<title>傻瓜比价 · 下载</title>
|
||
<style>
|
||
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
|
||
html,body { height:100%; }
|
||
body {
|
||
font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
||
background:linear-gradient(165deg,#FF7A3D 0%,#FF3B30 52%,#E0245E 100%);
|
||
color:#fff; min-height:100%; display:flex; flex-direction:column;
|
||
align-items:center; justify-content:center; padding:40px 26px; text-align:center;
|
||
overflow-x:hidden;
|
||
}
|
||
.logo {
|
||
width:104px; height:104px; border-radius:26px; background:#fff;
|
||
display:flex; align-items:center; justify-content:center; font-size:52px;
|
||
box-shadow:0 14px 34px rgba(0,0,0,.22); margin-bottom:24px;
|
||
}
|
||
h1 { font-size:30px; font-weight:800; letter-spacing:1px; }
|
||
.slogan { margin-top:12px; font-size:16px; line-height:1.7; opacity:.95; max-width:300px; }
|
||
.feats { margin-top:26px; display:flex; flex-direction:column; gap:12px; width:100%; max-width:320px; }
|
||
.feat { background:rgba(255,255,255,.16); border-radius:14px; padding:13px 16px; font-size:15px; display:flex; align-items:center; gap:10px; }
|
||
.feat b { font-weight:700; }
|
||
.btn {
|
||
margin-top:34px; width:100%; max-width:320px; border:none; cursor:pointer;
|
||
background:#fff; color:#FF3B30; font-size:19px; font-weight:800;
|
||
padding:17px 0; border-radius:999px; box-shadow:0 10px 26px rgba(0,0,0,.22);
|
||
display:flex; align-items:center; justify-content:center; gap:9px;
|
||
}
|
||
.btn:active { transform:translateY(1px); opacity:.92; }
|
||
.hint { margin-top:16px; font-size:13px; opacity:.85; }
|
||
.foot { margin-top:30px; font-size:12px; opacity:.6; line-height:1.6; max-width:320px; }
|
||
|
||
/* 微信内"去浏览器打开"引导蒙层 */
|
||
#wxmask {
|
||
display:none; position:fixed; inset:0; z-index:9999;
|
||
background:rgba(0,0,0,.86); padding:18px;
|
||
}
|
||
#wxmask.show { display:block; }
|
||
.arrow { position:absolute; top:8px; right:14px; width:120px; }
|
||
.wxtip { position:absolute; top:150px; right:18px; left:18px; text-align:right; }
|
||
.wxtip .big { font-size:21px; font-weight:800; line-height:1.5; }
|
||
.wxtip .big em { color:#FFD24D; font-style:normal; }
|
||
.wxtip .sub { margin-top:14px; font-size:15px; line-height:1.8; opacity:.9; }
|
||
.wxsteps { margin-top:26px; text-align:left; background:rgba(255,255,255,.1); border-radius:14px; padding:18px 18px; font-size:15px; line-height:2; }
|
||
.wxsteps .n { display:inline-block; width:22px; height:22px; line-height:22px; text-align:center; border-radius:50%; background:#FFD24D; color:#333; font-weight:800; font-size:13px; margin-right:8px; }
|
||
.closebar { position:absolute; bottom:30px; left:0; right:0; text-align:center; font-size:14px; opacity:.7; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="logo">🛒</div>
|
||
<h1>傻瓜比价</h1>
|
||
<div class="slogan">买什么都先比一比<br>自动帮你找全网最低价</div>
|
||
|
||
<div class="feats">
|
||
<div class="feat">🍔 <span>点外卖前一键比价,<b>美团/京东/淘宝</b>到手价一目了然</span></div>
|
||
<div class="feat">🎟️ <span>自动领遍各平台<b>红包券</b>,能省的一分不漏</span></div>
|
||
<div class="feat">💰 <span>省下的钱看得见,还能<b>赚金币提现</b></span></div>
|
||
</div>
|
||
|
||
<button class="btn" id="dlbtn">⬇️ 下载安装包</button>
|
||
<div class="hint" id="hint">Android 安卓版 · 安装包约 24 MB</div>
|
||
|
||
<div class="foot">
|
||
安装时如提示「未知来源」,请允许后继续安装。<br>
|
||
本页为内部测试页。
|
||
</div>
|
||
|
||
<!-- 微信内引导:跳出微信去浏览器 -->
|
||
<div id="wxmask">
|
||
<svg class="arrow" viewBox="0 0 120 130" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M30 120 C 30 70, 55 40, 95 28" stroke="#FFD24D" stroke-width="6" stroke-linecap="round" fill="none" stroke-dasharray="2 13"/>
|
||
<path d="M95 28 L 78 30 M95 28 L 92 46" stroke="#FFD24D" stroke-width="6" stroke-linecap="round"/>
|
||
</svg>
|
||
<div class="wxtip">
|
||
<div class="big">点击右上角 <em>···</em><br>选择「<em>在浏览器打开</em>」</div>
|
||
<div class="sub">微信里无法直接下载安装包<br>需在系统浏览器中完成下载</div>
|
||
<div class="wxsteps">
|
||
<div><span class="n">1</span>点右上角的 ··· 菜单</div>
|
||
<div><span class="n">2</span>选择「在浏览器打开」</div>
|
||
<div><span class="n">3</span>在浏览器里点「下载安装包」</div>
|
||
</div>
|
||
</div>
|
||
<div class="closebar" id="wxclose">我知道了 ✕</div>
|
||
</div>
|
||
|
||
<script>
|
||
// APK 下载地址跟随当前页面 origin:本地测试时页面由笔记本 app-server(LAN:8770)托管 →
|
||
// 下载也走 LAN;生产由 app-api.shaguabijia.com 托管 → 下载走生产域名。无需按机器改 IP。
|
||
var APK_URL = location.origin + "/media/shaguabijia.apk";
|
||
var ua = navigator.userAgent || "";
|
||
var isWeChat = /MicroMessenger/i.test(ua);
|
||
var isIOS = /iPhone|iPad|iPod/i.test(ua);
|
||
var isAndroid = /Android/i.test(ua);
|
||
var ref = new URLSearchParams(location.search).get("ref"); // 邀请码(来自二维码 URL ?ref=)
|
||
|
||
// 【任务 3】指纹归因兜底:页面加载即上报访问者指纹,后端存 invite_fingerprint 表。
|
||
// 当 APK 首启读剪贴板失败(被覆盖)时,客户端会用 (IP+屏幕+UA 解析的手机型号) 反查
|
||
// 本表 7 天内最近一条匹配 → 撞库出原邀请人 → 走原 bind 流程。
|
||
// 任何失败都 silent(不影响下载主流程);后端 invalid_code/no_ip 也只返 200。
|
||
if (ref) {
|
||
// screen 报【物理像素】= CSS 像素 × devicePixelRatio,跟 Android dm.widthPixels(物理像素)对齐。
|
||
// 不同设备 DPR 不同(常见 2/2.5/3/3.5),CSS 像素直接报会跟客户端不对齐 → 撞不上库。
|
||
var _dpr = window.devicePixelRatio || 1;
|
||
var _sw = Math.round(screen.width * _dpr);
|
||
var _sh = Math.round(screen.height * _dpr);
|
||
fetch("/api/v1/invite/landing-track", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
ref: ref,
|
||
screen: _sw + "x" + _sh,
|
||
// IP / UA 服务端从 HTTP 头自动拿,无需 JS 上报
|
||
}),
|
||
}).catch(function () {}); // silent,绝不阻断下载
|
||
}
|
||
|
||
// 把邀请码写进剪贴板,APK 首启时读出来完成归因(deferred deeplink 的关键一步)。
|
||
// 浏览器要求:必须在用户点击手势里调用 + HTTPS 下才允许写。
|
||
function legacyCopy(payload) { // 老 webview / 无 clipboard API 兜底
|
||
try {
|
||
var ta = document.createElement("textarea");
|
||
ta.value = payload; ta.style.position = "fixed"; ta.style.top = "-1000px"; ta.style.opacity = "0";
|
||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||
document.execCommand("copy"); document.body.removeChild(ta);
|
||
} catch (e) {}
|
||
}
|
||
function copyInviteCode() { // 返回 Promise(完成后才下载,避免异步写入被打断)
|
||
if (!ref) return Promise.resolve();
|
||
var payload = "SGBJ_INVITE:" + ref;
|
||
legacyCopy(payload); // 同步兜底:在用户手势内立刻 execCommand 写一次(最可靠)
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
return navigator.clipboard.writeText(payload).catch(function () {}); // 现代 API 锦上添花,失败无妨
|
||
}
|
||
return Promise.resolve();
|
||
}
|
||
// 确保剪贴板写完(或最多等 400ms 兜底,防写入卡住)再触发下载。
|
||
function startDownload() {
|
||
var fired = false;
|
||
function go() { if (!fired) { fired = true; window.location.href = APK_URL; } }
|
||
copyInviteCode().then(go);
|
||
setTimeout(go, 400);
|
||
}
|
||
|
||
var hint = document.getElementById("hint");
|
||
if (isIOS) hint.textContent = "检测到 iPhone · iOS 版请前往 App Store";
|
||
|
||
var mask = document.getElementById("wxmask");
|
||
function showMask(){ mask.classList.add("show"); }
|
||
function hideMask(){ mask.classList.remove("show"); }
|
||
document.getElementById("wxclose").addEventListener("click", hideMask);
|
||
|
||
// 微信里一进页面就提示去浏览器打开(下载在微信内必被拦)
|
||
if (isWeChat) showMask();
|
||
|
||
document.getElementById("dlbtn").addEventListener("click", function(){
|
||
if (isWeChat) { showMask(); return; } // 微信内:引导去浏览器
|
||
if (isIOS) { alert("iOS 版即将上线,请前往 App Store 搜索「傻瓜比价」"); return; }
|
||
// 普通浏览器:先把邀请码写进剪贴板(写完或 400ms 兜底),再下载 APK(首启读回完成归因)
|
||
startDownload();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|