补单逻辑
Build docker and publish / build (20.15.1) (push) Has been cancelled

This commit is contained in:
2026-05-06 04:59:04 -07:00
parent 463d3e2315
commit 54daa923da
18 changed files with 15992 additions and 2 deletions
+378
View File
@@ -0,0 +1,378 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<meta name="format-detection" content="telephone=no,email=no" />
<title>订单订阅恢复</title>
<style>
:root {
color-scheme: light;
--bg: #f5f7fb;
--panel: #ffffff;
--text: #162033;
--muted: #667085;
--line: #d8dee9;
--primary: #2563eb;
--primary-pressed: #1d4ed8;
--danger: #d92d20;
--success: #047857;
--shadow: 0 18px 48px rgba(15, 23, 42, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 50% -10%, rgba(37, 99, 235, 0.16), transparent 34rem),
var(--bg);
color: var(--text);
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
.page {
min-height: 100vh;
padding: max(20px, env(safe-area-inset-top)) 16px
max(24px, env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: center;
}
.shell {
width: min(100%, 440px);
}
.brand {
margin-bottom: 18px;
text-align: center;
}
.brand h1 {
margin: 0;
font-size: 24px;
line-height: 1.25;
letter-spacing: 0;
}
.brand p {
margin: 8px 0 0;
color: var(--muted);
font-size: 14px;
line-height: 1.6;
}
.card {
border: 1px solid rgba(216, 222, 233, 0.86);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
padding: 20px;
}
.field {
display: grid;
gap: 8px;
margin-bottom: 16px;
}
label {
font-size: 14px;
font-weight: 650;
}
input {
width: 100%;
height: 46px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0 13px;
color: var(--text);
background: #fff;
font-size: 16px;
outline: none;
}
input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.code-row {
display: grid;
grid-template-columns: 1fr 116px;
gap: 10px;
}
button {
height: 46px;
border: 0;
border-radius: 8px;
padding: 0 14px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition:
transform 0.12s ease,
background 0.12s ease,
opacity 0.12s ease;
}
button:active {
transform: translateY(1px);
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.btn-primary {
width: 100%;
background: var(--primary);
color: #fff;
}
.btn-primary:not(:disabled):hover {
background: var(--primary-pressed);
}
.btn-secondary {
background: #e9eefb;
color: #1e40af;
}
.hint {
margin: 14px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.65;
}
.status {
display: none;
margin: 14px 0 0;
border-radius: 8px;
padding: 12px;
font-size: 14px;
line-height: 1.55;
}
.status.show {
display: block;
}
.status.success {
border: 1px solid rgba(4, 120, 87, 0.24);
background: rgba(4, 120, 87, 0.08);
color: var(--success);
}
.status.error {
border: 1px solid rgba(217, 45, 32, 0.24);
background: rgba(217, 45, 32, 0.08);
color: var(--danger);
}
@media (max-width: 380px) {
.page {
padding-left: 12px;
padding-right: 12px;
}
.card {
padding: 16px;
}
.code-row {
grid-template-columns: 1fr;
}
.btn-secondary {
width: 100%;
}
}
</style>
</head>
<body>
<main class="page">
<section class="shell" aria-labelledby="page-title">
<div class="brand">
<h1 id="page-title">订单订阅恢复</h1>
<p>填写订单号并验证邮箱,系统会为该邮箱恢复对应订阅。</p>
</div>
<form class="card" id="recovery-form">
<div class="field">
<label for="order-no">订单号</label>
<input
id="order-no"
name="order_no"
autocomplete="off"
inputmode="text"
placeholder="请输入订单号"
required
/>
</div>
<div class="field">
<label for="email">邮箱</label>
<input
id="email"
name="email"
autocomplete="email"
inputmode="email"
placeholder="请输入接收验证码的邮箱"
required
type="email"
/>
</div>
<div class="field">
<label for="code">邮箱验证码</label>
<div class="code-row">
<input
id="code"
name="code"
autocomplete="one-time-code"
inputmode="numeric"
maxlength="8"
placeholder="请输入验证码"
required
/>
<button class="btn-secondary" id="send-code" type="button">
获取验证码
</button>
</div>
</div>
<button class="btn-primary" id="submit-btn" type="submit">
恢复订阅
</button>
<div class="status" id="status" role="status"></div>
<p class="hint">
恢复成功后,请使用该邮箱验证码登录账号查看订阅。每个订单号只能恢复一次。
</p>
</form>
</section>
</main>
<script>
const form = document.getElementById("recovery-form");
const sendCodeButton = document.getElementById("send-code");
const submitButton = document.getElementById("submit-btn");
const statusBox = document.getElementById("status");
const emailInput = document.getElementById("email");
const orderInput = document.getElementById("order-no");
const codeInput = document.getElementById("code");
let countdownTimer = null;
function showStatus(type, message) {
statusBox.className = `status show ${type}`;
statusBox.textContent = message;
}
function clearStatus() {
statusBox.className = "status";
statusBox.textContent = "";
}
function normalizeEmail() {
emailInput.value = emailInput.value.trim().toLowerCase();
return emailInput.value;
}
async function postJson(path, body) {
const response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload.code !== 200) {
throw new Error(payload.msg || "请求失败,请稍后再试");
}
return payload.data || {};
}
function startCountdown(seconds) {
let remaining = seconds;
sendCodeButton.disabled = true;
sendCodeButton.textContent = `${remaining}s`;
clearInterval(countdownTimer);
countdownTimer = setInterval(() => {
remaining -= 1;
if (remaining <= 0) {
clearInterval(countdownTimer);
sendCodeButton.disabled = false;
sendCodeButton.textContent = "获取验证码";
return;
}
sendCodeButton.textContent = `${remaining}s`;
}, 1000);
}
sendCodeButton.addEventListener("click", async () => {
clearStatus();
const email = normalizeEmail();
if (!emailInput.reportValidity()) return;
sendCodeButton.disabled = true;
sendCodeButton.textContent = "发送中";
try {
await postJson("/v1/public/recovery/send_code", { email });
showStatus("success", "验证码已发送,请查看邮箱。");
startCountdown(60);
} catch (error) {
sendCodeButton.disabled = false;
sendCodeButton.textContent = "获取验证码";
showStatus("error", error.message || "验证码发送失败,请稍后再试。");
}
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
clearStatus();
const orderNo = orderInput.value.trim();
const email = normalizeEmail();
const code = codeInput.value.trim();
if (!form.reportValidity()) return;
submitButton.disabled = true;
submitButton.textContent = "恢复中";
try {
const result = await postJson("/v1/public/recovery/order", {
order_no: orderNo,
email,
code
});
showStatus(
"success",
result.message || "恢复完成,请使用该邮箱验证码登录查看订阅。"
);
form.reset();
} catch (error) {
showStatus("error", "订单或邮箱验证码无效,请确认后重试。");
} finally {
submitButton.disabled = false;
submitButton.textContent = "恢复订阅";
}
});
</script>
</body>
</html>