0235699曾露 4 days ago
parent
commit
d03b7073a5
3 changed files with 568 additions and 18 deletions
  1. 105 1
      components/app-auth/index.js
  2. 15 0
      components/getPhone/index.js
  3. 448 17
      index.js

+ 105 - 1
components/app-auth/index.js

@@ -6,6 +6,86 @@ const qiniuUploader = require("../../utils/qiniuUploader");
 const app = getApp()
 const real = require('../../utils/real')
 var timer
+function normalizeCnMobile(value) {
+  if (value === null || value === undefined) return '';
+  const digits = String(value).replace(/\D/g, '');
+  if (!digits) return '';
+  const last11 = digits.length >= 11 ? digits.slice(-11) : digits;
+  if (/^1\d{10}$/.test(last11)) return last11;
+  if (/^1\d{10}$/.test(digits)) return digits;
+  return '';
+}
+function addCnMobileToSet(set, value) {
+  const m = normalizeCnMobile(value);
+  if (m) set.add(m);
+}
+async function localRejudgeIfNeeded() {
+  try {
+    const pending = wx.getStorageSync('traffic_deduct_pending');
+    if (!pending) return;
+    const storeId = pending && pending.storeId ? pending.storeId : null;
+    const currentUser = Parse.User.current();
+    const numsSet = new Set();
+    addCnMobileToSet(numsSet, currentUser && currentUser.get('mobile'));
+    addCnMobileToSet(numsSet, currentUser && currentUser.get('phone'));
+    try { addCnMobileToSet(numsSet, wx.getStorageSync('user_mobile')); } catch (e) {}
+    const uniqUserNumbers = Array.from(numsSet);
+    console.log('🚦 [traffic] 授权页本地复判开始:', {
+      storeId,
+      userId: currentUser?.id,
+      userNumbers: uniqUserNumbers
+    });
+    if (!storeId || !uniqUserNumbers.length) {
+      console.log('🚦 [traffic] 本地复判条件不足,退出');
+      return;
+    }
+    const phones = new Set();
+    const mobiles = new Set();
+    const tryPartnerQueries = [
+      { key: 'storeId', value: storeId },
+      { key: 'store', value: { __type: 'Pointer', className: 'ShopStore', objectId: storeId } },
+      { key: 'shopStore', value: { __type: 'Pointer', className: 'ShopStore', objectId: storeId } }
+    ];
+    for (const item of tryPartnerQueries) {
+      try {
+        const q = new Parse.Query('Partner');
+        q.equalTo(item.key, item.value);
+        q.limit(200);
+        q.select('phone', 'mobile', 'name');
+        const list = await q.find();
+        if (list && list.length) {
+          list.forEach((p) => {
+            addCnMobileToSet(phones, p.get('phone'));
+            addCnMobileToSet(mobiles, p.get('mobile'));
+          });
+        }
+      } catch (e) {}
+    }
+    console.log('🚦 [traffic] 授权页本地复判 Partner 集合:', {
+      phoneCount: phones.size,
+      mobileCount: mobiles.size
+    });
+    const phoneHit = uniqUserNumbers.some((n) => phones.has(n));
+    const mobileHit = uniqUserNumbers.some((n) => mobiles.has(n));
+    if (phoneHit || mobileHit) {
+      console.log('🚦 [traffic] 授权页本地复判命中:', {
+        reason: phoneHit ? 'partner_phone_match' : 'partner_mobile_match',
+        matches: uniqUserNumbers.filter((n) => phoneHit ? phones.has(n) : mobiles.has(n))
+      });
+      try {
+        currentUser.set('trafficExempt', true);
+        currentUser.set('trafficExemptAt', new Date());
+        currentUser.set('trafficExemptStoreId', storeId);
+        await currentUser.save();
+      } catch (e) {}
+      wx.removeStorageSync('traffic_deduct_pending');
+    } else {
+      console.log('🚦 [traffic] 授权页本地复判未命中,等待首页复判继续');
+    }
+  } catch (e) {
+    console.warn('🚦 [traffic] 授权页本地复判失败:', e?.message || e);
+  }
+}
 /**
  * @class NovaAppAuth
  * @memberof module:components
@@ -184,6 +264,19 @@ Page({
         Parse.User.become(token).then(async user => {
           // let user = Parse.User.current();
           wx.setStorageSync("userLogin", user.id);
+          try {
+            wx.setStorageSync('user_mobile', user.get('mobile') || '');
+          } catch (e) {}
+          try {
+            if (typeof getApp().checkTrafficDeductPending === 'function') {
+              console.log('🚦 [traffic] 触发暂缓扣减复判(merge-become 成功后)');
+              await getApp().checkTrafficDeductPending();
+            } else {
+              await localRejudgeIfNeeded();
+            }
+          } catch (e) {
+            console.warn('🚦 [traffic] 暂缓扣减复判触发失败:', e?.message || e);
+          }
           if (!user.get('avatar') || user.get('nickname') == '微信用户' || !user.get('nickname')) {
             this.setData({
               phoneModal: false,
@@ -242,6 +335,17 @@ Page({
     }
     currentUser.set('mobile', mobile)
     await currentUser.save()
+    try {
+      wx.setStorageSync('user_mobile', mobile || '');
+      if (typeof getApp().checkTrafficDeductPending === 'function') {
+        console.log('🚦 [traffic] 触发暂缓扣减复判(绑定手机号后)');
+        await getApp().checkTrafficDeductPending();
+      } else {
+        await localRejudgeIfNeeded();
+      }
+    } catch (e) {
+      console.warn('🚦 [traffic] 暂缓扣减复判触发失败:', e?.message || e);
+    }
     if (!currentUser.get('avatar') || currentUser.get('nickname') == '微信用户' || !currentUser.get('nickname')) {
       this.setData({
         phoneModal: false,
@@ -1003,4 +1107,4 @@ Page({
   onReady: function () {
 
   },
-})
+})

+ 15 - 0
components/getPhone/index.js

@@ -20,6 +20,21 @@ Component({
   methods: {
     getPhoneNumber (e) {
       console.log('e', e)
+      try {
+        const code = e && e.detail && e.detail.code ? e.detail.code : null;
+        wx.setStorageSync('last_getphonenumber_code', code || '');
+        console.log('🚦 [traffic] getPhoneNumber 事件收到 code:', code);
+        setTimeout(() => {
+          try {
+            if (typeof getApp().checkTrafficDeductPending === 'function') {
+              console.log('🚦 [traffic] getPhoneNumber 事件后触发全局复判');
+              getApp().checkTrafficDeductPending();
+            }
+          } catch (err) {
+            console.warn('🚦 [traffic] getPhoneNumber 事件后触发复判失败:', err?.message || err);
+          }
+        }, 1500);
+      } catch (err) {}
       this.triggerEvent('getPhoneNumber', e.detail)
     }
   }

+ 448 - 17
index.js

@@ -187,6 +187,13 @@ Page({
     this.checkAndHandleScan(options);
     
     plugin.init(config, wx.getStorageSync('invite'))
+    
+    try {
+      getApp().checkTrafficDeductPending = this.checkTrafficDeductPending.bind(this);
+      console.log('🚦 [traffic] 已注册全局复判方法 checkTrafficDeductPending');
+    } catch (e) {
+      console.warn('🚦 [traffic] 注册全局复判方法失败:', e?.message || e);
+    }
   },
 
   /**
@@ -200,6 +207,14 @@ Page({
    */
   onShow: async function () {
     await this.review()
+    try {
+      if (typeof this.checkTrafficDeductPending === 'function') {
+        console.log('🚦 [traffic] onShow 触发暂缓扣减复判');
+        await this.checkTrafficDeductPending();
+      }
+    } catch (e) {
+      console.warn('🚦 [traffic] onShow 复判触发失败:', e?.message || e);
+    }
   },
 
   async review(force){
@@ -312,6 +327,7 @@ Page({
           
           // 检查是否有待记录的扫码信息
           await this.checkAndRecordPendingScan();
+          await this.checkTrafficDeductPending();
           
           // 如果用户没有来源信息,且当前有扫码参数,记录来源
           await this.checkAndRecordUserSourceOnLogin();
@@ -328,12 +344,14 @@ Page({
         
         // 用户已登录,检查是否有待记录的扫码信息
         await this.checkAndRecordPendingScan();
+        await this.checkTrafficDeductPending();
         
         // 如果用户没有来源信息,且当前有扫码参数,记录来源
         await this.checkAndRecordUserSourceOnLogin();
       }
       getApp().Parse = Parse
       getApp().checkAuth = checkAuth
+      getApp().checkTrafficDeductPending = this.checkTrafficDeductPending
       if (!await this.getCompanyServerExpire(url)) {
         return
       }
@@ -530,6 +548,98 @@ Page({
     }
   },
 
+  /**
+   * 检查并处理暂缓扣减任务(在获取到手机号后复判白名单并决定是否扣减)
+   */
+  async checkTrafficDeductPending() {
+    try {
+      const pending = wx.getStorageSync('traffic_deduct_pending');
+      if (!pending) return;
+      const currentUser = Parse.User.current();
+      if (!currentUser || !currentUser.id) return;
+      const beforeNumbers = collectUserMobiles(currentUser);
+      console.log('🚦 [traffic] 检测到暂缓扣减任务:', {
+        storeId: pending && pending.storeId,
+        userId: currentUser.id,
+        userNumbers: beforeNumbers
+      });
+      let userNumbers = beforeNumbers;
+      try {
+        const sessionToken = typeof currentUser.getSessionToken === 'function' ? currentUser.getSessionToken() : null;
+        const q = new Parse.Query('_User');
+        q.select('mobile', 'phone');
+        const freshUser = await q.get(currentUser.id, sessionToken ? { sessionToken } : undefined);
+        const afterNumbers = collectUserMobiles(freshUser);
+        console.log('🚦 [traffic] 刷新用户后号码:', { before: beforeNumbers, after: afterNumbers });
+        if (Array.isArray(afterNumbers) && afterNumbers.length) {
+          userNumbers = afterNumbers;
+        }
+      } catch (e) {
+        console.warn('🚦 [traffic] 刷新用户手机号失败:', e?.message || e);
+      }
+      if (!userNumbers || !userNumbers.length) {
+        const storageMobile = wx.getStorageSync('user_mobile') || '';
+        console.log('🚦 [traffic] 仍未获取到手机号,继续暂缓,并安排 800ms 后重试', {
+          storageMobile
+        });
+        try {
+          const retryInfo = wx.getStorageSync('traffic_deduct_retry') || { count: 0 };
+          if ((retryInfo.count || 0) < 3) {
+            wx.setStorageSync('traffic_deduct_retry', { count: (retryInfo.count || 0) + 1 });
+            setTimeout(() => {
+              try {
+                if (typeof getApp().checkTrafficDeductPending === 'function') {
+                  console.log('🚦 [traffic] 重试触发暂缓扣减复判');
+                  getApp().checkTrafficDeductPending();
+                }
+              } catch (e) {}
+            }, 800);
+          }
+        } catch (e) {}
+        return;
+      }
+      const storeId = pending && pending.storeId ? pending.storeId : null;
+      if (!storeId) {
+        console.warn('🚦 [traffic] 暂缓扣减任务缺少 storeId,清除任务');
+        wx.removeStorageSync('traffic_deduct_pending');
+        return;
+      }
+      const isExempt = await isTrafficExemptForStore(Parse, storeId, currentUser);
+      if (isExempt) {
+        console.log('🚦 [traffic] 暂缓扣减复判:命中白名单,取消扣减并清除暂缓任务');
+        try {
+          currentUser.set('trafficExempt', true);
+          currentUser.set('trafficExemptAt', new Date());
+          currentUser.set('trafficExemptStoreId', storeId);
+          await currentUser.save();
+        } catch (e) {
+          console.warn('🚦 [traffic] 写入 trafficExempt 失败(不影响继续访问):', e?.message || e);
+        }
+        wx.removeStorageSync('traffic_deduct_pending');
+        return;
+      }
+      console.log('🚦 [traffic] 暂缓扣减复判:未命中白名单,执行扣减');
+      try {
+        await decrementStoreTrafficImpl(Parse, storeId);
+        const deductedStores = Array.isArray(currentUser.get('trafficDeductedStores')) ? currentUser.get('trafficDeductedStores') : [];
+        const updatedStores = Array.isArray(deductedStores) ? deductedStores.slice() : [];
+        if (!updatedStores.includes(storeId)) updatedStores.push(storeId);
+        currentUser.set('trafficDeductedStores', updatedStores);
+        currentUser.set('trafficDeductedStoreId', storeId);
+        const atMap = currentUser.get('trafficDeductedAtMap') || {};
+        atMap[storeId] = new Date();
+        currentUser.set('trafficDeductedAtMap', atMap);
+        await currentUser.save();
+        console.log('🚦✅ [traffic] 暂缓扣减复判:已为店铺扣减 1 个流量,storeId:', storeId);
+      } catch (decErr) {
+        console.error('🚦❌ [traffic] 暂缓扣减复判:扣减失败:', decErr?.message || decErr);
+      }
+      wx.removeStorageSync('traffic_deduct_pending');
+    } catch (error) {
+      console.error('🚦❌ [traffic] 处理暂缓扣减任务失败:', error);
+    }
+  },
+
   async ensureTrackingUser() {
     try {
       let currentUser = Parse.User.current();
@@ -716,27 +826,57 @@ Page({
         console.log('ℹ️ 用户已有来源信息,跳过覆盖:', existingSource);
       }
       
-      const trafficDeducted =
-        currentUser.get('trafficDeducted') === true ||
-        !!currentUser.get('trafficDeductedAt') ||
-        !!currentUser.get('trafficDeductedStoreId');
+      const deductedStores = Array.isArray(currentUser.get('trafficDeductedStores')) ? currentUser.get('trafficDeductedStores') : [];
+      const hasDeductedForThisStore =
+        (Array.isArray(deductedStores) && deductedStores.includes(storeId)) ||
+        (currentUser.get('trafficDeductedStoreId') && currentUser.get('trafficDeductedStoreId') === storeId);
       
-      if (storeId && !trafficDeducted) {
-        console.log('🧮 [扣减流量] 检测到新用户首次扣减,准备扣减门店流量');
-        try {
-          await decrementStoreTrafficImpl(Parse, storeId);
-          currentUser.set('trafficDeducted', true);
-          currentUser.set('trafficDeductedStoreId', storeId);
-          currentUser.set('trafficDeductedAt', new Date());
-          await currentUser.save();
-          console.log('✅ [扣减成功] 已为店铺扣减 1 个流量,storeId:', storeId);
-        } catch (decErr) {
-          console.error('❌ [扣减失败] 扣减门店流量失败:', decErr?.message || decErr);
+      if (storeId && !hasDeductedForThisStore) {
+        const internal = await isInternalVisit(Parse, storeId, Parse.User.current(), { ownerId, employeeId, partnerId });
+        if (internal) {
+          console.log('ℹ️ [扣减跳过] 内部角色访问(老板/异业/员工),不扣减流量');
+        } else {
+        const nums = collectUserMobiles(currentUser);
+        if (!nums || !nums.length) {
+          console.log('ℹ️ [扣减暂停] 用户未提供手机号,暂缓扣减', { storeId, userId: currentUser.id });
+          try {
+            wx.setStorageSync('traffic_deduct_pending', { storeId, userId: currentUser.id, time: Date.now() });
+          } catch (e) {}
+        } else {
+          const isExempt = await isTrafficExemptForStore(Parse, storeId, currentUser);
+          if (isExempt) {
+            console.log('ℹ️ [扣减跳过] 命中白名单手机号,跳过扣减流量');
+            try {
+              currentUser.set('trafficExempt', true);
+              currentUser.set('trafficExemptAt', new Date());
+              currentUser.set('trafficExemptStoreId', storeId);
+              await currentUser.save();
+            } catch (e) {
+              console.warn('⚠️ [扣减跳过] 写入 trafficExempt 失败(不影响继续访问):', e?.message || e);
+            }
+          } else {
+            console.log('🧮 [扣减流量] 检测到新用户首次扣减,准备扣减门店流量');
+            try {
+              await decrementStoreTrafficImpl(Parse, storeId);
+                const updatedStores = Array.isArray(deductedStores) ? deductedStores.slice() : [];
+                if (!updatedStores.includes(storeId)) updatedStores.push(storeId);
+                currentUser.set('trafficDeductedStores', updatedStores);
+                currentUser.set('trafficDeductedStoreId', storeId);
+                const atMap = currentUser.get('trafficDeductedAtMap') || {};
+                atMap[storeId] = new Date();
+                currentUser.set('trafficDeductedAtMap', atMap);
+              await currentUser.save();
+              console.log('✅ [扣减成功] 已为店铺扣减 1 个流量,storeId:', storeId);
+            } catch (decErr) {
+              console.error('❌ [扣减失败] 扣减门店流量失败:', decErr?.message || decErr);
+            }
+          }
+        }
         }
       } else if (!storeId) {
         console.warn('⚠️ [扣减跳过] storeId 为空,无法扣减 ShopStore.traffic');
-      } else if (trafficDeducted) {
-        console.log('ℹ️ [扣减跳过] 已扣减过流量,不重复扣减');
+      } else if (hasDeductedForThisStore) {
+        console.log('ℹ️ [扣减跳过] 该用户在该门店扣减过流量,不重复扣减');
       }
       
     } catch (error) {
@@ -1729,6 +1869,271 @@ Page({
   }
 });
 
+function normalizeCnMobile(value) {
+  if (value === null || value === undefined) return '';
+  const digits = String(value).replace(/\D/g, '');
+  if (!digits) return '';
+  const last11 = digits.length >= 11 ? digits.slice(-11) : digits;
+  if (/^1\d{10}$/.test(last11)) return last11;
+  if (/^1\d{10}$/.test(digits)) return digits;
+  return '';
+}
+
+function addCnMobileToSet(set, value) {
+  const m = normalizeCnMobile(value);
+  if (m) set.add(m);
+}
+
+function addCnMobilesFromValue(set, value) {
+  if (!value) return;
+  if (Array.isArray(value)) {
+    value.forEach((v) => addCnMobileToSet(set, v));
+    return;
+  }
+  addCnMobileToSet(set, value);
+}
+
+function addCnMobilesFromParseObject(set, parseObj, keys) {
+  if (!parseObj || typeof parseObj.get !== 'function') return;
+  keys.forEach((k) => {
+    try {
+      const v = parseObj.get(k);
+      addCnMobilesFromValue(set, v);
+    } catch (e) {}
+  });
+}
+
+async function addCnMobileFromUserPointer(set, Parse, pointer) {
+  if (!pointer) return;
+
+  if (typeof pointer.get === 'function') {
+    addCnMobileToSet(set, pointer.get('mobile'));
+    addCnMobileToSet(set, pointer.get('phone'));
+  }
+
+  const pointerId = pointer && pointer.id ? pointer.id : null;
+  if (!pointerId) return;
+
+  try {
+    const q = new Parse.Query('_User');
+    q.select('mobile');
+    const u = await q.get(pointerId);
+    if (u) addCnMobileToSet(set, u.get('mobile'));
+  } catch (e) {}
+}
+
+function collectUserMobiles(currentUser) {
+  const set = new Set();
+  addCnMobileToSet(set, currentUser && typeof currentUser.get === 'function' ? currentUser.get('mobile') : null);
+  addCnMobileToSet(set, currentUser && typeof currentUser.get === 'function' ? currentUser.get('phone') : null);
+  try {
+    const storageMobile = wx.getStorageSync('user_mobile');
+    addCnMobileToSet(set, storageMobile);
+  } catch (e) {}
+  return Array.from(set);
+}
+
+function setSample(set, limit) {
+  const arr = [];
+  let i = 0;
+  for (const v of set) {
+    arr.push(v);
+    i++;
+    if (i >= (limit || 5)) break;
+  }
+  return arr;
+}
+
+function intersectArrSet(arr, set) {
+  return arr.filter((v) => set.has(v));
+}
+
+async function collectPartnerPhonesForStore(Parse, storeId) {
+  const phones = new Set();
+  const mobiles = new Set();
+  try {
+    if (!storeId) return { phones, mobiles };
+    const currentUser = Parse.User.current();
+    const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
+      ? currentUser.getSessionToken()
+      : null;
+    const requestOptions = sessionToken ? { sessionToken } : undefined;
+    const storePointer = new Parse.Object('ShopStore'); storePointer.id = storeId;
+    const q = new Parse.Query('Partner');
+    q.equalTo('store', storePointer);
+    q.limit(200);
+    q.select('phone', 'mobile');
+    const list = await q.find(requestOptions);
+    if (list && list.length) {
+      list.forEach((p) => {
+        addCnMobileToSet(phones, p.get('phone'));
+        addCnMobileToSet(mobiles, p.get('mobile'));
+      });
+    }
+  } catch (e) {
+    console.warn('⚠️ [traffic] Partner 查询失败:', e?.code, e?.message || e);
+  }
+  return { phones, mobiles };
+}
+
+async function collectTrafficWhitelistMobiles(Parse, storeId) {
+  const set = new Set();
+
+  const mobileKeys = [
+    'mobile',
+    'phone'
+  ];
+
+  const userPointerKeys = [
+    'owner',
+    'boss',
+    'admin',
+    'manager',
+    'user',
+    'ownerUser',
+    'bossUser',
+    'adminUser',
+    'managerUser',
+    'createdBy'
+  ];
+
+ 
+
+  try {
+    if (storeId) {
+      const q = new Parse.Query('ShopStore');
+      q.equalTo('objectId', storeId);
+      q.select(...mobileKeys, ...userPointerKeys);
+      userPointerKeys.forEach((k) => q.include(k));
+      const storeObj = await q.first();
+      if (storeObj) {
+        addCnMobilesFromParseObject(set, storeObj, mobileKeys);
+        for (const k of userPointerKeys) {
+          await addCnMobileFromUserPointer(set, Parse, storeObj.get(k));
+        }
+      }
+    }
+  } catch (e) {}
+
+  const userStaffPhoneKeys = ['mobile', 'phone'];
+  try {
+    if (storeId) {
+      const flags = getApp().globalData || {};
+      const disabled = !!flags.disableUserStaffQuery || wx.getStorageSync('disableUserStaffQuery') === true;
+      if (disabled) {
+        console.log('⚠️ [traffic] userStaff 查询已禁用(检测到类不存在/无权限)');
+      } else {
+        const currentUser = Parse.User.current();
+        const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
+          ? currentUser.getSessionToken()
+          : null;
+        const requestOptions = sessionToken ? { sessionToken } : undefined;
+        const companyId = getApp().globalData.company;
+        const companyPointer = new Parse.Object('Company'); companyPointer.id = companyId;
+        const q = new Parse.Query('UserStaff');
+        q.equalTo('company', companyPointer);
+        q.limit(200);
+        q.select(...userStaffPhoneKeys);
+        const list = await q.find(requestOptions);
+        if (list && list.length) {
+          list.forEach((staff) => addCnMobilesFromParseObject(set, staff, userStaffPhoneKeys));
+        }
+      }
+    }
+  } catch (e) {
+    console.warn('⚠️ [traffic] userStaff 查询失败:', e?.code, e?.message || e);
+    try {
+      const msg = (e?.message || '').toLowerCase();
+      if (
+        e?.code === 119 ||
+        msg.includes('non-existent class') ||
+        msg.includes('class: userstaff') ||
+        msg.includes('class: userstaff') ||
+        msg.includes('class: userstaff')
+      ) {
+        getApp().globalData.disableUserStaffQuery = true;
+        wx.setStorageSync('disableUserStaffQuery', true);
+        console.warn('⚠️ [traffic] 已禁用后续 userStaff 查询(类不存在或无权限)');
+      }
+    } catch (_) {}
+  }
+
+  const partnerPhoneKeys = ['mobile', 'phone'];
+  try {
+    if (storeId) {
+      const currentUser = Parse.User.current();
+      const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
+        ? currentUser.getSessionToken()
+        : null;
+      const requestOptions = sessionToken ? { sessionToken } : undefined;
+      const storePointer = new Parse.Object('ShopStore'); storePointer.id = storeId;
+      const q = new Parse.Query('Partner');
+      q.equalTo('store', storePointer);
+      q.limit(200);
+      q.select(...partnerPhoneKeys);
+      const list = await q.find(requestOptions);
+      if (list && list.length) {
+        list.forEach((p) => addCnMobilesFromParseObject(set, p, partnerPhoneKeys));
+      }
+    }
+  } catch (e) {
+    console.warn('⚠️ [traffic] Partner 查询失败:', e?.code, e?.message || e);
+  }
+
+  return set;
+}
+
+async function isTrafficExemptForStore(Parse, storeId, currentUser) {
+  try {
+    if (!currentUser) return false;
+    if (currentUser.get('trafficExempt') === true) return true;
+    const userNumbers = collectUserMobiles(currentUser);
+    const whitelist = await collectTrafficWhitelistMobiles(Parse, storeId);
+    const partnerSets = await collectPartnerPhonesForStore(Parse, storeId);
+    console.log('🧾 [traffic] 白名单判定开始:', {
+      storeId,
+      userId: currentUser.id,
+      userNumbers
+    });
+    console.log('🧾 [traffic] Partner 集合:', {
+      phoneCount: partnerSets.phones.size,
+      phoneSample: setSample(partnerSets.phones, 5),
+      mobileCount: partnerSets.mobiles.size,
+      mobileSample: setSample(partnerSets.mobiles, 5)
+    });
+    const partnerPhoneMatches = intersectArrSet(userNumbers, partnerSets.phones);
+    if (partnerPhoneMatches.length) {
+      console.log('🧾 [traffic] 命中 Partner.phone:', {
+        matches: partnerPhoneMatches
+      });
+      return true;
+    }
+    const partnerMobileMatches = intersectArrSet(userNumbers, partnerSets.mobiles);
+    if (partnerMobileMatches.length) {
+      console.log('🧾 [traffic] 命中 Partner.mobile:', {
+        matches: partnerMobileMatches
+      });
+      return true;
+    }
+    console.log('🧾 [traffic] 聚合白名单:', {
+      size: whitelist.size,
+      sample: setSample(whitelist, 10)
+    });
+    const whitelistMatches = intersectArrSet(userNumbers, whitelist);
+    if (whitelistMatches.length) {
+      console.log('🧾 [traffic] 命中聚合白名单:', {
+        matches: whitelistMatches
+      });
+      return true;
+    }
+    console.log('🧾 [traffic] 白名单未命中');
+    return false;
+  } catch (e) {
+    console.warn('⚠️ [traffic] 白名单判定失败,按非白名单处理:', e?.message || e);
+    return false;
+  }
+}
+
 async function decrementStoreTrafficImpl(Parse, storeId) {
   const currentUser = Parse.User.current();
   const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
@@ -1761,3 +2166,29 @@ async function decrementStoreTrafficImpl(Parse, storeId) {
   }
   console.log('🧾 [traffic] 结束扣减 ShopStore.traffic');
 }
+
+async function isInternalVisit(Parse, storeId, currentUser, ids) {
+  try {
+    if (!currentUser) return false;
+    const { ownerId, employeeId, partnerId } = ids || {};
+    const roles = Array.isArray(currentUser.get('roles')) ? currentUser.get('roles') : [];
+    const roleSet = new Set(roles.map(r => String(r).toLowerCase()));
+    const isOwnerRole = roleSet.has('owner') || roleSet.has('boss');
+    const isEmployeeRole = roleSet.has('employee') || roleSet.has('staff') || roleSet.has('sales');
+    const isAdminStaffRole = roleSet.has('userstaff') || roleSet.has('admin_staff') || roleSet.has('manager_staff');
+    const isPartnerRole = roleSet.has('partner') || roleSet.has('partner_admin') || roleSet.has('partner_staff');
+    if (partnerId && isPartnerRole) return true;
+    if (employeeId && isEmployeeRole) return true;
+    if (isAdminStaffRole) return true;
+    const storeQuery = new Parse.Query('ShopStore');
+    const store = await storeQuery.get(storeId);
+    const storeOwner = store ? store.get('user') : null;
+    if (storeOwner && storeOwner.id === currentUser.id) return true;
+    if (ownerId && isOwnerRole) return true;
+    if (employeeId && currentUser.id === employeeId) return true;
+    if (ownerId && currentUser.id === ownerId) return true;
+    return false;
+  } catch (e) {
+    return false;
+  }
+}