This commit is contained in:
eric
2026-03-16 09:32:35 -05:00
parent c54c236d1b
commit 14b0cd2429
21 changed files with 1058 additions and 706 deletions

View File

@@ -66,14 +66,13 @@ function buildQrHtml(
imgSrc: string,
returnUrl: string,
orderId: string,
opts?: { channel?: string; successDesc?: string; confirmUrl?: string }
opts?: { channel?: string; confirmUrl?: string }
): string {
const ch = (opts?.channel || "wxpay").toLowerCase();
const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat";
const title = isWx ? "微信扫码支付" : "支付宝扫码支付";
const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付";
const successDesc = opts?.successDesc || "支付成功,即将跳转";
const confirmUrl = opts?.confirmUrl || "/api/salon/complete-order";
const confirmUrl = opts?.confirmUrl || "/api/pay/complete-order";
const esc = (s: string) =>
s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] || c);
return `<!DOCTYPE html>
@@ -96,14 +95,6 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
.countdown-num{font-size:1rem;font-weight:700;color:#1e293b;line-height:1;}
.countdown-unit{font-size:9px;color:#94a3b8;}
.tip{font-size:13px;color:#64748b;margin-top:16px;}
.success-wrap{display:none;}
.success-wrap.show{display:block;}
.wait-wrap.hide{display:none;}
.success-icon{font-size:4rem;margin-bottom:16px;animation:scaleIn .4s ease;}
.success-title{font-size:1.25rem;color:#16a34a;font-weight:600;margin-bottom:8px;}
.success-desc{color:#64748b;font-size:14px;margin-bottom:24px;}
.success-countdown{font-size:1.5rem;font-weight:700;color:#667eea;}
@keyframes scaleIn{from{transform:scale(0);opacity:0;}to{transform:scale(1);opacity:1;}}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.7;}}
.pulse{animation:pulse 2s ease-in-out infinite;}
</style></head>
@@ -129,12 +120,6 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
</div>
<p class="tip pulse" id="tip">等待支付中...</p>
</div>
<div class="success-wrap" id="successWrap">
<div class="success-icon">✅</div>
<div class="success-title">支付成功</div>
<div class="success-desc">${esc(successDesc)}</div>
<div class="success-countdown" id="successCountdown">3 秒后跳转</div>
</div>
</div>
<script>
(function(){
@@ -143,11 +128,9 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
var returnUrl=${JSON.stringify(returnUrl)};
if(!orderId){return;}
var waitWrap=document.getElementById("waitWrap");
var successWrap=document.getElementById("successWrap");
var tip=document.getElementById("tip");
var countdownNum=document.getElementById("countdownNum");
var progressCircle=document.getElementById("progressCircle");
var successCountdown=document.getElementById("successCountdown");
var maxT=300,left=maxT;
var circumference=2*Math.PI*32;
function fmt(t){var m=Math.floor(t/60),s=t%60;return m+":"+(s<10?"0":"")+s;}
@@ -159,36 +142,46 @@ h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
},1000);
function showSuccess(){
clearInterval(countdownTimer);
waitWrap.classList.add("hide");
successWrap.classList.add("show");
var s=3;
var t=setInterval(function(){
s--;
successCountdown.textContent=s>0?s+" 秒后跳转":"跳转中…";
if(s<=0){clearInterval(t);window.location.href=returnUrl;}
},1000);
window.location.href=returnUrl;
}
var confirmUrl=${JSON.stringify(confirmUrl)};
var confirmTimeoutMs=8000;
function doConfirm(attempt){
if(attempt>=3){showSuccess();return;}
fetch(confirmUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:orderId})})
if(tip){tip.textContent="检测到支付,正在确认…";}
var ctrl=new AbortController();
var tid=setTimeout(function(){ctrl.abort();},confirmTimeoutMs);
fetch(confirmUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:orderId}),signal:ctrl.signal})
.then(function(r){return r.json();})
.then(function(cd){
clearTimeout(tid);
if(cd&&cd.ok){showSuccess();}
else{setTimeout(function(){doConfirm(attempt+1);},800);}
})
.catch(function(){setTimeout(function(){doConfirm(attempt+1);},800);});
.catch(function(){clearTimeout(tid);if(tip){tip.textContent="确认失败,重试中…";}setTimeout(function(){doConfirm(attempt+1);},800);});
}
function check(){
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store"})
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store",credentials:"same-origin"})
.then(function(r){return r.json();})
.then(function(d){
if(d.ok&&d.paid){doConfirm(0);return;}
setTimeout(check,500);
if(d&&d.paid){doConfirm(0);return;}
setTimeout(check,300);
})
.catch(function(){setTimeout(check,500);});
.catch(function(){setTimeout(check,1000);});
}
setTimeout(check,500);
var manualBtn=document.createElement("button");
manualBtn.textContent="支付完成?点此手动检查";
manualBtn.style.cssText="margin-top:12px;padding:8px 16px;font-size:13px;color:#64748b;background:#f1f5f9;border:none;border-radius:8px;cursor:pointer";
manualBtn.onclick=function(){
manualBtn.disabled=true;
manualBtn.textContent="检查中…";
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store",credentials:"same-origin"})
.then(function(r){return r.json();})
.then(function(d){if(d&&d.paid){doConfirm(0);}else{manualBtn.disabled=false;manualBtn.textContent="支付完成?点此手动检查";}})
.catch(function(){manualBtn.disabled=false;manualBtn.textContent="检查失败,重试";});
};
if(tip&&tip.parentNode){tip.parentNode.appendChild(manualBtn);}
setTimeout(check,300);
})();
</script>
</body></html>`;
@@ -401,8 +394,7 @@ export async function POST(request: NextRequest) {
const rawOrderId = String(resData.order_id || created.orderId).trim();
const html = buildQrHtml(imgSrc, safeReturnUrl, rawOrderId, {
channel: resData.channel || "wxpay",
successDesc: "支付成功,申请已提交",
confirmUrl: "/api/salon/complete-order",
confirmUrl: "/api/pay/complete-order",
});
const response = new NextResponse(html, {
status: 200,