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:
1 parent
2618530afb
commit
20387fdb06
14 files changed
+715
-6
No files matched your search
@@ -27,6 +27,8 @@ export const affiliateSchema = {
|
||||
pay_rebate_user_s_2: ['all'],
|
||||
pay_rebate_ratio_vip_2: 20,
|
||||
affiliate_settlement_days: 7,
|
||||
affiliate_disabled_code_stops_commission: false,
|
||||
referral_attribution_model: 'first_touch',
|
||||
pay_rebate_text_desc: '加入分享计划,获得高额奖励',
|
||||
pay_rebate_text_details_title: '返佣详解',
|
||||
pay_rebate_text_details: '<p>通过您的推广链接打开本站后,在本站购买商品即可获得佣金</p><p>通过您的推广链接注册后的用户今后购买的商品均可获得佣金</p><p>申请提现后,需后台人工处理,一般2-3小时,请耐心等待</p>',
|
||||
@@ -77,6 +79,17 @@ export const affiliateSchema = {
|
||||
},
|
||||
{ key: 'pay_rebate_show_users', label: '用户中心显示我推荐的用户', type: 'switch', default: true, dependsOn: { key: 'pay_rebate_judgment', value: 'all' } },
|
||||
{ key: 'pay_rebate_self_s', label: '自己推广自己', type: 'switch', default: true, dependsOn: { key: 'pay_rebate_s', value: true }, help: '允许使用自己的推广链接购买商品并获得佣金' },
|
||||
{
|
||||
key: 'referral_attribution_model',
|
||||
label: '推广归因模式',
|
||||
type: 'radio',
|
||||
default: 'first_touch',
|
||||
options: [
|
||||
{ label: '首次触点(首触优先)', value: 'first_touch' },
|
||||
{ label: '最近触点(末触覆盖)', value: 'last_touch' },
|
||||
],
|
||||
help: '首触:访客首次通过推广链接进入后,归因固定且不被后续链接覆盖;末触:最近一次点击的推广码覆盖之前的归因',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -140,6 +153,7 @@ export const affiliateSchema = {
|
||||
],
|
||||
},
|
||||
{ key: 'affiliate_settlement_days', label: '结算观察期(天)', type: 'number', default: 7, min: 0, max: 90, dependsOn: { key: 'pay_rebate_s', value: true }, help: '订单完成后经过观察期,佣金才会结算到可提现余额' },
|
||||
{ key: 'affiliate_disabled_code_stops_commission', label: '推广码禁用后停止返佣', type: 'switch', default: false, dependsOn: { key: 'pay_rebate_s', value: true }, help: '开启后,推广者的推广码被禁用(或缺失)时,其名下已绑定用户的新订单不再产生佣金;关闭则绑定关系继续生效' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -211,6 +211,15 @@
|
||||
.uc-content .uc-revenue-summary-row--link button { min-width: 64px; height: 38px; padding: 0 12px; border-radius: 7px; }
|
||||
.uc-content .uc-revenue-summary-row--link small { grid-column: 2 / -1; min-height: 16px; color: var(--color-success, #16a34a); font-size: 12px; }
|
||||
.uc-content .uc-revenue-summary-row--link small.is-error { color: var(--color-error, #dc2626); }
|
||||
|
||||
/* 推广二维码 — bundled qrcode canvas + download. */
|
||||
.uc-content .uc-revenue-summary-row--qr { align-items: flex-start; }
|
||||
.uc-content .uc-revenue-qr { display: grid; justify-items: start; gap: 8px; }
|
||||
.uc-content .uc-revenue-qr canvas { display: block; width: 148px; height: 148px; border: 1px solid var(--color-line-soft, rgba(15, 23, 42, .12)); border-radius: 10px; background: #fff; }
|
||||
.uc-content .uc-revenue-qr__actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.uc-content .uc-revenue-qr__actions button { min-width: 96px; height: 32px; padding: 0 12px; border-radius: 7px; }
|
||||
.uc-content .uc-revenue-qr small { min-height: 16px; color: var(--color-success, #16a34a); font-size: 12px; }
|
||||
.uc-content .uc-revenue-qr small.is-error { color: var(--color-error, #dc2626); }
|
||||
.uc-content .uc-revenue-summary-row--total > div { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.uc-content .uc-revenue-summary-row--total strong { padding: 7px 11px; border-radius: 7px; color: var(--color-primary); background: var(--surface-2, #f8fafc); font-size: 14px; }
|
||||
.uc-content .uc-revenue-summary-row--explain { align-items: start; padding-top: 16px; border-bottom: 0; }
|
||||
@@ -382,4 +391,5 @@
|
||||
.uc-content .uc-revenue-summary-row--link small { grid-column: 1 / -1; }
|
||||
.uc-content .uc-revenue-summary-row--explain { grid-template-columns: 1fr; }
|
||||
.uc-content .uc-revenue-summary-row--explain .uc-reward-richtext { grid-column: 1; }
|
||||
.uc-content .uc-revenue-summary-row--qr { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
var qrcode=function(){function i(t,r){function a(t,r){g=function(t){for(var r=new Array(t),e=0;e<t;e+=1){r[e]=new Array(t);for(var n=0;n<t;n+=1)r[e][n]=null}return r}(l=4*u+17),e(0,0),e(l-7,0),e(0,l-7),i(),o(),v(t,r),7<=u&&h(t),null==n&&(n=w(u,f,c)),d(n,r)}var u=t,f=y[r],g=null,l=0,n=null,c=[],s={},e=function(t,r){for(var e=-1;e<=7;e+=1)if(!(t+e<=-1||l<=t+e))for(var n=-1;n<=7;n+=1)r+n<=-1||l<=r+n||(g[t+e][r+n]=0<=e&&e<=6&&(0==n||6==n)||0<=n&&n<=6&&(0==e||6==e)||2<=e&&e<=4&&2<=n&&n<=4)},o=function(){for(var t=8;t<l-8;t+=1)null==g[t][6]&&(g[t][6]=t%2==0);for(var r=8;r<l-8;r+=1)null==g[6][r]&&(g[6][r]=r%2==0)},i=function(){for(var t=B.getPatternPosition(u),r=0;r<t.length;r+=1)for(var e=0;e<t.length;e+=1){var n=t[r],o=t[e];if(null==g[n][o])for(var i=-2;i<=2;i+=1)for(var a=-2;a<=2;a+=1)g[n+i][o+a]=-2==i||2==i||-2==a||2==a||0==i&&0==a}},h=function(t){for(var r=B.getBCHTypeNumber(u),e=0;e<18;e+=1){var n=!t&&1==(r>>e&1);g[Math.floor(e/3)][e%3+l-8-3]=n}for(e=0;e<18;e+=1){n=!t&&1==(r>>e&1);g[e%3+l-8-3][Math.floor(e/3)]=n}},v=function(t,r){for(var e=f<<3|r,n=B.getBCHTypeInfo(e),o=0;o<15;o+=1){var i=!t&&1==(n>>o&1);o<6?g[o][8]=i:o<8?g[o+1][8]=i:g[l-15+o][8]=i}for(o=0;o<15;o+=1){i=!t&&1==(n>>o&1);o<8?g[8][l-o-1]=i:o<9?g[8][15-o-1+1]=i:g[8][15-o-1]=i}g[l-8][8]=!t},d=function(t,r){for(var e=-1,n=l-1,o=7,i=0,a=B.getMaskFunction(r),u=l-1;0<u;u-=2)for(6==u&&(u-=1);;){for(var f=0;f<2;f+=1)if(null==g[n][u-f]){var c=!1;i<t.length&&(c=1==(t[i]>>>o&1)),a(n,u-f)&&(c=!c),g[n][u-f]=c,-1==(o-=1)&&(i+=1,o=7)}if((n+=e)<0||l<=n){n-=e,e=-e;break}}},w=function(t,r,e){for(var n=b.getRSBlocks(t,r),o=M(),i=0;i<e.length;i+=1){var a=e[i];o.put(a.getMode(),4),o.put(a.getLength(),B.getLengthInBits(a.getMode(),t)),a.write(o)}var u=0;for(i=0;i<n.length;i+=1)u+=n[i].dataCount;if(o.getLengthInBits()>8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u<r.length;u+=1){var f=r[u].dataCount,c=r[u].totalCount-f;n=Math.max(n,f),o=Math.max(o,c),i[u]=new Array(f);for(var g=0;g<i[u].length;g+=1)i[u][g]=255&t.getBuffer()[g+e];e+=f;var l=B.getErrorCorrectPolynomial(c),h=C(i[u],l.getLength()-1).mod(l);a[u]=new Array(l.getLength()-1);for(g=0;g<a[u].length;g+=1){var s=g+h.getLength()-a[u].length;a[u][g]=0<=s?h.getAt(s):0}}var v=0;for(g=0;g<r.length;g+=1)v+=r[g].totalCount;var d=new Array(v),w=0;for(g=0;g<n;g+=1)for(u=0;u<r.length;u+=1)g<i[u].length&&(d[w]=i[u][g],w+=1);for(g=0;g<o;g+=1)for(u=0;u<r.length;u+=1)g<a[u].length&&(d[w]=a[u][g],w+=1);return d}(o,n)};s.addData=function(t,r){var e=null;switch(r=r||"Byte"){case"Numeric":e=x(t);break;case"Alphanumeric":e=m(t);break;case"Byte":e=L(t);break;case"Kanji":e=D(t);break;default:throw"mode:"+r}c.push(e),n=null},s.isDark=function(t,r){if(t<0||l<=t||r<0||l<=r)throw t+","+r;return g[t][r]},s.getModuleCount=function(){return l},s.make=function(){if(u<1){for(var t=1;t<40;t++){for(var r=b.getRSBlocks(t,f),e=M(),n=0;n<c.length;n++){var o=c[n];e.put(o.getMode(),4),e.put(o.getLength(),B.getLengthInBits(o.getMode(),t)),o.write(e)}var i=0;for(n=0;n<r.length;n++)i+=r[n].dataCount;if(e.getLengthInBits()<=8*i)break}u=t}a(!1,function(){for(var t=0,r=0,e=0;e<8;e+=1){a(!0,e);var n=B.getLostPoint(s);(0==e||n<t)&&(t=n,r=e)}return r}())},s.createTableTag=function(t,r){t=t||2;var e="";e+='<table style="',e+=" border-width: 0px; border-style: none;",e+=" border-collapse: collapse;",e+=" padding: 0px; margin: "+(r=void 0===r?4*t:r)+"px;",e+='">',e+="<tbody>";for(var n=0;n<s.getModuleCount();n+=1){e+="<tr>";for(var o=0;o<s.getModuleCount();o+=1)e+='<td style="',e+=" border-width: 0px; border-style: none;",e+=" border-collapse: collapse;",e+=" padding: 0px; margin: 0px;",e+=" width: "+t+"px;",e+=" height: "+t+"px;",e+=" background-color: ",e+=s.isDark(n,o)?"#000000":"#ffffff",e+=";",e+='"/>';e+="</tr>"}return e+="</tbody>",e+="</table>"},s.createSvgTag=function(t,r,e,n){var o={};"object"==typeof t&&(t=(o=t).cellSize,r=o.margin,e=o.alt,n=o.title),t=t||2,r=void 0===r?4*t:r,(e="string"==typeof e?{text:e}:e||{}).text=e.text||null,e.id=e.text?e.id||"qrcode-description":null,(n="string"==typeof n?{text:n}:n||{}).text=n.text||null,n.id=n.text?n.id||"qrcode-title":null;var i,a,u,f,c=s.getModuleCount()*t+2*r,g="";for(f="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",g+='<svg version="1.1" xmlns="http://www.w3.org/2000/svg"',g+=o.scalable?"":' width="'+c+'px" height="'+c+'px"',g+=' viewBox="0 0 '+c+" "+c+'" ',g+=' preserveAspectRatio="xMinYMin meet"',g+=n.text||e.text?' role="img" aria-labelledby="'+p([n.id,e.id].join(" ").trim())+'"':"",g+=">",g+=n.text?'<title id="'+p(n.id)+'">'+p(n.text)+"</title>":"",g+=e.text?'<description id="'+p(e.id)+'">'+p(e.text)+"</description>":"",g+='<rect width="100%" height="100%" fill="white" cx="0" cy="0"/>',g+='<path d="',a=0;a<s.getModuleCount();a+=1)foLine truncated
|
||||
Reference in new issue
Block a user