feat(invite-code): one-time invite codes with rewards, usage log, import/export
- 一次性邀请码:批量生成(次数/有效期/备注/四类奖励)、原子消费防超发、 开启注册验证时奖励延迟发放(账本幂等键) - Zibll 兼容文本导入(points/balance/vip/vip_time/level_integral,分隔符可自定义, 第三段标识存为备注)、CSV 全字段导出与可回流文本导出 - 使用记录表 aksrt_invite_code_usage + 后台使用者列;注册表单 400ms 防抖实时校验 - 注册门控三模式(close/open/must)设置链路(PHP schema + admin SPA schema) - 契约测试覆盖建表/消费竞态/导入导出/备注全链路
This commit is contained in:
@@ -22,6 +22,9 @@ export const authSchema = {
|
||||
invit_code_s: 'close',
|
||||
invit_code_open_desc: '如您受到邀请,可填写推广码。部分推广码会有额外积分赠送',
|
||||
invit_code_must_desc: '<div>本站需使用推广码注册</div><div>获取推广码请与客服联系</div>',
|
||||
invite_code_s: 'close',
|
||||
invite_code_open_desc: '请输入邀请码,注册成功后奖励将自动发放',
|
||||
invite_code_must_desc: '<div>本站需凭邀请码注册</div><div>获取邀请码请与客服联系</div>',
|
||||
user_signin_phone_s: false,
|
||||
user_signin_nopas_s: false,
|
||||
user_signin_nopas_type: 'email',
|
||||
@@ -84,6 +87,16 @@ export const authSchema = {
|
||||
{ key: 'invit_code_must_desc', label: '必填提示文案', type: 'textarea', default: '<div>本站需使用推广码注册</div><div>获取推广码请与客服联系</div>', dependsOn: { key: 'invit_code_s', value: 'must' }, help: '支持安全 HTML' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'invite_code',
|
||||
title: '邀请码注册',
|
||||
icon: 'fa fas fa-envelope-open-text',
|
||||
fields: [
|
||||
{ key: 'invite_code_s', label: '邀请码注册模式', type: 'radio', default: 'close', options: [{ label: '关闭', value: 'close' }, { label: '选填邀请码', value: 'open' }, { label: '必须填写邀请码', value: 'must' }], help: '一次性邀请码与推广码相互独立:邀请码用于控制注册并按码发放奖励,在「用户 → 邀请码管理」批量生成' },
|
||||
{ key: 'invite_code_open_desc', label: '选填提示文案', type: 'input', default: '请输入邀请码,注册成功后奖励将自动发放', dependsOn: { key: 'invite_code_s', value: 'open' } },
|
||||
{ key: 'invite_code_must_desc', label: '必填提示文案', type: 'textarea', default: '<div>本站需凭邀请码注册</div><div>获取邀请码请与客服联系</div>', dependsOn: { key: 'invite_code_s', value: 'must' }, help: '支持安全 HTML' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'verification',
|
||||
title: '登录与验证',
|
||||
|
||||
@@ -506,6 +506,37 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 邀请码输入实时校验(公开只读接口;提交时服务端仍是硬门禁)。
|
||||
function initInviteVerify(root) {
|
||||
root.querySelectorAll('input[data-invite-verify]').forEach(function (input) {
|
||||
if (input.dataset.inviteVerifyBound === '1') return;
|
||||
input.dataset.inviteVerifyBound = '1';
|
||||
var feedback = root.querySelector('[data-invite-feedback]');
|
||||
var timer = null;
|
||||
function setFeedback(text, isError) {
|
||||
if (!feedback) return;
|
||||
if (!text) { feedback.hidden = true; feedback.textContent = ''; feedback.classList.remove('is-error'); return; }
|
||||
feedback.hidden = false;
|
||||
feedback.textContent = text;
|
||||
feedback.classList.toggle('is-error', !!isError);
|
||||
}
|
||||
input.addEventListener('input', function () {
|
||||
if (timer) window.clearTimeout(timer);
|
||||
var code = input.value.trim();
|
||||
if (!code) { setFeedback(''); return; }
|
||||
timer = window.setTimeout(function () {
|
||||
window.fetch('/wp-json/aksrt/v1/invite-code/verify?code=' + encodeURIComponent(code), { credentials: 'same-origin' })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
if (!payload) { setFeedback(''); return; }
|
||||
setFeedback(payload.valid ? '邀请码有效。' : '邀请码无效或已用完。', !payload.valid);
|
||||
})
|
||||
.catch(function () { setFeedback(''); });
|
||||
}, 400);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initRoot(root) {
|
||||
root.querySelectorAll('input').forEach(function (input) {
|
||||
if (!input.hasAttribute('placeholder')) input.setAttribute('placeholder', ' ');
|
||||
@@ -521,6 +552,7 @@
|
||||
if (!button.getAttribute('aria-label')) button.setAttribute('aria-label', '显示密码');
|
||||
});
|
||||
setOAuthRedirect(root, root.dataset.redirectTo || '');
|
||||
initInviteVerify(root);
|
||||
var initial = root.dataset.initialTab || 'signin';
|
||||
if (initial === 'oauth' && window.matchMedia && window.matchMedia('(max-width: 767px)').matches) initial = 'signin';
|
||||
var mayActivateQr = initial !== 'oauth' || document.visibilityState === 'visible';
|
||||
|
||||
Reference in New Issue
Block a user