PaymentCenter/front/templates/payPage.html

891 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "payPage.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>收银台页面</title>
<style>
/* 基础样式 */
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
/* 段落样式 */
p {
color: #666666;
margin-bottom: 10px;
}
/* 无序列表样式 */
ul {
list-style: none;
padding: 0;
}
/* 列表项样式 */
li {
margin: 10px 0;
display: flex;
align-items: center;
}
/* 按钮样式 */
button {
background-color: #007bff;
color: #fff;
border: none;
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.3s ease;
border-radius: 4px;
font-size: 16px;
margin-top: 20px;
}
/* 按钮悬停样式 */
button:hover {
background-color: #0056b3;
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 30px;
border-radius: 10px;
text-align: center;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 400px;
}
.modal-content h3 {
margin-top: 0;
color: #333;
}
.modal-content p {
color: #666;
margin-bottom: 30px;
}
.modal-buttons {
display: flex;
justify-content: space-around;
gap: 20px;
}
.modal-button {
padding: 12px 24px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
flex: 1;
max-width: 150px;
}
.paid-button {
background-color: #28a745;
color: white;
}
.paid-button:hover {
background-color: #218838;
}
.unpaid-button {
background-color: #dc3545;
color: white;
}
.unpaid-button:hover {
background-color: #c82333;
}
/* Toast提示样式 */
.toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 5px;
color: white;
font-size: 14px;
z-index: 1001;
background-color: rgba(0, 0, 0, 0.7);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.show {
display: block;
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast.hide {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
/* loading样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.loading-spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #007bff;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
color: #666;
font-size: 18px;
}
/* 支付信息卡片 */
.payment-info {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.payment-info h2 {
margin-top: 0;
color: #333;
}
/* 支付方式选项 */
.payment-option {
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.payment-option:hover {
border-color: #007bff;
background-color: #f0f7ff;
}
.payment-option input[type="radio"] {
margin-right: 15px;
}
.payment-option.selected {
border-color: #007bff;
background-color: #f0f7ff;
}
</style>
</head>
{{if eq .code 200 }}
<body>
<!-- 页面内容 -->
<div class="payment-info" id="payment-info" style="display: none">
<h2>订单支付</h2>
<p>{{.desc}}</p>
<p>交易号:{{ .id }}</p>
<p>
交易金额:<strong style="color: #ff5500; font-size: 24px"
>{{ .amount }}</strong
>
</p>
</div>
<!-- 支付方式选择区域 -->
<div id="pay-container" style="display: none">
<div id="pay"></div>
<!-- 支付方式列表 -->
<ul id="payment-list"></ul>
</div>
<!-- Loading状态 -->
<div id="loading" class="loading-container" style="display: flex">
<div class="loading-spinner"></div>
<p class="loading-text">支付处理中,请稍等...</p>
</div>
<!-- 支付状态确认弹窗 -->
<div id="payment-status-modal" class="modal" style="display: none;">
<div class="modal-content">
<h3>支付状态确认</h3>
<p>请确认您的支付状态</p>
<div class="modal-buttons">
<button id="btn-paid" class="modal-button paid-button">已支付</button>
<button id="btn-unpaid" class="modal-button unpaid-button">未支付</button>
</div>
</div>
</div>
<!-- Toast提示 -->
<div id="toast" class="toast" style="display: none;"></div>
</body>
<script>
// 支付页面模块化实现
const API_BASE_URL = 'https://pay.cdlsxd.cn';
// ========== 配置模块 ==========
const CONFIG = {
API: {
LIST: API_BASE_URL + '/pay/front/api/v1/payPage/list',
SUBMIT: API_BASE_URL + '/pay/front/api/v1/payPage/submit',
QUERY: API_BASE_URL + '/pay/front/api/v1/payPage/query'
},
POLLING: {
MAX_ATTEMPTS: 30,
DELAY: 2000,
TIMEOUT: 60000,
REQUEST_TIMEOUT: 10000
},
LOCAL_STORAGE: {
AUTO_REDIRECT: 'auto-redirect',
AUTO_REDIRECT_TIMESTAMP: 'auto-redirect-timestamp'
},
PAYMENT_STATUS: {
PROCESSING: 2,
SUCCESS: 3
},
REDIRECT_DELAY: 3000,
// 自动跳转标记的有效期(毫秒)
REDIRECT_FLAG_EXPIRY: 10 * 60 * 1000 // 10分钟
};
// ========== 工具函数模块 ==========
const Utils = {
// 从URL中提取参数
getQueryParam(param) {
const queryString = window.location.search.substring(1);
const params = queryString.split("&");
for (let i = 0; i < params.length; i++) {
const [key, value] = params[i].split("=");
if (key === param) {
return decodeURIComponent(value);
}
}
return null;
},
// 验证订单号格式
validateOrderNumber(orderNo) {
// 订单号格式验证假设订单号由字母、数字、下划线组成长度在10-32之间
const orderNoPattern = /^[a-zA-Z0-9_]{10,32}$/;
return orderNoPattern.test(orderNo);
},
// 检查是否为同域请求
isSameOrigin(url) {
const parser = document.createElement('a');
parser.href = url;
return parser.origin === window.location.origin;
},
// 显示Loading效果
showLoading(text = "支付处理中,请稍等...") {
document.getElementById("payment-info").style.display = "none";
document.getElementById("pay-container").style.display = "none";
const loadingElement = document.getElementById("loading");
const loadingText = loadingElement.querySelector(".loading-text");
if (loadingText) {
loadingText.textContent = text;
}
loadingElement.style.display = "flex";
},
// 关闭Loading效果
closeLoading() {
document.getElementById("loading").style.display = "none";
document.getElementById("payment-info").style.display = "block";
document.getElementById("pay-container").style.display = "block";
},
// 清除支付状态标记
clearRedirectFlag() {
localStorage.removeItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT);
localStorage.removeItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT_TIMESTAMP);
},
// 设置支付状态标记
setRedirectFlag(value) {
localStorage.setItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT, value);
localStorage.setItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT_TIMESTAMP, Date.now().toString());
},
// 获取支付状态标记
getRedirectFlag() {
return localStorage.getItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT);
},
// 检查自动跳转标记是否有效
isRedirectFlagValid() {
const flag = localStorage.getItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT);
const timestampStr = localStorage.getItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT_TIMESTAMP);
if (!flag || !timestampStr) {
return false;
}
const timestamp = parseInt(timestampStr, 10);
if (isNaN(timestamp)) {
return false;
}
// 检查标记是否在有效期内
return (Date.now() - timestamp) < CONFIG.REDIRECT_FLAG_EXPIRY;
},
// 显示支付状态确认弹窗
showPaymentStatusModal() {
const modal = document.getElementById('payment-status-modal');
if (modal) {
modal.style.display = 'flex';
}
},
// 隐藏支付状态确认弹窗
hidePaymentStatusModal() {
const modal = document.getElementById('payment-status-modal');
if (modal) {
modal.style.display = 'none';
}
},
// 显示Toast提示
showToast(message, duration = 3000) {
const toast = document.getElementById('toast');
if (toast) {
toast.textContent = message;
toast.className = 'toast show';
// 自动隐藏
setTimeout(() => {
Utils.hideToast();
}, duration);
}
},
// 隐藏Toast提示
hideToast() {
const toast = document.getElementById('toast');
if (toast) {
toast.className = 'toast hide';
// 动画结束后完全隐藏
setTimeout(() => {
toast.style.display = 'none';
}, 300);
}
}
};
// ========== 轮询处理模块 ==========
const PollingManager = {
// 处理轮询超时
handleTimeout() {
Utils.closeLoading();
// 清除自动跳转标记
Utils.clearRedirectFlag();
this.showError("支付结果查询超时,请稍后手动查询订单状态。");
},
// 处理超过最大轮询次数
handleMaxAttempts() {
Utils.closeLoading();
// 清除自动跳转标记
Utils.clearRedirectFlag();
this.showError("支付结果查询次数已达上限,请稍后手动查询订单状态。");
},
// 显示轮询错误信息
showError(message) {
const payContainer = document.getElementById("pay-container");
payContainer.innerHTML = `
<div style="text-align: center; padding: 40px;">
<h3 style="color: #ff5500;">支付结果查询失败</h3>
<p style="margin: 20px 0;">${message}</p>
<div style="display: flex; justify-content: center; gap: 20px;">
<button onclick="location.reload()" style="background-color: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">
重新查询
</button>
<button onclick="Utils.clearRedirectFlag(); PaymentManager.fetchPaymentMethods()" style="background-color: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">
返回支付
</button>
</div>
</div>
`;
}
};
// ========== 支付管理模块 ==========
const PaymentManager = {
// 处理支付回调
handleCallback() {
const id = Utils.getQueryParam("no");
if (!id || !Utils.validateOrderNumber(id)) {
Utils.closeLoading();
console.error("无效的订单号");
return;
}
// 显示查询支付结果的loading
Utils.showLoading("正在查询支付结果...");
// 轮询状态
const pollState = {
attempts: 0,
startTime: Date.now(),
lastDelay: CONFIG.POLLING.DELAY,
errorCount: 0
};
// 取消请求的控制器
let abortController = null;
// 计算动态轮询间隔
function getDynamicDelay(attempts, errorCount) {
// 初始间隔较短,随着尝试次数增加而延长
let baseDelay = CONFIG.POLLING.DELAY;
// 每5次尝试增加基础间隔
const delayIncrease = Math.floor(attempts / 5) * 500;
// 网络错误时增加额外延迟
const errorDelay = errorCount * 1000;
// 计算最终延迟,不超过最大间隔
const finalDelay = Math.min(baseDelay + delayIncrease + errorDelay, 10000);
return finalDelay;
}
// 执行轮询
function pollOrderStatus() {
// 检查是否超时
if (Date.now() - pollState.startTime > CONFIG.POLLING.TIMEOUT) {
PollingManager.handleTimeout();
return;
}
// 检查是否超过最大轮询次数
if (pollState.attempts >= CONFIG.POLLING.MAX_ATTEMPTS) {
PollingManager.handleMaxAttempts();
return;
}
pollState.attempts++;
// 取消之前的请求(如果有)
if (abortController) {
abortController.abort();
}
// 创建新的取消控制器
abortController = new AbortController();
// 查询订单状态
fetch(`${CONFIG.API.QUERY}?no=${id}`, {
method: "POST",
timeout: CONFIG.POLLING.REQUEST_TIMEOUT,
signal: abortController.signal
})
.then(async (response) => {
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);
return await response.json();
})
.then((data) => {
pollState.errorCount = 0; // 重置错误计数
switch (data.status) {
case CONFIG.PAYMENT_STATUS.PROCESSING: // 处理中
// 计算动态延迟
const dynamicDelay = getDynamicDelay(pollState.attempts, pollState.errorCount);
pollState.lastDelay = dynamicDelay;
// 延迟后继续轮询
setTimeout(pollOrderStatus, dynamicDelay);
break;
case CONFIG.PAYMENT_STATUS.SUCCESS: // 支付成功
PaymentManager.handlePaymentSuccess(data);
break;
default: // 其他状态(待支付/失败/关闭)
// 显示未查到支付状态的提示
Utils.closeLoading();
Utils.showToast("未查到支付状态");
// 重新显示支付状态确认弹窗
Utils.showPaymentStatusModal();
}
})
.catch((error) => {
// 检查是否为取消错误
if (error.name === 'AbortError') {
console.log("轮询请求已取消");
return;
}
// 网络错误或请求失败
console.error("查询订单状态失败:", error);
pollState.errorCount++;
// 如果是第5次或10次失败显示网络不稳定提示
if (pollState.attempts % 5 === 0) {
const loadingText = document.querySelector('.loading-text');
if (loadingText) {
loadingText.textContent = `支付处理中,请稍等...(网络不稳定,正在重试)`;
}
}
// 计算动态延迟,网络错误时增加延迟
const dynamicDelay = getDynamicDelay(pollState.attempts, pollState.errorCount);
pollState.lastDelay = dynamicDelay;
setTimeout(pollOrderStatus, dynamicDelay);
});
}
// 开始轮询
pollOrderStatus();
},
// 处理支付成功
handlePaymentSuccess(data) {
Utils.closeLoading();
// 清除自动跳转标记,避免影响后续支付
Utils.clearRedirectFlag();
// 显示支付成功提示
const payContainer = document.getElementById("pay");
payContainer.innerHTML = `
<div style="text-align: center; padding: 40px;">
<h3 style="color: #28a745;">支付成功!</h3>
<p style="margin: 20px 0; font-size: 16px;">正在为您跳转...<span id="countdown">${Math.ceil(CONFIG.REDIRECT_DELAY / 1000)}</span></p>
<p style="color: #666; font-size: 14px;">如果没有自动跳转,请点击下方按钮</p>
<button onclick="window.location.href='${data.return_url}'" style="background-color: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; margin-top: 10px;">
立即跳转
</button>
</div>
`;
// 添加倒计时跳转
let countdown = Math.ceil(CONFIG.REDIRECT_DELAY / 1000);
const countdownElement = document.getElementById("countdown");
const countdownInterval = setInterval(() => {
countdown--;
if (countdownElement) {
countdownElement.textContent = countdown;
}
if (countdown <= 0) {
clearInterval(countdownInterval);
if (data.return_url) {
window.location.href = data.return_url;
}
}
}, 1000);
// 如果有返回URL设置跳转
if (data.return_url) {
// 设置自动跳转
setTimeout(() => {
window.location.href = data.return_url;
}, CONFIG.REDIRECT_DELAY);
}
},
// 处理支付取消
handlePaymentCanceled() {
Utils.closeLoading();
// 清除自动跳转标记,避免影响后续支付
Utils.clearRedirectFlag();
// 显示支付取消提示
const payContainer = document.getElementById("pay");
payContainer.innerHTML = `<p style="color: #666; margin-bottom: 20px;">支付已取消,您可以重新选择支付方式</p>`;
PaymentManager.fetchPaymentMethods();
},
// 获取支付方式列表
fetchPaymentMethods() {
const id = Utils.getQueryParam("no");
if (id && Utils.validateOrderNumber(id)) {
// 显示获取支付方式的loading
Utils.showLoading("正在加载支付方式...");
fetch(`${CONFIG.API.LIST}?id=${id}`, {
method: "POST"
})
.then(async (response) => {
if (response.ok) {
const data = await response.json();
console.log(data);
if (data.code !== 200) {
throw new Error("无效");
} else {
return data;
}
} else {
throw new Error("无效");
}
})
.then((data) => {
PaymentManager.processPaymentMethods(data, id);
})
.catch((error) => {
Utils.closeLoading();
// 清除自动跳转标记
Utils.clearRedirectFlag();
console.error("获取支付方式失败:", error);
const pay = document.getElementById("pay");
pay.innerHTML = `
<h3 style="color: red;">获取支付方式失败</h3>
<p style="color: #666;">请检查网络连接或刷新页面重试</p>
<button onclick="location.reload()" style="background-color: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">
刷新重试
</button>
`;
});
} else {
Utils.closeLoading();
console.error("Order no not found in URL");
}
},
// 处理支付方式数据
processPaymentMethods(data, id) {
// 处理返回的数据,例如渲染支付方式列表
if (data === null || data.data.length === 0) {
const pay = document.getElementById("pay");
pay.innerHTML = "<h3>支付环境异常,请检查</h3>";
} else if (data.data.length === 1) {
// 检查是否是从支付页面返回
if (Utils.getRedirectFlag() != 2 || !Utils.isRedirectFlagValid()) {
// 首次访问或标记已失效,且只有一种支付方式,自动跳转
Utils.setRedirectFlag(2);
window.location.href = `${CONFIG.API.SUBMIT}?pay_channel_id=${data.data[0].pay_channel_id}&no=${id}`;
} else {
// 从支付页面返回,显示支付状态确认弹窗
Utils.closeLoading();
Utils.showPaymentStatusModal();
}
} else {
// 多种支付方式,先检查是否从支付页面返回
if (Utils.getRedirectFlag() == 2 && Utils.isRedirectFlagValid()) {
// 从支付页面返回,显示支付状态确认弹窗
Utils.closeLoading();
Utils.showPaymentStatusModal();
} else {
Utils.closeLoading();
// 首次访问或标记已失效,展示支付界面
PaymentManager.renderPaymentMethods(data.data);
}
}
},
// 渲染支付方式列表
renderPaymentMethods(paymentMethods) {
const pay = document.getElementById("pay");
pay.innerHTML = "<h3>请选择支付方式</h3>";
const paymentList = document.getElementById("payment-list");
paymentList.innerHTML = "";
// 标记是否是第一个支付方式
let isFirstMethod = true;
paymentMethods.forEach((method) => {
const listItem = document.createElement("li");
listItem.className = "payment-option";
const radioInput = document.createElement("input");
radioInput.type = "radio";
radioInput.name = "paymentMethod";
radioInput.value = method.pay_channel_id;
radioInput.id = `method-${method.pay_channel_id}`;
// 如果是第一个支付方式,默认选中
if (isFirstMethod) {
radioInput.checked = true;
listItem.classList.add("selected");
isFirstMethod = false;
}
const label = document.createElement("label");
label.htmlFor = `method-${method.pay_channel_id}`;
label.textContent = method.pay_name;
if (method.icon_url) {
const icon = document.createElement("img");
icon.src = method.icon_url;
icon.style.height = "24px";
icon.style.marginRight = "10px";
label.prepend(icon);
}
listItem.appendChild(radioInput);
listItem.appendChild(label);
paymentList.appendChild(listItem);
// 点击整个区域也可以选择
listItem.addEventListener("click", () => {
// 移除所有选中样式
document.querySelectorAll(".payment-option").forEach((item) => {
item.classList.remove("selected");
});
// 添加当前选中样式
listItem.classList.add("selected");
radioInput.checked = true;
});
});
// 添加提交按钮
const submitButton = document.createElement("button");
submitButton.type = "button";
submitButton.textContent = "立即支付";
submitButton.onclick = function () {
PaymentManager.submitPayment();
};
const buttonContainer = document.createElement("div");
buttonContainer.style.textAlign = "center";
buttonContainer.appendChild(submitButton);
paymentList.appendChild(buttonContainer);
},
// 提交支付
submitPayment() {
const no = Utils.getQueryParam("no");
const selectedMethod = document.querySelector(
'input[name="paymentMethod"]:checked'
);
if (selectedMethod) {
// 显示加载状态
Utils.showLoading();
// 尝试导航到支付页面,如果失败则显示错误
try {
Utils.setRedirectFlag(2);
window.location.href = `${CONFIG.API.SUBMIT}?pay_channel_id=${selectedMethod.value}&no=${no}`;
// 设置超时检查,如果长时间未跳转则显示错误
setTimeout(() => {
Utils.closeLoading();
// 清除自动跳转标记,避免影响后续支付
Utils.clearRedirectFlag();
const payContainer = document.getElementById("pay");
payContainer.innerHTML = `
<h3 style="color: red;">支付提交超时</h3>
<p style="color: #666;">请检查网络连接并重试</p>
<button onclick="location.reload()" style="background-color: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">
重新提交
</button>
`;
}, 5000);
} catch (error) {
Utils.closeLoading();
// 清除自动跳转标记,避免影响后续支付
Utils.clearRedirectFlag();
alert("支付提交失败,请重试");
console.error("支付提交失败:", error);
}
} else {
alert("请选择支付方式");
}
}
};
// 监听页面可见性变化
document.addEventListener("visibilitychange", function () {
if (!document.hidden && localStorage.getItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT) == 2 && Utils.isRedirectFlagValid()) {
// 当页面从隐藏变为可见,且有有效标记时,显示支付状态确认弹窗
// 这样无论用户是支付成功后切换回来,还是点击完成跳回来,都能让用户确认支付状态
Utils.closeLoading();
Utils.showPaymentStatusModal();
}
});
// 页面加载时执行
window.onload = function () {
// 检查是否有return参数或已尝试支付的标记
if (Utils.getQueryParam("return") || (localStorage.getItem(CONFIG.LOCAL_STORAGE.AUTO_REDIRECT) == 2 && Utils.isRedirectFlagValid())) {
// 用户从支付页面返回,显示支付状态确认弹窗
Utils.closeLoading();
Utils.showPaymentStatusModal();
} else {
// 首次访问或标记已失效,清除可能的旧标记并设置新标记
Utils.clearRedirectFlag();
Utils.setRedirectFlag(1);
PaymentManager.fetchPaymentMethods(); // 获取支付方式
}
// 绑定弹窗按钮事件
document.getElementById('btn-paid').addEventListener('click', function() {
// 显示加载状态
Utils.showLoading("正在查询支付结果...");
// 隐藏弹窗
Utils.hidePaymentStatusModal();
// 查询订单状态
PaymentManager.handleCallback();
});
document.getElementById('btn-unpaid').addEventListener('click', function() {
// 隐藏弹窗
Utils.hidePaymentStatusModal();
// 清除支付状态标记
Utils.clearRedirectFlag();
Utils.setRedirectFlag(1);
// 重新获取支付方式
PaymentManager.fetchPaymentMethods();
});
};
</script>
{{ else}}
<body>
<div class="payment-info" style="text-align: center">
<h2 style="color: #ff0000">支付异常</h2>
<p>{{.message}}</p>
</div>
</body>
{{end}}
</html>
{{end}}