feat(affiliate): attribution modes, settlement hardening, trusted proxies, QR and poster

- 归因模式可配置(first_touch/last_touch)、停佣开关、懒结算与佣金基数降级复查
- aksrt_get_client_ip 接入 aksrt_trusted_proxies 信任代理名单,防 CDN 头伪造
- 收益页 Canvas 二维码 + 750x1000 推广海报(本地打包 qrcode.min.js,无 CDN)
- 契约测试固化归因/海报/信任代理链路
This commit is contained in:
2026-09-21 20:53:22 +08:00
parent 2618530afb
commit 20387fdb06
14 changed files with 715 additions and 6 deletions
+179
View File
@@ -137,6 +137,8 @@
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
var promoters = root.querySelector('[data-affiliate-promoters]'); if (promoters) promoters.textContent = String(stats.promoters || 0);
var payers = root.querySelector('[data-affiliate-payers]'); if (payers) payers.textContent = String(stats.effective_payers || 0);
renderAffiliateQr(root, j.url || '');
initAffiliatePoster(root, j.url || '', j.poster || {}, j.site_name || document.title);
}).catch(function () { setFeedback('推广数据加载失败。', true); });
if (copy) copy.addEventListener('click', function () {
if (!input || !input.value) return;
@@ -146,6 +148,183 @@
});
}
// ===== Referral QR code (bundled qrcode-generator, no CDN) =====
function renderAffiliateQr(root, url) {
var row = root.querySelector('[data-affiliate-qr-row]');
var canvas = root.querySelector('[data-affiliate-qr]');
if (!row || !canvas) return;
if (!url || typeof window.qrcode !== 'function') {
row.hidden = true;
return;
}
var feedback = root.querySelector('[data-affiliate-qr-feedback]');
function setQrFeedback(text, error) { if (feedback) { feedback.textContent = text; feedback.classList.toggle('is-error', !!error); } }
try {
var qr = window.qrcode(0, 'M');
qr.addData(url);
qr.make();
var count = qr.getModuleCount();
var cell = 4, margin = 4;
var size = (count + margin * 2) * cell;
canvas.width = size;
canvas.height = size;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#0f172a';
for (var r = 0; r < count; r++) {
for (var c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
ctx.fillRect((c + margin) * cell, (r + margin) * cell, cell, cell);
}
}
}
} catch (e) {
row.hidden = true;
return;
}
var download = root.querySelector('[data-affiliate-qr-download]');
if (download) download.addEventListener('click', function () {
try {
var link = document.createElement('a');
link.download = 'referral-qr.png';
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
link.remove();
setQrFeedback('二维码已下载。');
} catch (err) {
setQrFeedback('二维码下载失败,请截图保存。', true);
}
});
}
// ===== Referral poster (canvas: hero image + copy + QR + link) =====
function initAffiliatePoster(root, url, poster, siteName) {
var button = root.querySelector('[data-affiliate-poster]');
if (!button) return;
if (!url || !poster || poster.enabled !== true) return;
button.hidden = false;
button.addEventListener('click', function () {
var feedback = root.querySelector('[data-affiliate-qr-feedback]');
function setPosterFeedback(text, error) { if (feedback) { feedback.textContent = text; feedback.classList.toggle('is-error', !!error); } }
if (typeof window.qrcode !== 'function') { setPosterFeedback('二维码组件未加载。', true); return; }
button.disabled = true;
buildPosterCanvas(url, poster, siteName).then(function (canvas) {
var link = document.createElement('a');
link.download = 'referral-poster.png';
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
link.remove();
setPosterFeedback('推广海报已生成并下载。');
}).catch(function () {
setPosterFeedback('海报生成失败,请重试。', true);
}).finally(function () {
button.disabled = false;
});
});
}
function buildPosterCanvas(url, poster, siteName) {
var width = 750, height = 1000;
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
var loadImage = null;
if (poster.image) {
loadImage = new Promise(function (resolve) {
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { resolve(null); };
image.src = poster.image;
});
} else {
loadImage = Promise.resolve(null);
}
return loadImage.then(function (image) {
// 顶部图(cover 裁切)或纯色占位。
if (image) {
var ratio = Math.max(width / image.width, 360 / image.height);
var drawWidth = image.width * ratio;
var drawHeight = image.height * ratio;
ctx.drawImage(image, (width - drawWidth) / 2, 0, drawWidth, drawHeight, 0, 0, width, 360);
} else {
ctx.fillStyle = '#0f172a';
ctx.fillRect(0, 0, width, 200);
}
var cursorY = image ? 420 : 280;
ctx.fillStyle = '#0f172a';
ctx.font = 'bold 38px sans-serif';
var title = String(poster.title || siteName || '');
var titleLines = wrapText(ctx, title, width - 80).slice(0, 2);
for (var t = 0; t < titleLines.length; t++) {
ctx.fillText(titleLines[t], 40, cursorY);
cursorY += 56;
}
cursorY += 8;
var description = String(poster.description || '');
if (description) {
ctx.fillStyle = '#475569';
ctx.font = '26px sans-serif';
var lines = wrapText(ctx, description, width - 80, 26).slice(0, 3);
for (var i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 40, cursorY);
cursorY += 40;
}
}
// 二维码 + 链接 + 站点名。
var qr = window.qrcode(0, 'M');
qr.addData(url);
qr.make();
var count = qr.getModuleCount();
var cell = 6, margin = 4;
var qrSize = (count + margin * 2) * cell;
var qrY = height - qrSize - 130;
ctx.fillStyle = '#ffffff';
ctx.fillRect(40 - 8, qrY - 8, qrSize + 16, qrSize + 16);
ctx.strokeStyle = '#e2e8f0';
ctx.strokeRect(40 - 8, qrY - 8, qrSize + 16, qrSize + 16);
ctx.fillStyle = '#0f172a';
for (var r = 0; r < count; r++) {
for (var c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
ctx.fillRect(40 + (c + margin) * cell, qrY + (r + margin) * cell, cell, cell);
}
}
}
ctx.fillStyle = '#475569';
ctx.font = '24px sans-serif';
ctx.fillText('扫描二维码访问', 40 + qrSize + 40, qrY + 40);
ctx.fillStyle = '#0f172a';
ctx.font = 'bold 28px sans-serif';
ctx.fillText(String(siteName || ''), 40 + qrSize + 40, qrY + 90);
ctx.fillStyle = '#94a3b8';
ctx.font = '20px sans-serif';
ctx.fillText(url.slice(0, 60), 40, height - 40);
return canvas;
});
}
function wrapText(ctx, text, maxWidth) {
var lines = [];
var current = '';
for (var i = 0; i < text.length; i++) {
if (ctx.measureText(current + text[i]).width > maxWidth && current) {
lines.push(current);
current = text[i];
} else {
current += text[i];
}
}
if (current) lines.push(current);
return lines;
}
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
// ===== VIP 开通 =====
File diff suppressed because one or more lines are too long