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:
@@ -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 开通 =====
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user