diff --git a/application/api/controller/Auth.php b/application/api/controller/Auth.php index 109a75c..48eff2f 100644 --- a/application/api/controller/Auth.php +++ b/application/api/controller/Auth.php @@ -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); diff --git a/application/api/controller/Distribution.php b/application/api/controller/Distribution.php index 6eaa90a..8325ca8 100644 --- a/application/api/controller/Distribution.php +++ b/application/api/controller/Distribution.php @@ -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) - Σ(申请中/已打款)提现 diff --git a/application/common/library/DistributionService.php b/application/common/library/DistributionService.php index a28f157..fc2fb58 100644 --- a/application/common/library/DistributionService.php +++ b/application/common/library/DistributionService.php @@ -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; } diff --git a/application/route.php b/application/route.php index 30816f5..6923e36 100755 --- a/application/route.php +++ b/application/route.php @@ -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', diff --git a/uniapp/App.vue b/uniapp/App.vue index 22da80c..de2665b 100644 --- a/uniapp/App.vue +++ b/uniapp/App.vue @@ -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)) { diff --git a/uniapp/common/api/distribution.js b/uniapp/common/api/distribution.js index 79febda..b53f6f5 100644 --- a/uniapp/common/api/distribution.js +++ b/uniapp/common/api/distribution.js @@ -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 diff --git a/uniapp/common/store/index.js b/uniapp/common/store/index.js index 3c59882..983e70a 100644 --- a/uniapp/common/store/index.js +++ b/uniapp/common/store/index.js @@ -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 } diff --git a/uniapp/pages/detail/detail.vue b/uniapp/pages/detail/detail.vue index 05c2af3..f846d89 100644 --- a/uniapp/pages/detail/detail.vue +++ b/uniapp/pages/detail/detail.vue @@ -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 = '' diff --git a/uniapp/pages/distribution/index.vue b/uniapp/pages/distribution/index.vue index 086ca06..06b16e3 100644 --- a/uniapp/pages/distribution/index.vue +++ b/uniapp/pages/distribution/index.vue @@ -82,7 +82,7 @@ - + @@ -95,7 +95,7 @@ - + @@ -109,12 +109,16 @@ 专属海报 + - - - - 复制链接 - + + + 我的推荐人:{{ overview.inviterNickname || ('合伙人 #' + overview.inviterId) }} + 已锁定 + + + 有好友邀请码?填写后锁定推荐关系 + 填写邀请码 › @@ -477,6 +481,44 @@ + + + + + + 绑定推荐人邀请码 + + + + + + + + + + + + 每位用户仅可绑定一次推荐人,绑定后终身锁定不可更改 + + + + {{ bindSubmitting ? '绑定中…' : '确认绑定' }} + + + + + @@ -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; diff --git a/uniapp/pages/index/index.vue b/uniapp/pages/index/index.vue index ef29765..41eb880 100644 --- a/uniapp/pages/index/index.vue +++ b/uniapp/pages/index/index.vue @@ -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)) diff --git a/uniapp/pages/merchant/detail.vue b/uniapp/pages/merchant/detail.vue index d636996..2ea1f6e 100644 --- a/uniapp/pages/merchant/detail.vue +++ b/uniapp/pages/merchant/detail.vue @@ -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))