feat(pay): content cashier coupon and rebate discount chain

Wire zibll-style cash content discounts: VIP list price → buyer 推广折扣 → coupon, then commission on final pay. Reuse AKSRT_Coupon, add Invite::get_referrer, cashier UI, and paid-time coupon burn + rebate_log.
This commit is contained in:
kate522 committed 2026-07-14 19:38:05 +08:00
1 parent 9eaddce457
commit 5d7b138c2c
11 files changed
+724 -22

No files matched your search

+97 -13
View File
@@ -366,23 +366,36 @@
};
}
function openCashier(postId, returnFocusEl, priceType) {
function buildPreviewUrl(postId, priceType, couponCode) {
var previewUrl = REST_BASE + '/pay/content/' + postId;
var qs = [];
if (priceType) qs.push('price_type=' + encodeURIComponent(priceType));
if (couponCode) qs.push('coupon_code=' + encodeURIComponent(couponCode));
if (qs.length) previewUrl += '?' + qs.join('&');
return previewUrl;
}
function fetchPreview(postId, priceType, couponCode) {
return fetch(buildPreviewUrl(postId, priceType, couponCode), {
method: 'GET',
credentials: 'same-origin',
headers: restHeaders(),
}).then(function (resp) {
return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; });
});
}
function openCashier(postId, returnFocusEl, priceType, couponCode) {
if (!postId) return;
priceType = priceType || '';
couponCode = couponCode || '';
open(
'<h3 class="aksrt-dl-modal__title">' + tt('cashier_loading', '确认购买') + '</h3>' +
'<p class="aksrt-dl-modal__msg">' + tt('loading', '加载中…') + '</p>',
returnFocusEl
);
var previewUrl = REST_BASE + '/pay/content/' + postId;
if (priceType) previewUrl += (previewUrl.indexOf('?') >= 0 ? '&' : '?') + 'price_type=' + encodeURIComponent(priceType);
fetch(previewUrl, {
method: 'GET',
credentials: 'same-origin',
headers: restHeaders(),
})
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; }); })
fetchPreview(postId, priceType, couponCode)
.then(function (r) {
if (!r.ok || (r.json && r.json.code)) {
const code = r.json && r.json.code ? r.json.code : '';
@@ -404,6 +417,7 @@
}
var prev = r.json || {};
if (priceType) prev._price_type = priceType;
if (couponCode) prev._coupon_code = couponCode;
renderCashier(prev, returnFocusEl);
})
.catch(function () {
@@ -430,7 +444,8 @@
const priceType = (preview._price_type || preview.price_type || '');
let amountLabel = escapeHtml(preview.amount_label || '');
let baseLabel = escapeHtml(preview.base_label || '');
if (priceType === 'download_limit_over' && preview.download_limit_over_label) {
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
if (priceType === 'download_limit_over' && preview.download_limit_over_label && !preview.discounts) {
amountLabel = escapeHtml(preview.download_limit_over_label);
baseLabel = amountLabel;
preview.amount = preview.download_limit_over_price;
@@ -440,14 +455,25 @@
const typeLabel = escapeHtml(preview.pay_type_label || '');
const balance = typeof preview.balance === 'number' ? preview.balance : 0;
const methods = Array.isArray(preview.payment_methods) ? preview.payment_methods : [];
const couponAllowed = !isPoints && !!preview.coupon_allowed;
const couponCodeVal = escapeAttr(preview.coupon_code || preview._coupon_code || '');
let priceRows = '';
if (!preview.pay_limit) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('price', '价格') + '</span><b>' + baseLabel + '</b></div>';
}
if (preview.amount !== preview.base) {
// VIP list price row when VIP reduced list amount.
if (typeof preview.list_amount === 'number' && typeof preview.base === 'number' && preview.list_amount !== preview.base) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('vip_price', '会员价') + '</span><b class="is-sale">' + listLabel + '</b></div>';
} else if (preview.amount !== preview.base && !preview.rebate_discount && !preview.coupon_discount) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('vip_price', '会员价') + '</span><b class="is-sale">' + amountLabel + '</b></div>';
}
if (preview.rebate_discount > 0) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('rebate_discount', '推广折扣') + '</span><b class="is-sale">' + escapeHtml(preview.rebate_discount_label || ('-' + preview.rebate_discount)) + '</b></div>';
}
if (preview.coupon_discount > 0) {
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('coupon_discount', '优惠码') + '</span><b class="is-sale">' + escapeHtml(preview.coupon_discount_label || ('-' + preview.coupon_discount)) + '</b></div>';
}
priceRows += '<div class="aksrt-cashier__row"><span>' + tt('pay_amount', '支付金额') + '</span><b class="is-sale">' + amountLabel + '</b></div>';
let body = '';
@@ -470,6 +496,23 @@
body += '<button type="button" class="btn-primary aksrt-dl-modal__download" data-aksrt-pay-submit data-mode="points" data-price-type="' + escapeAttr(priceType) + '">' + tt('pay_now', '立即支付') + ' ' + amountLabel + '</button>';
body += '</div>';
} else {
if (couponAllowed) {
body += '<div class="aksrt-cashier__coupon">';
body += '<div class="aksrt-cashier__methods-title">' + tt('use_coupon', '使用优惠码') + '</div>';
body += '<div class="aksrt-cashier__coupon-row">';
body += '<input type="text" class="aksrt-cashier__coupon-input" name="aksrt_coupon_code" value="' + couponCodeVal + '" placeholder="' + escapeAttr(tt('enter_coupon', '请输入优惠码')) + '" autocomplete="off">';
body += '<button type="button" class="btn-sm" data-aksrt-check-coupon>' + tt('check_coupon', '检查优惠码') + '</button>';
body += '</div>';
if (preview.coupon_desc) {
body += '<div class="aksrt-cashier__coupon-desc">' + escapeHtml(preview.coupon_desc) + '</div>';
}
if (preview.coupon_error) {
body += '<div class="aksrt-cashier__warn">' + escapeHtml(preview.coupon_error) + '</div>';
} else if (preview.coupon_discount > 0) {
body += '<div class="aksrt-cashier__coupon-ok">' + tt('coupon_ok', '优惠码可用') + '</div>';
}
body += '</div>';
}
let methodHtml = '';
methods.forEach(function (m, idx) {
const id = escapeAttr(m.id || '');
@@ -486,6 +529,39 @@
open('<h3 class="aksrt-dl-modal__title">' + tt('confirm_buy', '确认购买') + '</h3>' + body, returnFocusEl);
const checkBtn = document.querySelector('[data-aksrt-check-coupon]');
if (checkBtn) {
checkBtn.addEventListener('click', function () {
const input = document.querySelector('input[name="aksrt_coupon_code"]');
const code = input ? String(input.value || '').trim() : '';
if (!code) {
if (input) input.focus();
return;
}
checkBtn.disabled = true;
const old = checkBtn.innerHTML;
checkBtn.innerHTML = tt('checking', '检查中…');
fetchPreview(preview.post_id, priceType, code)
.then(function (r) {
checkBtn.disabled = false;
checkBtn.innerHTML = old;
if (!r.ok || (r.json && r.json.code && !r.json.amount && r.json.amount !== 0)) {
renderError((r.json && r.json.message) || tt('default_err', '请稍后重试'), (r.json && r.json.data) || {}, returnFocusEl, (r.json && r.json.code) || '');
return;
}
var prev = r.json || {};
if (priceType) prev._price_type = priceType;
prev._coupon_code = code;
renderCashier(prev, returnFocusEl);
})
.catch(function () {
checkBtn.disabled = false;
checkBtn.innerHTML = old;
renderError(tt('network_err', '网络错误,请稍后重试'), {}, returnFocusEl);
});
});
}
const submitBtn = document.querySelector('[data-aksrt-pay-submit]');
if (submitBtn) {
submitBtn.addEventListener('click', function () {
@@ -493,12 +569,14 @@
let paymentMethod = 'balance';
const checked = document.querySelector('input[name="aksrt_pay_method"]:checked');
if (checked) paymentMethod = checked.value;
submitPurchase(preview.post_id, mode, paymentMethod, submitBtn, returnFocusEl);
const input = document.querySelector('input[name="aksrt_coupon_code"]');
const code = input ? String(input.value || '').trim() : (preview.coupon_code || preview._coupon_code || '');
submitPurchase(preview.post_id, mode, paymentMethod, submitBtn, returnFocusEl, code);
});
}
}
function submitPurchase(postId, mode, paymentMethod, button, returnFocusEl) {
function submitPurchase(postId, mode, paymentMethod, button, returnFocusEl, couponCode) {
if (!postId) return;
const original = button.innerHTML;
button.disabled = true;
@@ -508,6 +586,7 @@
if (mode === 'cash') payload.payment_method = paymentMethod || 'balance';
const pt = button.getAttribute('data-price-type') || '';
if (pt) payload.price_type = pt;
if (mode === 'cash' && couponCode) payload.coupon_code = couponCode;
fetch(REST_BASE + '/pay/content/' + postId, {
method: 'POST',
@@ -627,6 +706,11 @@
'.aksrt-cashier__methods{margin:0 0 14px;}' +
'.aksrt-cashier__methods-title{font-size:13px;font-weight:600;color:#374151;margin:0 0 8px;}' +
'.aksrt-cashier__method{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;margin:0 0 6px;cursor:pointer;font-size:13px;}' +
'.aksrt-cashier__coupon{margin:0 0 14px;}' +
'.aksrt-cashier__coupon-row{display:flex;gap:8px;align-items:center;}' +
'.aksrt-cashier__coupon-input{flex:1;min-width:0;padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;}' +
'.aksrt-cashier__coupon-desc{margin-top:6px;font-size:12px;color:#9ca3af;line-height:1.5;}' +
'.aksrt-cashier__coupon-ok{margin-top:6px;font-size:12px;color:#059669;}' +
'.resource-download-box__attrs{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:6px;}' +
'.resource-download-box__attrs-item{display:flex;justify-content:space-between;gap:12px;font-size:12px;color:#6b7280;}' +
'.resource-download-box__attrs-name{flex:0 0 auto;}' +