feat: 新增分销推荐人手动绑定功能,调整自动绑定规则
本次修改: 1. 新增分销绑定 API 接口与路由 2. 老用户不再自动绑定推荐人,需手动输入邀请码绑定 3. 新增绑定推荐人 UI 弹层与展示组件 4. 新增防环校验与关系补全逻辑 5. 调整邀请码存储逻辑,仅未登录用户自动存储
这个提交包含在:
@@ -101,10 +101,7 @@ class Auth extends V1
|
||||
DistributionService::bindRelation($uid, $inviterId, $inviteCode);
|
||||
$user = Db::name('user')->where('id', $uid)->find();
|
||||
} else {
|
||||
// 老用户:若未绑定过推荐人,本次传入有效推荐人则尝试绑定
|
||||
if ($inviterId > 0 || $inviteCode !== '') {
|
||||
DistributionService::bindRelation($user['id'], $inviterId, $inviteCode);
|
||||
}
|
||||
// 老用户:不再自动补绑推荐人(老用户须在分销中心手动填写邀请码补绑)
|
||||
// 老用户:仅在本次带上了新值且确实不同时更新(不覆盖运营在后台改过的资料)
|
||||
$update = [];
|
||||
if ($nickname !== '' && $nickname !== (string)$user['nickname']) {
|
||||
@@ -550,20 +547,12 @@ class Auth extends V1
|
||||
// 频率限制:同一手机号 + IP,1 分钟最多 5 次
|
||||
$this->rateLimit('pwdlogin:' . $phone . ':' . $this->request->ip(), 5, 60);
|
||||
|
||||
$inviterId = (int)$this->input('inviterId', $this->input('inviter_id', 0));
|
||||
$inviteCode = trim((string)$this->input('inviteCode', $this->input('invite_code', '')));
|
||||
|
||||
// 查用户:不存在直接失败,不再自动注册(防止批量造号)
|
||||
$user = Db::name('user')->where('mobile', $phone)->find();
|
||||
if (!$user) {
|
||||
$this->fail('手机号未注册,请先使用微信登录');
|
||||
}
|
||||
|
||||
// 老用户:若未绑定过推荐人,本次传入有效推荐人则尝试绑定
|
||||
if ($inviterId > 0 || $inviteCode !== '') {
|
||||
DistributionService::bindRelation($user['id'], $inviterId, $inviteCode);
|
||||
}
|
||||
|
||||
// 校验密码:只认加盐哈希(已移除历史遗留的 123456 万能密码)
|
||||
$salt = isset($user['salt']) ? (string)$user['salt'] : '';
|
||||
$checkHash = md5(md5($password) . $salt);
|
||||
|
||||
@@ -46,6 +46,22 @@ class Distribution extends V1
|
||||
$code = (string)$ext['invite_code'];
|
||||
$shareUrl = 'https://fenleixinxi.gzyakj.com.cn/#/pages/index/index?ic=' . urlencode($code);
|
||||
|
||||
// 查询当前用户的上级推荐人信息(用于前端展示「已绑定推荐人」或「手动绑定邀请码」入口)
|
||||
$inviterId = (int)(isset($ext['inviter_id']) ? $ext['inviter_id'] : 0);
|
||||
if ($inviterId <= 0) {
|
||||
$l1Rel = Db::name('fc_dist_relation')->where('user_id', $uid)->where('level', 1)->find();
|
||||
if ($l1Rel && (int)$l1Rel['parent_id'] > 0) {
|
||||
$inviterId = (int)$l1Rel['parent_id'];
|
||||
}
|
||||
}
|
||||
$inviterNickname = '';
|
||||
if ($inviterId > 0) {
|
||||
$inviterUser = Db::name('user')->where('id', $inviterId)->field('nickname,username')->find();
|
||||
if ($inviterUser) {
|
||||
$inviterNickname = (string)($inviterUser['nickname'] ?: $inviterUser['username']);
|
||||
}
|
||||
}
|
||||
|
||||
// 商家转账确认收款页需要的商户号与小程序 AppID(非敏感,可下发)
|
||||
$epay = (array)get_addon_config('epay');
|
||||
$wx = isset($epay['wechat']) && is_array($epay['wechat']) ? $epay['wechat'] : [];
|
||||
@@ -60,6 +76,8 @@ class Distribution extends V1
|
||||
'l1Count' => $l1,
|
||||
'l2Count' => $l2,
|
||||
'inviteCode' => $code,
|
||||
'inviterId' => $inviterId,
|
||||
'inviterNickname' => $inviterNickname,
|
||||
'shareUrl' => $shareUrl,
|
||||
'posterUrl' => '',
|
||||
// 最低提现金额(后台「分销设置」下发,默认 50)
|
||||
@@ -351,6 +369,72 @@ class Distribution extends V1
|
||||
$this->ok($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* D7 手动输入邀请码补绑推荐人(老用户不支持自动补绑,仅支持通过手动填写邀请码完成补绑)
|
||||
* POST /api/v1/distribution/bind 入参 {inviteCode}
|
||||
*
|
||||
* @return void 输出绑定结果与推荐人信息JSON
|
||||
*/
|
||||
public function bind()
|
||||
{
|
||||
$user = $this->auth();
|
||||
$uid = (int)$user['id'];
|
||||
|
||||
$inviteCode = strtoupper(trim((string)$this->input('inviteCode', $this->input('invite_code', ''))));
|
||||
if ($inviteCode === '') {
|
||||
$this->fail('请输入有效的邀请码');
|
||||
}
|
||||
|
||||
// 检查当前用户是否已绑定过推荐人
|
||||
$ext = $this->ext($uid);
|
||||
$existingInviter = (int)(isset($ext['inviter_id']) ? $ext['inviter_id'] : 0);
|
||||
if ($existingInviter <= 0) {
|
||||
$l1Rel = Db::name('fc_dist_relation')->where('user_id', $uid)->where('level', 1)->find();
|
||||
if ($l1Rel && (int)$l1Rel['parent_id'] > 0) {
|
||||
$existingInviter = (int)$l1Rel['parent_id'];
|
||||
}
|
||||
}
|
||||
if ($existingInviter > 0) {
|
||||
$this->fail('您已绑定过推荐人,不可重复绑定');
|
||||
}
|
||||
|
||||
// 校验邀请码并定位目标推荐人
|
||||
if (isset($ext['invite_code']) && strtoupper((string)$ext['invite_code']) === $inviteCode) {
|
||||
$this->fail('不能填写自己的邀请码');
|
||||
}
|
||||
$parentExt = Db::name('fc_user_ext')->where('invite_code', $inviteCode)->find();
|
||||
if (!$parentExt || (int)$parentExt['user_id'] <= 0) {
|
||||
$this->fail('邀请码不存在,请核对后重试');
|
||||
}
|
||||
$targetInviterId = (int)$parentExt['user_id'];
|
||||
if ($targetInviterId === $uid) {
|
||||
$this->fail('不能绑定自己为推荐人');
|
||||
}
|
||||
|
||||
// 防环校验:不能绑定自己的下级成员
|
||||
$isDownline = Db::name('fc_dist_relation')
|
||||
->where('user_id', $targetInviterId)
|
||||
->where('parent_id', $uid)
|
||||
->count();
|
||||
if ($isDownline > 0) {
|
||||
$this->fail('不能绑定您的下级成员为推荐人');
|
||||
}
|
||||
|
||||
$ok = \app\common\library\DistributionService::bindRelation($uid, $targetInviterId, $inviteCode);
|
||||
if (!$ok) {
|
||||
$this->fail('绑定失败,请核对邀请码后重试');
|
||||
}
|
||||
|
||||
$inviterUser = Db::name('user')->where('id', $targetInviterId)->field('nickname,username')->find();
|
||||
$inviterNickname = $inviterUser ? (string)($inviterUser['nickname'] ?: $inviterUser['username']) : '同城合伙人';
|
||||
|
||||
$this->ok([
|
||||
'bound' => true,
|
||||
'inviterId' => $targetInviterId,
|
||||
'inviterNickname' => $inviterNickname,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前用户可提现余额
|
||||
* 可提现余额 = Σ已结算佣金(status=1) - Σ(申请中/已打款)提现
|
||||
|
||||
@@ -37,8 +37,8 @@ class DistributionService
|
||||
}
|
||||
|
||||
$parent1Id = 0;
|
||||
// 优先通过邀请码查询推荐人
|
||||
$inviteCode = trim((string)$inviteCode);
|
||||
// 优先通过邀请码查询推荐人(忽略大小写)
|
||||
$inviteCode = strtoupper(trim((string)$inviteCode));
|
||||
if ($inviteCode !== '') {
|
||||
$parentExt = Db::name('fc_user_ext')->where('invite_code', $inviteCode)->find();
|
||||
if ($parentExt && (int)$parentExt['user_id'] > 0) {
|
||||
@@ -59,6 +59,15 @@ class DistributionService
|
||||
return false;
|
||||
}
|
||||
|
||||
// 防环校验:不能将自己的直接或间接下线绑定为自己的推荐人
|
||||
$isDownline = Db::name('fc_dist_relation')
|
||||
->where('user_id', $parent1Id)
|
||||
->where('parent_id', $userId)
|
||||
->count();
|
||||
if ($isDownline > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
// 更新用户扩展表的直接推荐人ID
|
||||
@@ -94,6 +103,32 @@ class DistributionService
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 若当前用户在补绑推荐人之前已经发展了 1 级下线,则为这些 1 级下线补齐以 $parent1Id 为 2 级推荐人的关系
|
||||
$myL1Children = Db::name('fc_dist_relation')
|
||||
->where('parent_id', $userId)
|
||||
->where('level', 1)
|
||||
->select();
|
||||
if (!empty($myL1Children)) {
|
||||
foreach ($myL1Children as $childRow) {
|
||||
$childId = (int)$childRow['user_id'];
|
||||
if ($childId <= 0 || $childId === $parent1Id) {
|
||||
continue;
|
||||
}
|
||||
$hasL2 = Db::name('fc_dist_relation')
|
||||
->where('user_id', $childId)
|
||||
->where('level', 2)
|
||||
->count();
|
||||
if ($hasL2 <= 0) {
|
||||
Db::name('fc_dist_relation')->insert([
|
||||
'user_id' => $childId,
|
||||
'parent_id' => $parent1Id,
|
||||
'level' => 2,
|
||||
'createtime' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ return [
|
||||
'api/v1/distribution/poster' => 'api/Distribution/poster',
|
||||
'api/v1/distribution/withdraw' => 'api/Distribution/withdraw',
|
||||
'api/v1/distribution/withdraw-records' => 'api/Distribution/withdrawRecords',
|
||||
'api/v1/distribution/bind' => 'api/Distribution/bind',
|
||||
|
||||
/* =============== 六、用户端 · 消息中心(Module-MESSAGE) =============== */
|
||||
'api/v1/message/list' => 'api/Message/list',
|
||||
|
||||
@@ -13,10 +13,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 捕获并解析邀请分销参数(支持普通 query 与微信小程序码 scene 场景值)
|
||||
* 捕获并解析邀请分销参数(仅供未注册新用户首次登录建档使用,老用户须手动输入邀请码补绑)
|
||||
* @param {Object} options 小程序启动或页面入参
|
||||
*/
|
||||
captureInvite(options) {
|
||||
// 已登录老用户不自动捕获或自动补绑,须在分销中心手动填写邀请码绑定
|
||||
if (uni.getStorageSync('fc_token')) {
|
||||
return
|
||||
}
|
||||
const query = (options && options.query) || {}
|
||||
let ic = query.ic || query.inviteCode || query.invite_code
|
||||
let inviterId = query.inviterId || query.inviter_id
|
||||
@@ -31,9 +35,6 @@ export default {
|
||||
ic = sceneStr.slice(3)
|
||||
} else if (sceneStr.indexOf('uid_') === 0) {
|
||||
inviterId = sceneStr.slice(4)
|
||||
} else if (/^id_\d+_u_(\d+)$/.test(sceneStr)) {
|
||||
const m = sceneStr.match(/^id_\d+_u_(\d+)$/)
|
||||
if (m && m[1]) inviterId = m[1]
|
||||
} else if (sceneStr.indexOf('uid=') === 0) {
|
||||
inviterId = sceneStr.slice(4)
|
||||
} else if (/^id_\d+_u_(\d+)$/.test(sceneStr)) {
|
||||
|
||||
@@ -17,6 +17,8 @@ export const distributionApi = {
|
||||
withdraw: (payload) => post('/distribution/withdraw', payload, { silent: true }),
|
||||
/** 提现记录 */
|
||||
withdrawRecords: () => get('/distribution/withdraw-records', {}, { silent: true }),
|
||||
/** 绑定分销推荐人(扫码自动补绑 / 手动填写邀请码绑定) */
|
||||
bind: (payload = {}) => post('/distribution/bind', payload, { silent: true }),
|
||||
}
|
||||
|
||||
export default distributionApi
|
||||
|
||||
@@ -41,6 +41,8 @@ export const useUserStore = () => {
|
||||
storage.set(config.storageKeys.token, state.token)
|
||||
storage.set(config.storageKeys.userInfo, state.userInfo)
|
||||
storage.set(config.storageKeys.userType, state.userType)
|
||||
storage.remove('fc_invite_code')
|
||||
storage.remove('fc_inviter_id')
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
@@ -708,8 +708,10 @@ export default {
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
|
||||
if (inviterId) uni.setStorageSync('fc_inviter_id', Number(inviterId))
|
||||
if (!uni.getStorageSync('fc_token')) {
|
||||
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
|
||||
if (inviterId) uni.setStorageSync('fc_inviter_id', Number(inviterId))
|
||||
}
|
||||
}
|
||||
this.id = Number((query && query.id) || postIdFromScene || 101)
|
||||
this.posterImagePath = ''
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心分享动作区(支持:原生分享给好友、生成专属海报、复制链接) -->
|
||||
<!-- 核心分享动作区(支持:原生分享给好友、生成专属海报) -->
|
||||
<view class="dist__invite-actions">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- 微信小程序端:button open-type="share" 直接唤起微信好友选择列表 -->
|
||||
@@ -95,7 +95,7 @@
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<!-- H5/App 端:调用系统分享或复制邀请链接 -->
|
||||
<!-- H5/App 端:调用系统分享或复制邀请码 -->
|
||||
<view class="dist__btn dist__btn--primary" @click="shareInvite">
|
||||
<view class="dist__btn-icon-circle">
|
||||
<image class="dist__btn-icon" :src="iconUrl('share')" mode="aspectFit" />
|
||||
@@ -109,12 +109,16 @@
|
||||
<image class="dist__btn-icon dist__btn-icon--plain" :src="iconUrl('bar-poster')" mode="aspectFit" />
|
||||
<text>专属海报</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 复制链接按钮 -->
|
||||
<view class="dist__btn dist__btn--ghost" @click="copyLink">
|
||||
<image class="dist__btn-icon dist__btn-icon--plain" :src="iconUrl('network')" mode="aspectFit" />
|
||||
<text>复制链接</text>
|
||||
</view>
|
||||
<!-- 推荐人状态展示 / 手动填写邀请码绑定入口 -->
|
||||
<view v-if="overview.inviterId > 0" class="dist__inviter-bar dist__inviter-bar--bound">
|
||||
<text class="dist__inviter-label">我的推荐人:{{ overview.inviterNickname || ('合伙人 #' + overview.inviterId) }}</text>
|
||||
<text class="dist__inviter-tag">已锁定</text>
|
||||
</view>
|
||||
<view v-else class="dist__inviter-bar" @click="openBindModal">
|
||||
<text class="dist__inviter-label">有好友邀请码?填写后锁定推荐关系</text>
|
||||
<text class="dist__inviter-action">填写邀请码 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -477,6 +481,44 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 手动绑定推荐人邀请码弹层 ==================== -->
|
||||
<view v-if="showBindModal" class="dist__mask" @click="showBindModal = false" @touchmove.stop.prevent>
|
||||
<view class="dist__sheet" @click.stop>
|
||||
<view class="dist__sheet-handle"></view>
|
||||
<view class="dist__sheet-head">
|
||||
<text class="dist__sheet-title">绑定推荐人邀请码</text>
|
||||
<view class="dist__sheet-close" @click="showBindModal = false">
|
||||
<image class="dist__sheet-close-img" :src="iconUrl('close')" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dist__sheet-field">
|
||||
<input
|
||||
v-model="bindCodeInput"
|
||||
class="dist__sheet-input dist__sheet-input--name"
|
||||
type="text"
|
||||
maxlength="16"
|
||||
placeholder="请输入好友的专属邀请码"
|
||||
placeholder-class="dist__sheet-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="dist__sheet-tip-row">
|
||||
<image class="dist__sheet-tip-icon" :src="iconUrl('info')" mode="aspectFit" />
|
||||
<text class="dist__sheet-tip">每位用户仅可绑定一次推荐人,绑定后终身锁定不可更改</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="dist__sheet-btn"
|
||||
:class="{ 'dist__sheet-btn--disabled': bindSubmitting || !bindCodeInput.trim() }"
|
||||
@click="submitBindCode"
|
||||
>
|
||||
<text>{{ bindSubmitting ? '绑定中…' : '确认绑定' }}</text>
|
||||
</view>
|
||||
<view class="dist__sheet-safe"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 专属邀请海报弹层 ==================== -->
|
||||
<view v-if="showPosterModal" class="dist__mask" @click="showPosterModal = false" @touchmove.stop.prevent>
|
||||
<view class="dist__poster-sheet" @click.stop>
|
||||
@@ -557,6 +599,9 @@ export default {
|
||||
minWithdraw: 50,
|
||||
showWithdraw: false,
|
||||
showRuleModal: false,
|
||||
showBindModal: false,
|
||||
bindCodeInput: '',
|
||||
bindSubmitting: false,
|
||||
showPosterModal: false,
|
||||
posterLoading: false,
|
||||
posterImagePath: '',
|
||||
@@ -930,6 +975,49 @@ export default {
|
||||
this.copyText(this.overview.inviteCode, '邀请码已复制')
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开手动绑定推荐人邀请码弹层
|
||||
*/
|
||||
openBindModal() {
|
||||
this.bindCodeInput = ''
|
||||
this.showBindModal = true
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交手动绑定推荐人邀请码
|
||||
*/
|
||||
async submitBindCode() {
|
||||
if (this.bindSubmitting) return
|
||||
const code = String(this.bindCodeInput || '').trim().toUpperCase()
|
||||
if (!code) {
|
||||
uni.showToast({ title: '请输入好友的邀请码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.overview && this.overview.inviteCode && code === String(this.overview.inviteCode).toUpperCase()) {
|
||||
uni.showToast({ title: '不能填写自己的邀请码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.bindSubmitting = true
|
||||
try {
|
||||
const res = await distributionApi.bind({ inviteCode: code })
|
||||
this.showBindModal = false
|
||||
this.bindCodeInput = ''
|
||||
if (res && res.inviterId) {
|
||||
this.overview = {
|
||||
...this.overview,
|
||||
inviterId: res.inviterId,
|
||||
inviterNickname: res.inviterNickname || '',
|
||||
}
|
||||
}
|
||||
uni.showToast({ title: '推荐人绑定成功', icon: 'success' })
|
||||
this.init()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: (e && e.message) || '绑定失败,请核对邀请码', icon: 'none' })
|
||||
} finally {
|
||||
this.bindSubmitting = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 复制我的专属推广链接
|
||||
*/
|
||||
@@ -1867,6 +1955,46 @@ export default {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
/* 推荐人展示与手动绑定入口栏 */
|
||||
.dist__inviter-bar {
|
||||
margin-top: 20rpx;
|
||||
padding: 18rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #F8FAFC;
|
||||
border: 1rpx dashed #CBD5E1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
.dist__inviter-bar--bound {
|
||||
background: #F0FDF4;
|
||||
border: 1rpx solid #DCFCE7;
|
||||
}
|
||||
.dist__inviter-label {
|
||||
font-size: 24rpx;
|
||||
color: #475569;
|
||||
}
|
||||
.dist__inviter-bar--bound .dist__inviter-label {
|
||||
color: #1E293B;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dist__inviter-action {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #00A854;
|
||||
}
|
||||
.dist__inviter-tag {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #00A854;
|
||||
background: #DCFCE7;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
/* ==================== 顶部 Tab 导航 ==================== */
|
||||
.dist__tabs-bar {
|
||||
display: flex;
|
||||
|
||||
@@ -423,7 +423,7 @@ export default {
|
||||
* @param {Object} query 路由启动参数
|
||||
*/
|
||||
onLoad(query) {
|
||||
if (query) {
|
||||
if (query && !uni.getStorageSync('fc_token')) {
|
||||
const ic = query.ic || query.inviteCode || query.invite_code
|
||||
const inviterId = query.inviterId || query.inviter_id
|
||||
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
|
||||
|
||||
@@ -233,7 +233,7 @@ export default {
|
||||
* @param {Object} query 路由入参
|
||||
*/
|
||||
onLoad(query) {
|
||||
if (query) {
|
||||
if (query && !uni.getStorageSync('fc_token')) {
|
||||
const ic = query.ic || query.inviteCode || query.invite_code
|
||||
const inviterId = query.inviterId || query.inviter_id
|
||||
if (ic) uni.setStorageSync('fc_invite_code', String(ic))
|
||||
|
||||
在新工单中引用
屏蔽一个用户