index.js 88 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330
  1. // var Parse = getApp().Parse;
  2. // var app = getApp()
  3. // const { wxLogin } = require('./utils/login')
  4. const CONFIG = require("config.js");
  5. let config = {
  6. appid: CONFIG.default.appid,
  7. company: CONFIG.default.company,
  8. rootPage: CONFIG.default.rootPage,
  9. }
  10. const plugin = requirePlugin('fm-plugin')
  11. const { Parse, checkAuth } = plugin
  12. Page({
  13. /**
  14. * 页面的初始数据
  15. */
  16. data: {
  17. splashUrl: wx.getStorageSync("enabledOptions")[0],
  18. loading:true
  19. },
  20. /**
  21. * 生命周期函数--监听页面加载
  22. */
  23. onLoad: async function (options) {
  24. // 拦截插件的"登录信息已过期"提示
  25. const originalShowModal = wx.showModal;
  26. wx.showModal = function(options) {
  27. // 如果是"登录信息已过期"的提示,改为友好的重新登录提示
  28. if (options.content && options.content.includes('登录信息过期')) {
  29. console.log('⚠️ 拦截到"登录信息已过期"提示,改为友好提示');
  30. return originalShowModal.call(this, {
  31. title: '提示',
  32. content: '登录状态异常,是否重新登录?',
  33. showCancel: true,
  34. cancelText: '取消',
  35. confirmText: '重新登录',
  36. success: (result) => {
  37. if (result.confirm) {
  38. // 清除登录状态
  39. wx.removeStorageSync("sessionToken");
  40. wx.removeStorageSync("userLogin");
  41. // 重新加载首页
  42. const rootPage = getApp().globalData.rootPage || getApp().globalData.defaultTabBar?.list?.[0]?.pagePath || '/pages/index/index';
  43. wx.reLaunch({
  44. url: rootPage
  45. });
  46. }
  47. // 调用原始的 success 回调
  48. if (options.success) {
  49. options.success(result);
  50. }
  51. }
  52. });
  53. }
  54. // 其他提示正常显示
  55. return originalShowModal.call(this, options);
  56. };
  57. wx.login({
  58. success: function (res) {
  59. if (res.code) {
  60. console.log(res);
  61. // wx.request({
  62. // url: "https://server.fmode.cn/api/wxapp/auth_wxapp",
  63. // data: {
  64. // c: getApp().globalData.company,
  65. // code: res.code,
  66. // },
  67. // async success(res) {
  68. // wx.setStorageSync("userInfo", res.data);
  69. // resolve(res)
  70. // },
  71. // });
  72. }
  73. },
  74. fail: function (err) {
  75. wx.showToast({
  76. title: '服务器繁忙,请稍后重试',
  77. })
  78. }
  79. });
  80. wx.setStorageSync("invite", null);
  81. // 处理扫码链接(options.q 包含完整的URL或activityId)
  82. if (options.q) {
  83. let str = decodeURIComponent(options.q); // 扫描二维码获得的跳转链接
  84. // 兼容一些环境中把 & 编码成 & 的情况,防止参数解析错误(如 scanCount=0&storeId=...)
  85. if (str.indexOf('&') !== -1) {
  86. console.log('🔧 检测到 URL 中包含 &,自动还原为 &');
  87. str = str.replace(/&/g, '&');
  88. }
  89. try { wx.setStorageSync('scan_raw_url', str); } catch (e) {}
  90. // 检查是否是员工邀请链接(app.fmode.cn/dev/pobingfeng/manager/staff?invite=xxx)
  91. if (str.includes('app.fmode.cn/dev/pobingfeng/manager/staff')) {
  92. let obj = this.getParaName(str);
  93. if (obj && obj.invite) {
  94. wx.setStorageSync("invite", obj.invite);
  95. console.log('✅ 检测到员工邀请链接,invite:', obj.invite);
  96. }
  97. // 员工邀请链接不需要跳转到店铺,直接返回
  98. this.setData({ options: options });
  99. plugin.init(config, wx.getStorageSync('invite'));
  100. return;
  101. }
  102. // 检查是否是活动海报二维码(activityId作为qrCode参数值)
  103. // 如果str不包含http/https,且看起来像是一个ID,则可能是activityId
  104. if (!str.includes('http://') && !str.includes('https://') && !str.includes('?')) {
  105. // 可能是活动ID,尝试作为activityId处理
  106. if (str && str.length > 0 && !isNaN(str)) {
  107. options.activityId = str;
  108. console.log('✅ 检测到活动海报二维码,activityId:', str);
  109. // 活动二维码需要特殊处理,先保存activityId
  110. wx.setStorageSync("activityId", str);
  111. }
  112. }
  113. // 处理店铺相关的二维码链接(pwa.fmode.cn/gomini/pmd/)
  114. // 从完整URL中提取参数
  115. if (str.includes('pwa.fmode.cn/gomini/pmd') || str.includes('?')) {
  116. let obj = this.getParaName(str);
  117. if (obj && obj.invite) {
  118. wx.setStorageSync("invite", obj.invite);
  119. }
  120. // 将所有参数合并到 options 中
  121. obj && Object.keys(obj).forEach(key=> options[key] = obj[key]);
  122. }
  123. }
  124. // 兼容从编译参数或页面直达传入的 storeId
  125. if (options && options.scene && !options.storeId) {
  126. try {
  127. const sceneStr = decodeURIComponent(options.scene)
  128. if (sceneStr) {
  129. const pairs = sceneStr.split('&')
  130. for (const p of pairs) {
  131. const [k, v] = p.split('=')
  132. if (k === 'storeId' && v) {
  133. options.storeId = v
  134. break
  135. }
  136. }
  137. }
  138. } catch (e) {
  139. console.warn('解析 scene 失败:', e)
  140. }
  141. }
  142. this.setData({
  143. options: options
  144. })
  145. let {
  146. time,
  147. dramaId,
  148. roomId,
  149. orderId,
  150. shopId,
  151. invite,
  152. activityId,
  153. company,
  154. inviteHost,
  155. storeId
  156. } = options
  157. time && wx.setStorageSync("time", time);
  158. dramaId && wx.setStorageSync("dramaId", dramaId);
  159. roomId && wx.setStorageSync("roomId", roomId);
  160. orderId && wx.setStorageSync("orderId", orderId);
  161. shopId && wx.setStorageSync("shopId", shopId);
  162. invite && wx.setStorageSync("invite", invite);
  163. activityId && wx.setStorageSync("activityId", activityId);
  164. inviteHost && wx.setStorageSync("inviteHost", true);
  165. if (storeId) {
  166. // 扫码进入时,强制设置店铺 ID,标记为扫码来源
  167. wx.setStorageSync('storeId', storeId)
  168. getApp().globalData.storeId = storeId
  169. wx.setStorageSync('storeId_from_scan', true)
  170. console.log('✅ 入口页扫码进入,已设置店铺 ID(优先级最高):', storeId)
  171. }
  172. if (company) getApp().globalData.toCompany = true;
  173. // 检查是否是扫码进入(需要统计扫码次数)
  174. this.checkAndHandleScan(options);
  175. plugin.init(config, wx.getStorageSync('invite'))
  176. try {
  177. getApp().checkTrafficDeductPending = this.checkTrafficDeductPending.bind(this);
  178. console.log('🚦 [traffic] 已注册全局复判方法 checkTrafficDeductPending');
  179. } catch (e) {
  180. console.warn('🚦 [traffic] 注册全局复判方法失败:', e?.message || e);
  181. }
  182. try {
  183. getApp().checkAndRecordPendingScan = this.checkAndRecordPendingScan.bind(this);
  184. } catch (e) {}
  185. },
  186. /**
  187. * 生命周期函数--监听页面初次渲染完成
  188. */
  189. onReady: async function () { },
  190. /**
  191. * 生命周期函数--监听页面显示
  192. */
  193. onShow: async function () {
  194. await this.review()
  195. try {
  196. if (typeof this.checkTrafficDeductPending === 'function') {
  197. console.log('🚦 [traffic] onShow 触发暂缓扣减复判');
  198. await this.checkTrafficDeductPending();
  199. }
  200. } catch (e) {
  201. console.warn('🚦 [traffic] onShow 复判触发失败:', e?.message || e);
  202. }
  203. },
  204. async review(force){
  205. try {
  206. let options = this.data.options
  207. let url = getApp().globalData.rootPage || getApp().globalData.defaultTabBar.list[0].pagePath
  208. if (options) {
  209. let objArr = Object.keys(options)
  210. if (objArr && objArr.length > 0) {
  211. let parms = '?'
  212. objArr.forEach((o, index) => {
  213. if (index > 0) {
  214. parms += '&' + o + '=' + options[o]
  215. } else {
  216. parms += o + '=' + options[o]
  217. }
  218. })
  219. url += parms
  220. }
  221. }
  222. let currentUser = Parse.User.current()
  223. console.log('===========================================');
  224. console.log('======= index.js review 方法 =======');
  225. console.log('当前用户:', currentUser ? currentUser.id : '无');
  226. console.log('用户手机号:', currentUser?.get('mobile') || '无');
  227. console.log('用户名:', currentUser?.get('username') || '无');
  228. console.log('Session Token:', currentUser?.getSessionToken()?.substring(0, 20) || '无');
  229. console.log('userLogin 存储:', wx.getStorageSync('userLogin') || '无');
  230. console.log('force 参数:', force);
  231. console.log('===========================================');
  232. // 查询 Company 的 isPublishing 字段
  233. let isPublishing = false;
  234. try {
  235. const companyQuery = new Parse.Query('Company');
  236. companyQuery.equalTo('objectId', getApp().globalData.company);
  237. companyQuery.select('isPublishing');
  238. try {
  239. const companyObj = await companyQuery.first();
  240. if (companyObj) {
  241. isPublishing = companyObj.get('isPublishing') === true;
  242. console.log('📋 Company isPublishing:', isPublishing);
  243. } else {
  244. console.log('⚠️ 未找到 Company 记录,默认 isPublishing = false');
  245. isPublishing = false;
  246. }
  247. } catch (queryError) {
  248. console.error('❌ 查询 Company 失败(可能 token 无效):', queryError.message);
  249. // 查询失败时,默认为 false(强制登录)
  250. isPublishing = false;
  251. }
  252. // 保存到全局,供其他页面使用
  253. getApp().globalData.isPublishing = isPublishing;
  254. wx.setStorageSync('isPublishing', isPublishing);
  255. } catch (error) {
  256. console.error('❌ 查询 Company isPublishing 失败:', error);
  257. // 查询失败时,默认为 false(强制登录)
  258. isPublishing = false;
  259. }
  260. // 根据 isPublishing 决定是否强制登录
  261. if (!currentUser || force) {
  262. console.log('🔄 开始调用 checkAuth...');
  263. // isPublishing == true 时不强制授权,否则强制授权
  264. const forceAuth = !isPublishing;
  265. console.log('🔐 是否强制登录:', forceAuth);
  266. let r = await checkAuth(forceAuth);
  267. console.log('===========================================');
  268. console.log('======= checkAuth 返回结果 =======');
  269. console.log('返回值:', r);
  270. // 重新获取用户信息
  271. currentUser = Parse.User.current();
  272. console.log('checkAuth 后的用户:', currentUser ? currentUser.id : '无');
  273. console.log('checkAuth 后的手机号:', currentUser?.get('mobile') || '无');
  274. console.log('checkAuth 后的 Session Token:', currentUser?.getSessionToken()?.substring(0, 20) || '无');
  275. console.log('===========================================');
  276. // 如果强制登录但用户未登录,不允许继续访问
  277. if (forceAuth && !r) {
  278. console.log('❌ 强制登录模式,用户未登录,停止访问');
  279. return;
  280. }
  281. // 即使登录失败,也允许继续访问(仅在非强制登录模式下)
  282. if(!r) {
  283. console.log('⚠️ 用户未登录或拒绝授权,允许游客访问');
  284. if (wx.getStorageSync('need_scan_redirect') === true) {
  285. await this.ensureTrackingUser();
  286. await this.checkAndRecordUserSourceOnLogin();
  287. }
  288. // 不要 return,继续执行后面的跳转逻辑
  289. } else {
  290. // 登录成功,设置 userLogin
  291. if (currentUser && currentUser.get('mobile')) {
  292. wx.setStorageSync("userLogin", currentUser.id);
  293. console.log('✅ 授权登录成功,已设置 userLogin:', currentUser.id);
  294. console.log('✅ 用户手机号:', currentUser.get('mobile'));
  295. } else {
  296. console.warn('⚠️ checkAuth 返回成功,但用户没有手机号!');
  297. console.warn(' 用户对象:', currentUser);
  298. }
  299. // 检查是否有待记录的扫码信息
  300. await this.checkAndRecordPendingScan();
  301. await this.checkTrafficDeductPending();
  302. // 如果用户没有来源信息,且当前有扫码参数,记录来源
  303. await this.checkAndRecordUserSourceOnLogin();
  304. }
  305. } else {
  306. console.log('✅ 用户已登录,跳过 checkAuth');
  307. // 用户已登录,确保 userLogin 已设置
  308. if (currentUser.get('mobile')) {
  309. wx.setStorageSync("userLogin", currentUser.id);
  310. console.log('✅ 已确认 userLogin:', currentUser.id);
  311. }
  312. this.updateUser(currentUser.id);
  313. // 用户已登录,检查是否有待记录的扫码信息
  314. await this.checkAndRecordPendingScan();
  315. await this.checkTrafficDeductPending();
  316. // 如果用户没有来源信息,且当前有扫码参数,记录来源
  317. await this.checkAndRecordUserSourceOnLogin();
  318. }
  319. getApp().Parse = Parse
  320. getApp().checkAuth = checkAuth
  321. getApp().checkTrafficDeductPending = this.checkTrafficDeductPending
  322. if (!await this.getCompanyServerExpire(url)) {
  323. return
  324. }
  325. // 检查是否需要跳转到活动页面
  326. if (wx.getStorageSync('need_activity_redirect') === true) {
  327. await this.redirectToActivityPage();
  328. return;
  329. }
  330. // 检查是否需要跳转到扫码统计页面
  331. if (this.shouldRedirectToScanPage()) {
  332. await this.redirectToScanPage();
  333. return;
  334. }
  335. console.log('✅ 准备跳转到:', url);
  336. wx.redirectTo({
  337. url: url,
  338. success: () => {
  339. console.log('✅ redirectTo 跳转成功');
  340. },
  341. fail: (err) => {
  342. console.error('❌ redirectTo 失败:', err);
  343. console.log('⚠️ 尝试使用 reLaunch');
  344. // 降级:尝试使用 reLaunch
  345. wx.reLaunch({
  346. url: url,
  347. success: () => {
  348. console.log('✅ reLaunch 跳转成功');
  349. },
  350. fail: (err2) => {
  351. console.error('❌ reLaunch 也失败:', err2);
  352. // 显示错误提示
  353. this.setData({ loading: false });
  354. wx.showModal({
  355. title: '温馨提示',
  356. content: '页面加载失败,请检查网络后重试。',
  357. showCancel: true,
  358. cancelText: '退出',
  359. confirmText: '重试',
  360. success: (result) => {
  361. if (result.confirm) {
  362. this.review(true);
  363. } else {
  364. wx.exitMiniProgram();
  365. }
  366. }
  367. });
  368. }
  369. });
  370. }
  371. });
  372. }
  373. catch (err) {
  374. console.log('❌ review 方法出错:', err);
  375. /* 登录身份信息到期,重新登陆 */
  376. if((err?.message?.indexOf('Session token is expired') != -1 || err?.message?.indexOf('Invalid session token') != -1) && !force){
  377. console.log('⚠️ Session Token 过期,准备重新登录');
  378. // 保存需要保留的数据
  379. const invite = wx.getStorageSync('invite');
  380. const agreementAccepted = wx.getStorageSync('user_agreement_accepted');
  381. const storeId = wx.getStorageSync('storeId');
  382. const isPublishing = wx.getStorageSync('isPublishing');
  383. // 先登出 Parse 用户
  384. try {
  385. await Parse.User.logOut();
  386. console.log('✅ Parse 用户已登出');
  387. } catch (logoutErr) {
  388. console.warn('⚠️ Parse 登出失败:', logoutErr);
  389. }
  390. // 清除所有存储
  391. wx.clearStorageSync();
  392. // 恢复需要保留的数据
  393. if (invite) wx.setStorageSync('invite', invite);
  394. if (agreementAccepted) wx.setStorageSync('user_agreement_accepted', agreementAccepted);
  395. if (storeId) wx.setStorageSync('storeId', storeId);
  396. if (isPublishing !== undefined) wx.setStorageSync('isPublishing', isPublishing);
  397. console.log('✅ 已清除过期登录信息,保留必要数据');
  398. console.log(' 保留的 invite:', invite || '无');
  399. console.log(' 保留的协议状态:', agreementAccepted || '无');
  400. console.log(' 保留的店铺ID:', storeId || '无');
  401. /* 强制重新登录 */
  402. this.review(true);
  403. return;
  404. }
  405. // 如果出错,尝试继续跳转到主页(游客模式)
  406. console.log('⚠️ 登录出错,尝试游客模式访问');
  407. let url = getApp().globalData.rootPage || getApp().globalData.defaultTabBar.list[0].pagePath;
  408. if (this.data.options) {
  409. let objArr = Object.keys(this.data.options)
  410. if (objArr && objArr.length > 0) {
  411. let parms = '?'
  412. objArr.forEach((o, index) => {
  413. if (index > 0) {
  414. parms += '&' + o + '=' + this.data.options[o]
  415. } else {
  416. parms += o + '=' + this.data.options[o]
  417. }
  418. })
  419. url += parms
  420. }
  421. }
  422. wx.redirectTo({
  423. url: url,
  424. fail: () => {
  425. // 如果跳转失败,显示错误提示
  426. this.setData({
  427. loading:false
  428. })
  429. wx.showModal({
  430. title: '温馨提示',
  431. content: '页面加载失败,请检查网络后重试。',
  432. showCancel: true,
  433. cancelText: '退出',
  434. confirmText: '重试',
  435. success: (result) => {
  436. if (result.confirm) {
  437. // 用户选择重试
  438. this.review(true)
  439. } else {
  440. // 用户选择退出
  441. wx.exitMiniProgram()
  442. }
  443. },
  444. });
  445. }
  446. });
  447. }
  448. },
  449. /**
  450. * 检查并记录待处理的扫码统计
  451. * 同时记录用户来源信息到 _User 表
  452. */
  453. async checkAndRecordPendingScan() {
  454. try {
  455. const pendingScan = wx.getStorageSync('pending_scan_record');
  456. if (!pendingScan) {
  457. return;
  458. }
  459. console.log('===========================================');
  460. console.log('======= 发现待记录的扫码信息 =======');
  461. console.log('扫码信息:', pendingScan);
  462. console.log('===========================================');
  463. // 检查是否超时(24小时)
  464. const now = Date.now();
  465. const scanTime = pendingScan.timestamp || 0;
  466. const hoursPassed = (now - scanTime) / (1000 * 60 * 60);
  467. if (hoursPassed > 24) {
  468. console.log('⚠️ 扫码信息已超过24小时,不再记录');
  469. wx.removeStorageSync('pending_scan_record');
  470. return;
  471. }
  472. // 记录用户来源信息
  473. await this.recordUserSource(pendingScan);
  474. // 记录扫码统计
  475. await this.recordScanStatistics({
  476. storeId: pendingScan.storeId,
  477. sourceType: pendingScan.sourceType,
  478. sourceId: pendingScan.sourceId,
  479. ownerId: pendingScan.ownerId,
  480. employeeId: pendingScan.employeeId,
  481. partnerId: pendingScan.partnerId,
  482. userId: pendingScan.userId,
  483. productId: pendingScan.productId,
  484. scanCount: pendingScan.scanCount,
  485. scanTimestamp: pendingScan.timestamp
  486. });
  487. // 清除待记录的扫码信息
  488. wx.removeStorageSync('pending_scan_record');
  489. console.log('✅ 扫码统计已记录并清除');
  490. } catch (error) {
  491. console.error('❌ 记录待处理扫码信息失败:', error);
  492. }
  493. },
  494. /**
  495. * 检查并处理暂缓扣减任务(在获取到手机号后复判白名单并决定是否扣减)
  496. */
  497. async checkTrafficDeductPending() {
  498. try {
  499. const pending = wx.getStorageSync('traffic_deduct_pending');
  500. if (!pending) return;
  501. const currentUser = Parse.User.current();
  502. if (!currentUser || !currentUser.id) return;
  503. const beforeNumbers = collectUserMobiles(currentUser);
  504. console.log('🚦 [traffic] 检测到暂缓扣减任务:', {
  505. storeId: pending && pending.storeId,
  506. userId: currentUser.id,
  507. userNumbers: beforeNumbers
  508. });
  509. let userNumbers = beforeNumbers;
  510. try {
  511. const sessionToken = typeof currentUser.getSessionToken === 'function' ? currentUser.getSessionToken() : null;
  512. const q = new Parse.Query('_User');
  513. q.select('mobile', 'phone');
  514. const freshUser = await q.get(currentUser.id, sessionToken ? { sessionToken } : undefined);
  515. const afterNumbers = collectUserMobiles(freshUser);
  516. console.log('🚦 [traffic] 刷新用户后号码:', { before: beforeNumbers, after: afterNumbers });
  517. if (Array.isArray(afterNumbers) && afterNumbers.length) {
  518. userNumbers = afterNumbers;
  519. }
  520. } catch (e) {
  521. console.warn('🚦 [traffic] 刷新用户手机号失败:', e?.message || e);
  522. }
  523. if (!userNumbers || !userNumbers.length) {
  524. const storageMobile = wx.getStorageSync('user_mobile') || '';
  525. console.log('🚦 [traffic] 仍未获取到手机号,继续暂缓,并安排 800ms 后重试', {
  526. storageMobile
  527. });
  528. try {
  529. const retryInfo = wx.getStorageSync('traffic_deduct_retry') || { count: 0 };
  530. if ((retryInfo.count || 0) < 3) {
  531. wx.setStorageSync('traffic_deduct_retry', { count: (retryInfo.count || 0) + 1 });
  532. setTimeout(() => {
  533. try {
  534. if (typeof getApp().checkTrafficDeductPending === 'function') {
  535. console.log('🚦 [traffic] 重试触发暂缓扣减复判');
  536. getApp().checkTrafficDeductPending();
  537. }
  538. } catch (e) {}
  539. }, 800);
  540. }
  541. } catch (e) {}
  542. return;
  543. }
  544. const storeId = pending && pending.storeId ? pending.storeId : null;
  545. if (!storeId) {
  546. console.warn('🚦 [traffic] 暂缓扣减任务缺少 storeId,清除任务');
  547. wx.removeStorageSync('traffic_deduct_pending');
  548. return;
  549. }
  550. const isExempt = await isTrafficExemptForStore(Parse, storeId, currentUser);
  551. if (isExempt) {
  552. console.log('🚦 [traffic] 暂缓扣减复判:命中白名单,取消扣减并清除暂缓任务');
  553. try {
  554. currentUser.set('trafficExempt', true);
  555. currentUser.set('trafficExemptAt', new Date());
  556. currentUser.set('trafficExemptStoreId', storeId);
  557. await currentUser.save();
  558. } catch (e) {
  559. console.warn('🚦 [traffic] 写入 trafficExempt 失败(不影响继续访问):', e?.message || e);
  560. }
  561. wx.removeStorageSync('traffic_deduct_pending');
  562. return;
  563. }
  564. console.log('🚦 [traffic] 暂缓扣减复判:未命中白名单,执行扣减');
  565. try {
  566. await decrementStoreTrafficImpl(Parse, storeId);
  567. const deductedStores = Array.isArray(currentUser.get('trafficDeductedStores')) ? currentUser.get('trafficDeductedStores') : [];
  568. const updatedStores = Array.isArray(deductedStores) ? deductedStores.slice() : [];
  569. if (!updatedStores.includes(storeId)) updatedStores.push(storeId);
  570. currentUser.set('trafficDeductedStores', updatedStores);
  571. currentUser.set('trafficDeductedStoreId', storeId);
  572. const atMap = currentUser.get('trafficDeductedAtMap') || {};
  573. atMap[storeId] = new Date();
  574. currentUser.set('trafficDeductedAtMap', atMap);
  575. await currentUser.save();
  576. console.log('🚦✅ [traffic] 暂缓扣减复判:已为店铺扣减 1 个流量,storeId:', storeId);
  577. } catch (decErr) {
  578. console.error('🚦❌ [traffic] 暂缓扣减复判:扣减失败:', decErr?.message || decErr);
  579. }
  580. wx.removeStorageSync('traffic_deduct_pending');
  581. } catch (error) {
  582. console.error('🚦❌ [traffic] 处理暂缓扣减任务失败:', error);
  583. }
  584. },
  585. async ensureTrackingUser() {
  586. try {
  587. let currentUser = Parse.User.current();
  588. if (currentUser) {
  589. return currentUser;
  590. }
  591. if (Parse.AnonymousUtils && typeof Parse.AnonymousUtils.logIn === 'function') {
  592. await Parse.AnonymousUtils.logIn();
  593. currentUser = Parse.User.current();
  594. return currentUser || null;
  595. }
  596. return null;
  597. } catch (e) {
  598. console.warn('ensureTrackingUser 失败:', e?.message || e);
  599. return null;
  600. }
  601. },
  602. /**
  603. * 记录用户来源信息到 _User 表
  604. * 根据扫码参数判断来源类型并保存到用户的 source 字段
  605. *
  606. * 来源类型:
  607. * 1. 渠道xxx→异业xxx(老板后台添加的异业)
  608. * 2. 渠道xxx→异业xxx→员工xxx(员工后台添加的异业)
  609. * 3. 员工xxx
  610. * 4. 老板
  611. * 5. 自主进入(无任何推荐)
  612. */
  613. async recordUserSource(scanInfo) {
  614. try {
  615. const currentUser = Parse.User.current();
  616. if (!currentUser) {
  617. console.log('⚠️ 用户未登录,无法记录来源');
  618. return;
  619. }
  620. const existingSource = currentUser.get('source');
  621. console.log('📊 ===========================================');
  622. console.log('📊 [记录来源] 开始记录用户来源信息');
  623. console.log('📊 用户ID:', currentUser.id);
  624. console.log('📊 ===========================================');
  625. const { partnerId, employeeId, ownerId, userId, storeId } = scanInfo;
  626. console.log('📌 [扣减前置] storeId:', storeId || '无');
  627. console.log('📌 [扣减前置] existingSource:', existingSource ? '有' : '无');
  628. console.log('📌 [扣减前置] trafficDeducted:', currentUser.get('trafficDeducted') === true ? 'true' : 'false');
  629. console.log('📌 [扣减前置] trafficDeductedStoreId:', currentUser.get('trafficDeductedStoreId') || '无');
  630. console.log('📌 [扣减前置] trafficDeductedAt:', currentUser.get('trafficDeductedAt') || '无');
  631. try {
  632. const scanSnapshot = {
  633. storeId: wx.getStorageSync('scan_storeId') || null,
  634. sourceType: wx.getStorageSync('scan_sourceType') || null,
  635. sourceId: wx.getStorageSync('scan_sourceId') || null,
  636. ownerId: wx.getStorageSync('scan_ownerId') || null,
  637. employeeId: wx.getStorageSync('scan_employeeId') || null,
  638. partnerId: wx.getStorageSync('scan_partnerId') || null,
  639. userId: wx.getStorageSync('scan_userId') || null,
  640. productId: wx.getStorageSync('scan_productId') || null,
  641. caseId: wx.getStorageSync('scan_caseId') || null,
  642. planId: wx.getStorageSync('scan_planId') || null,
  643. shareUserId: wx.getStorageSync('scan_shareUserId') || null,
  644. scanCount: wx.getStorageSync('scan_scanCount') || null,
  645. rawUrl: wx.getStorageSync('scan_raw_url') || null
  646. };
  647. console.log('🔗 [扫码参数快照] scanInfo:', scanInfo);
  648. console.log('🔗 [扫码参数快照] storage:', scanSnapshot);
  649. } catch (e) {
  650. console.warn('⚠️ 读取扫码参数快照失败:', e?.message || e);
  651. }
  652. // 员工推荐判定与日志
  653. try {
  654. let staffJudge = { isStaff: false, reason: 'no_staff_params' };
  655. if (employeeId) {
  656. staffJudge = { isStaff: true, reason: 'employeeId_param', staffId: employeeId };
  657. } else if (ownerId) {
  658. staffJudge = { isStaff: true, reason: 'ownerId_param', staffId: ownerId };
  659. } else if (userId) {
  660. try {
  661. const uq = new Parse.Query('_User');
  662. const u = await uq.get(userId);
  663. const roles = Array.isArray(u.get('roles')) ? u.get('roles').map(r => String(r).toLowerCase()) : [];
  664. const isStaff = roles.includes('staff') || roles.includes('userstaff') || roles.includes('employee') || roles.includes('sales');
  665. staffJudge = { isStaff, reason: isStaff ? 'user_role_staff' : 'user_role_non_staff', staffId: userId, roles };
  666. } catch (e) {
  667. staffJudge = { isStaff: false, reason: 'user_lookup_failed', error: e?.message || e };
  668. }
  669. }
  670. console.log('👥 [员工推荐判定] 结果:', staffJudge);
  671. } catch (e) {
  672. console.warn('⚠️ [员工推荐判定] 失败:', e?.message || e);
  673. }
  674. // 来源优先级判定与升级覆盖
  675. const pickType = () => {
  676. if (partnerId && employeeId) return 'channel_partner_employee';
  677. if (partnerId) return 'channel_partner';
  678. if (employeeId) return 'employee';
  679. if (ownerId) return 'owner';
  680. if (userId) return 'promoter';
  681. return 'self_entry';
  682. };
  683. const prio = (t) => {
  684. if (t === 'channel_partner_employee') return 4;
  685. if (t === 'owner' || t === 'employee') return 3;
  686. if (t === 'channel_partner') return 2;
  687. if (t === 'promoter') return 1;
  688. return 0;
  689. };
  690. const newType = pickType();
  691. const oldType = existingSource && existingSource.type ? String(existingSource.type) : 'self_entry';
  692. const shouldOverride = prio(newType) > prio(oldType);
  693. console.log('🔀 [来源优先级] oldType:', oldType, 'newType:', newType, 'override:', shouldOverride ? 'YES' : 'NO');
  694. if (!existingSource || shouldOverride) {
  695. if (existingSource && shouldOverride) {
  696. console.log('🔁 [来源升级] 触发覆盖: 从', oldType, '→', newType);
  697. }
  698. let sourceInfo = {
  699. type: 'self_entry',
  700. label: '自主进入',
  701. timestamp: new Date(),
  702. storeId: storeId
  703. };
  704. if (partnerId && employeeId) {
  705. console.log('🔍 [来源类型] 渠道→异业→员工');
  706. try {
  707. const partnerQuery = new Parse.Query('Partner');
  708. const partner = await partnerQuery.get(partnerId);
  709. const partnerName = partner.get('name') || '未知异业';
  710. const channelName = partner.get('channelName') || partner.get('category') || '未知渠道';
  711. const employeeQuery = new Parse.Query('Employee');
  712. const employee = await employeeQuery.get(employeeId);
  713. const employeeName = employee.get('name') || '未知员工';
  714. sourceInfo = {
  715. type: 'channel_partner_employee',
  716. label: `${channelName},${partnerName},员工${employeeName}`,
  717. channelName: channelName,
  718. partnerId: partnerId,
  719. partnerName: partnerName,
  720. employeeId: employeeId,
  721. employeeName: employeeName,
  722. timestamp: new Date(),
  723. storeId: storeId
  724. };
  725. console.log('✅ [来源信息] 渠道→异业→员工:', sourceInfo.label);
  726. } catch (error) {
  727. console.error('❌ 查询异业或员工信息失败:', error);
  728. }
  729. } else if (partnerId && !employeeId) {
  730. console.log('🔍 [来源类型] 渠道→异业');
  731. try {
  732. const partnerQuery = new Parse.Query('Partner');
  733. const partner = await partnerQuery.get(partnerId);
  734. const partnerName = partner.get('name') || '未知异业';
  735. const channelName = partner.get('channelName') || partner.get('category') || '未知渠道';
  736. sourceInfo = {
  737. type: 'channel_partner',
  738. label: `${channelName},${partnerName}`,
  739. channelName: channelName,
  740. partnerId: partnerId,
  741. partnerName: partnerName,
  742. timestamp: new Date(),
  743. storeId: storeId
  744. };
  745. console.log('✅ [来源信息] 渠道→异业:', sourceInfo.label);
  746. } catch (error) {
  747. console.error('❌ 查询异业信息失败:', error);
  748. }
  749. } else if (employeeId && !partnerId) {
  750. console.log('🔍 [来源类型] 员工');
  751. try {
  752. // 兼容管理员端员工表 UserStaff
  753. let employeeName = '未知员工';
  754. try {
  755. const staffQuery = new Parse.Query('UserStaff');
  756. const staff = await staffQuery.get(employeeId);
  757. employeeName = staff.get('name') || staff.get('nickname') || staff.get('mobile') || '管理员端员工';
  758. } catch (e1) {
  759. try {
  760. const employeeQuery = new Parse.Query('Employee');
  761. const employee = await employeeQuery.get(employeeId);
  762. employeeName = employee.get('name') || '未知员工';
  763. } catch (e2) {}
  764. }
  765. sourceInfo = {
  766. type: 'employee',
  767. label: employeeName,
  768. employeeId: employeeId,
  769. employeeName: employeeName,
  770. timestamp: new Date(),
  771. storeId: storeId
  772. };
  773. console.log('✅ [来源信息] 员工:', sourceInfo.label);
  774. } catch (error) {
  775. console.error('❌ 查询员工信息失败:', error);
  776. }
  777. } else if (ownerId) {
  778. console.log('🔍 [来源类型] 老板');
  779. sourceInfo = {
  780. type: 'owner',
  781. label: '老板',
  782. ownerId: ownerId,
  783. timestamp: new Date(),
  784. storeId: storeId
  785. };
  786. console.log('✅ [来源信息] 老板');
  787. } else if (userId) {
  788. console.log('🔍 [来源类型] 推广员');
  789. try {
  790. const userQuery = new Parse.Query('_User');
  791. const promoter = await userQuery.get(userId);
  792. const promoterName = promoter.get('username') || promoter.get('mobile') || '未知推广员';
  793. sourceInfo = {
  794. type: 'promoter',
  795. label: `推广员${promoterName}`,
  796. userId: userId,
  797. promoterName: promoterName,
  798. timestamp: new Date(),
  799. storeId: storeId
  800. };
  801. console.log('✅ [来源信息] 推广员:', sourceInfo.label);
  802. } catch (error) {
  803. console.error('❌ 查询推广员信息失败:', error);
  804. }
  805. } else {
  806. console.log('🔍 [来源类型] 自主进入');
  807. console.log('✅ [来源信息] 自主进入');
  808. }
  809. currentUser.set('source', sourceInfo);
  810. await currentUser.save();
  811. console.log('✅ [保存成功] 用户来源信息已保存');
  812. console.log(' - 来源类型:', sourceInfo.type);
  813. console.log(' - 来源标签:', sourceInfo.label);
  814. console.log('📊 ===========================================');
  815. } else {
  816. console.log('ℹ️ 用户已有来源信息,跳过覆盖:', existingSource);
  817. }
  818. const deductedStores = Array.isArray(currentUser.get('trafficDeductedStores')) ? currentUser.get('trafficDeductedStores') : [];
  819. const hasDeductedForThisStore =
  820. (Array.isArray(deductedStores) && deductedStores.includes(storeId)) ||
  821. (currentUser.get('trafficDeductedStoreId') && currentUser.get('trafficDeductedStoreId') === storeId);
  822. if (storeId && !hasDeductedForThisStore) {
  823. const internal = await isInternalVisit(Parse, storeId, Parse.User.current(), { ownerId, employeeId, partnerId });
  824. if (internal) {
  825. console.log('ℹ️ [扣减跳过] 内部角色访问(老板/异业/员工),不扣减流量');
  826. } else {
  827. const nums = collectUserMobiles(currentUser);
  828. if (!nums || !nums.length) {
  829. console.log('ℹ️ [扣减暂停] 用户未提供手机号,暂缓扣减', { storeId, userId: currentUser.id });
  830. try {
  831. wx.setStorageSync('traffic_deduct_pending', { storeId, userId: currentUser.id, time: Date.now() });
  832. } catch (e) {}
  833. } else {
  834. const isExempt = await isTrafficExemptForStore(Parse, storeId, currentUser);
  835. if (isExempt) {
  836. console.log('ℹ️ [扣减跳过] 命中白名单手机号,跳过扣减流量');
  837. try {
  838. currentUser.set('trafficExempt', true);
  839. currentUser.set('trafficExemptAt', new Date());
  840. currentUser.set('trafficExemptStoreId', storeId);
  841. await currentUser.save();
  842. } catch (e) {
  843. console.warn('⚠️ [扣减跳过] 写入 trafficExempt 失败(不影响继续访问):', e?.message || e);
  844. }
  845. } else {
  846. console.log('🧮 [扣减流量] 检测到新用户首次扣减,准备扣减门店流量');
  847. try {
  848. await decrementStoreTrafficImpl(Parse, storeId);
  849. const updatedStores = Array.isArray(deductedStores) ? deductedStores.slice() : [];
  850. if (!updatedStores.includes(storeId)) updatedStores.push(storeId);
  851. currentUser.set('trafficDeductedStores', updatedStores);
  852. currentUser.set('trafficDeductedStoreId', storeId);
  853. const atMap = currentUser.get('trafficDeductedAtMap') || {};
  854. atMap[storeId] = new Date();
  855. currentUser.set('trafficDeductedAtMap', atMap);
  856. await currentUser.save();
  857. console.log('✅ [扣减成功] 已为店铺扣减 1 个流量,storeId:', storeId);
  858. } catch (decErr) {
  859. console.error('❌ [扣减失败] 扣减门店流量失败:', decErr?.message || decErr);
  860. }
  861. }
  862. }
  863. }
  864. } else if (!storeId) {
  865. console.warn('⚠️ [扣减跳过] storeId 为空,无法扣减 ShopStore.traffic');
  866. } else if (hasDeductedForThisStore) {
  867. console.log('ℹ️ [扣减跳过] 该用户已在该门店扣减过流量,不重复扣减');
  868. }
  869. } catch (error) {
  870. console.error('❌ 记录用户来源失败:', error);
  871. }
  872. },
  873. /**
  874. * 用户登录后检查并记录来源信息
  875. * 如果用户是首次登录且没有来源信息,根据当前的扫码参数记录来源
  876. */
  877. async checkAndRecordUserSourceOnLogin() {
  878. try {
  879. const currentUser = Parse.User.current();
  880. if (!currentUser) {
  881. return;
  882. }
  883. // 检查是否有扫码参数
  884. const scanStoreId = wx.getStorageSync('scan_storeId');
  885. const scanPartnerId = wx.getStorageSync('scan_partnerId');
  886. const scanEmployeeId = wx.getStorageSync('scan_employeeId');
  887. const scanOwnerId = wx.getStorageSync('scan_ownerId');
  888. const scanUserId = wx.getStorageSync('scan_userId');
  889. // 如果有任何扫码参数,记录来源
  890. if (scanStoreId || scanPartnerId || scanEmployeeId || scanOwnerId || scanUserId) {
  891. console.log('📊 检测到扫码参数,记录用户来源');
  892. await this.recordUserSource({
  893. storeId: scanStoreId,
  894. partnerId: scanPartnerId,
  895. employeeId: scanEmployeeId,
  896. ownerId: scanOwnerId,
  897. userId: scanUserId
  898. });
  899. } else {
  900. // 没有任何扫码参数,标记为自主进入
  901. console.log('📊 无扫码参数,标记为自主进入');
  902. await this.recordUserSource({
  903. storeId: wx.getStorageSync('storeId') || null
  904. });
  905. }
  906. } catch (error) {
  907. console.error('❌ 检查并记录用户来源失败:', error);
  908. }
  909. },
  910. async updateUser(id) {
  911. try {
  912. let User = new Parse.Query('_User')
  913. let user = await User.get(id)
  914. let invite = wx.getStorageSync('invite')
  915. //查询邀请人user
  916. let query = new Parse.Query("_User")
  917. query.equalTo('objectId', invite)
  918. let result = await query.first()
  919. if (result && result.id && result.get("invite")?.id == user.id) {
  920. console.error('邀请人不能是自己的下级')
  921. return
  922. }
  923. if (invite && !user.get('invite') && user.id != invite && !user.get('agentLevel')) {
  924. console.log('上下级绑定成功');
  925. user.set('invite', {
  926. __type: "Pointer",
  927. className: "_User",
  928. objectId: invite
  929. })
  930. user.set('agent', {
  931. __type: "Pointer",
  932. className: "_User",
  933. objectId: invite
  934. })
  935. await Parse.Cloud.run('user_save', {
  936. userJson: user.toJSON()
  937. })
  938. }
  939. } catch (error) {
  940. console.error('❌ updateUser 失败:', error.message);
  941. // 不阻断流程,继续执行
  942. }
  943. },
  944. async getCompanyServerExpire(url) {
  945. try {
  946. let query = new Parse.Query('Company')
  947. query.equalTo('objectId', getApp().globalData.company)
  948. query.select('expireDate', 'expireMap')
  949. let com = await query.first()
  950. if (com?.id && com?.get('expireDate')) {
  951. let now = + new Date()
  952. let expireTime = + new Date(com?.get('expireDate'))
  953. if (com?.get('expireMap') && com.get('expireMap')[getApp().globalData.appid]) {
  954. expireTime = + new Date(com.get('expireMap')[getApp().globalData.appid])
  955. }
  956. if (now >= expireTime) {
  957. console.log('服务器到期');
  958. wx.reLaunch({
  959. url: `common-page/pages/loading/index?url=${url}`,
  960. });
  961. return
  962. }
  963. }
  964. return true
  965. } catch (error) {
  966. console.error('❌ getCompanyServerExpire 失败:', error.message);
  967. // 查询失败时,允许继续访问
  968. return true
  969. }
  970. },
  971. onUnload: function () {
  972. wx.setStorageSync("active", 0);
  973. },
  974. getParaName(url) {
  975. if (!url || url.indexOf('?') == -1) {
  976. return
  977. }
  978. // 兼容 URL 中 &amp; 的情况,先统一还原为 &
  979. if (url.indexOf('&amp;') !== -1) {
  980. url = url.replace(/&amp;/g, '&');
  981. }
  982. // 提取查询参数部分(去除可能的 hash 部分)
  983. let queryString = url.split('?')[1];
  984. // 如果包含 #,只取 # 之前的部分
  985. if (queryString.indexOf('#') !== -1) {
  986. queryString = queryString.split('#')[0];
  987. }
  988. return this.setObject(queryString) //封装成对象
  989. },
  990. setObject(paraArr) {
  991. let obj = {}
  992. let arr1 = paraArr.split('&')
  993. arr1.forEach(item => {
  994. let str = item.split('=')
  995. let key = str[0]
  996. let val = str[1]
  997. obj[key] = val
  998. })
  999. return obj
  1000. },
  1001. /**
  1002. * 检查并处理扫码参数
  1003. * 支持所有类型的二维码:
  1004. * 1. 推广员二维码: ?scanCount=0&storeId=xxx&userId=xxx
  1005. * 2. 产品二维码: ?scanCount=0&storeId=xxx&productId=xxx
  1006. * 3. 案例二维码: ?scanCount=0&storeId=xxx&caseId=xxx
  1007. * 4. 活动海报二维码: activityId作为qrCode参数值
  1008. * 5. 异业合作伙伴二维码(移动端 / PC端统一): ?scanCount=xxx&storeId=xxx&partnerId=xxx
  1009. * 6. 员工邀请二维码(移动端): ?scanCount=0&storeId=xxx&employeeId=xxx
  1010. * 7. 我的二维码(老板): ?scanCount=0&storeId=xxx&ownerId=xxx
  1011. * 8. 我的二维码(员工): ?scanCount=0&storeId=xxx&employeeId=xxx
  1012. * 9. 方案分享二维码: ?scanCount=0&storeId=xxx&planId=xxx&shareUserId=xxx
  1013. */
  1014. checkAndHandleScan(options) {
  1015. console.log('🔍🔍🔍 ========================================');
  1016. console.log('🔍 [扫码检测] 开始检查扫码参数');
  1017. console.log('🔍 完整 options 对象:', JSON.stringify(options, null, 2));
  1018. console.log('🔍🔍🔍 ========================================');
  1019. const {
  1020. scanCount,
  1021. ownerId,
  1022. employeeId,
  1023. partnerId,
  1024. storeId,
  1025. productId,
  1026. caseId,
  1027. userId, // 推广员二维码使用userId
  1028. activityId,
  1029. schemeId, // 方案ID(旧参数名)
  1030. planId, // 方案ID(新参数名,优先使用)
  1031. shareUserId, // 分享方案的用户ID
  1032. q // 扫码链接
  1033. } = options;
  1034. // planId 和 schemeId 兼容处理,优先使用 planId
  1035. const finalPlanId = planId || schemeId;
  1036. console.log('📋 [扫码参数] 提取的参数:');
  1037. console.log(' - scanCount:', scanCount || '无');
  1038. console.log(' - storeId:', storeId || '无');
  1039. console.log(' - ownerId:', ownerId || '无');
  1040. console.log(' - employeeId:', employeeId || '无');
  1041. console.log(' - partnerId:', partnerId || '无');
  1042. console.log(' - userId:', userId || '无');
  1043. console.log(' - productId:', productId || '无');
  1044. console.log(' - caseId:', caseId || '无');
  1045. console.log(' - activityId:', activityId || '无');
  1046. console.log(' - planId:', planId || '无');
  1047. console.log(' - schemeId:', schemeId || '无');
  1048. console.log(' - finalPlanId (最终使用):', finalPlanId || '无');
  1049. console.log(' - shareUserId:', shareUserId || '无');
  1050. console.log(' - q (扫码链接):', q || '无');
  1051. // 处理活动海报二维码(activityId作为qrCode参数值)
  1052. if (activityId && !storeId) {
  1053. console.log('🎯 ===========================================');
  1054. console.log('🎯 [活动二维码] 检测到活动海报二维码');
  1055. console.log('🎯 活动ID (activityId):', activityId);
  1056. console.log('🎯 来源类型: 活动海报');
  1057. console.log('🎯 ===========================================');
  1058. // 活动二维码需要跳转到活动页面,不需要跳转到店铺
  1059. wx.setStorageSync('scan_activityId', activityId);
  1060. wx.setStorageSync('need_activity_redirect', true);
  1061. return;
  1062. }
  1063. // 如果存在 storeId,说明是扫码进入具体门店(包括异业分享)
  1064. if (storeId) {
  1065. console.log('🏪 ===========================================');
  1066. console.log('🏪 [店铺扫码] 检测到扫码参数');
  1067. console.log('🏪 店铺ID (storeId):', storeId);
  1068. console.log('🏪 扫码次数 (scanCount):', scanCount || '0');
  1069. // 确定来源类型
  1070. // 规则:
  1071. // 1. 扫描异业码(partnerId)→ sourceType = "partner"
  1072. // 2. 扫描员工码(employeeId、老板/员工)→ sourceType = "sales" 或 "promoter"
  1073. // 3. 扫描其他码(产品码、案例码等)→ sourceType = "scan"
  1074. let sourceType = 'scan'; // 默认为通用扫码
  1075. let sourceId = null;
  1076. if (partnerId) {
  1077. // 异业合作伙伴二维码
  1078. sourceType = 'partner';
  1079. sourceId = partnerId;
  1080. console.log('🤝 [来源识别] 来源类型: 异业合作伙伴 (partner)');
  1081. console.log('🤝 合作伙伴ID (partnerId):', partnerId);
  1082. } else if (employeeId) {
  1083. // 员工二维码(包括老板和员工)
  1084. sourceType = 'sales';
  1085. sourceId = employeeId;
  1086. console.log('👤 [来源识别] 来源类型: 员工展业 (sales)');
  1087. console.log('👤 员工ID (employeeId):', employeeId);
  1088. } else if (ownerId) {
  1089. // 老板二维码
  1090. sourceType = 'sales';
  1091. sourceId = ownerId;
  1092. console.log('👔 [来源识别] 来源类型: 老板展业 (sales)');
  1093. console.log('👔 老板ID (ownerId):', ownerId);
  1094. } else if (userId) {
  1095. // 推广员二维码
  1096. sourceType = 'promoter';
  1097. sourceId = userId;
  1098. console.log('📢 [来源识别] 来源类型: 推广员展业 (promoter)');
  1099. console.log('📢 推广员ID (userId):', userId);
  1100. } else {
  1101. // 其他情况(产品码、案例码、店铺分享等)
  1102. sourceType = 'scan';
  1103. console.log('🏬 [来源识别] 来源类型: 通用扫码 (scan)');
  1104. }
  1105. if (productId) {
  1106. console.log('📦 [目标内容] 产品ID (productId):', productId);
  1107. }
  1108. if (caseId) {
  1109. console.log('📸 [目标内容] 案例ID (caseId):', caseId);
  1110. }
  1111. if (finalPlanId) {
  1112. console.log('📋 [目标内容] 方案ID (planId/schemeId):', finalPlanId);
  1113. console.log('👤 [分享人] 分享用户ID (shareUserId):', shareUserId || '无');
  1114. }
  1115. console.log('🏪 ===========================================');
  1116. // 保存到临时存储,用于后续跳转和统计
  1117. console.log('💾 [保存扫码信息] 开始保存到本地存储...');
  1118. wx.setStorageSync('scan_storeId', storeId);
  1119. wx.setStorageSync('scan_scanCount', scanCount || '0');
  1120. wx.setStorageSync('scan_sourceType', sourceType);
  1121. wx.setStorageSync('scan_sourceId', sourceId || '');
  1122. wx.setStorageSync('scan_ownerId', ownerId || '');
  1123. wx.setStorageSync('scan_employeeId', employeeId || '');
  1124. wx.setStorageSync('scan_partnerId', partnerId || '');
  1125. wx.setStorageSync('scan_userId', userId || '');
  1126. if (productId) {
  1127. wx.setStorageSync('scan_productId', productId);
  1128. }
  1129. if (caseId) {
  1130. wx.setStorageSync('scan_caseId', caseId);
  1131. }
  1132. if (finalPlanId) {
  1133. wx.setStorageSync('scan_planId', finalPlanId);
  1134. if (shareUserId) {
  1135. wx.setStorageSync('scan_shareUserId', shareUserId);
  1136. }
  1137. }
  1138. wx.setStorageSync('need_scan_redirect', true);
  1139. console.log('✅ [保存完成] 扫码信息已保存');
  1140. console.log('📋 [保存内容摘要]:');
  1141. console.log(' - 来源类型:', sourceType);
  1142. console.log(' - 来源ID:', sourceId || '无');
  1143. console.log(' - 店铺ID:', storeId);
  1144. console.log(' - 需要跳转:', true);
  1145. } else if (partnerId) {
  1146. // 理论上异业二维码现在也必须带 storeId,这里仅作为兜底保护
  1147. console.warn('⚠️ 检测到异业合作伙伴二维码缺少 storeId,无法确定门店,请检查二维码生成逻辑');
  1148. }
  1149. },
  1150. /**
  1151. * 判断是否需要跳转到扫码统计页面
  1152. */
  1153. shouldRedirectToScanPage() {
  1154. return wx.getStorageSync('need_scan_redirect') === true;
  1155. },
  1156. /**
  1157. * 跳转到扫码对应的店铺页面
  1158. * 根据 storeId 跳转到对应店铺,同时记录所有来源信息用于统计
  1159. */
  1160. async redirectToScanPage() {
  1161. try {
  1162. console.log('🚀 ===========================================');
  1163. console.log('🚀 [跳转处理] 开始处理扫码跳转');
  1164. console.log('🚀 ===========================================');
  1165. // 获取所有扫码相关参数
  1166. const storeId = wx.getStorageSync('scan_storeId');
  1167. const scanCount = wx.getStorageSync('scan_scanCount');
  1168. const sourceType = wx.getStorageSync('scan_sourceType');
  1169. const sourceId = wx.getStorageSync('scan_sourceId');
  1170. const ownerId = wx.getStorageSync('scan_ownerId');
  1171. const employeeId = wx.getStorageSync('scan_employeeId');
  1172. const partnerId = wx.getStorageSync('scan_partnerId');
  1173. const userId = wx.getStorageSync('scan_userId');
  1174. const productId = wx.getStorageSync('scan_productId');
  1175. const caseId = wx.getStorageSync('scan_caseId');
  1176. const planId = wx.getStorageSync('scan_planId');
  1177. const shareUserId = wx.getStorageSync('scan_shareUserId');
  1178. console.log('📖 [读取存储] 从本地存储读取扫码信息:');
  1179. console.log(' - storeId:', storeId || '无');
  1180. console.log(' - sourceType:', sourceType || '无');
  1181. console.log(' - sourceId:', sourceId || '无');
  1182. console.log(' - scanCount:', scanCount || '无');
  1183. console.log(' - ownerId:', ownerId || '无');
  1184. console.log(' - employeeId:', employeeId || '无');
  1185. console.log(' - partnerId:', partnerId || '无');
  1186. console.log(' - userId:', userId || '无');
  1187. console.log(' - productId:', productId || '无');
  1188. console.log(' - caseId:', caseId || '无');
  1189. console.log(' - planId:', planId || '无');
  1190. console.log(' - shareUserId:', shareUserId || '无');
  1191. // 清除临时存储
  1192. console.log('🧹 [清理存储] 清除临时扫码信息...');
  1193. wx.removeStorageSync('scan_storeId');
  1194. wx.removeStorageSync('scan_scanCount');
  1195. wx.removeStorageSync('scan_sourceType');
  1196. wx.removeStorageSync('scan_sourceId');
  1197. wx.removeStorageSync('scan_ownerId');
  1198. wx.removeStorageSync('scan_employeeId');
  1199. wx.removeStorageSync('scan_partnerId');
  1200. wx.removeStorageSync('scan_userId');
  1201. wx.removeStorageSync('scan_productId');
  1202. wx.removeStorageSync('scan_caseId');
  1203. wx.removeStorageSync('scan_planId');
  1204. wx.removeStorageSync('scan_shareUserId');
  1205. wx.removeStorageSync('need_scan_redirect');
  1206. console.log('✅ [清理完成] 临时存储已清除');
  1207. if (!storeId) {
  1208. console.error('❌ [错误] 缺少 storeId 参数,无法跳转');
  1209. return;
  1210. }
  1211. console.log('📊 ===========================================');
  1212. console.log('📊 [扫码来源汇总] 扫码进入店铺');
  1213. console.log('📊 店铺 ID:', storeId);
  1214. console.log('📊 来源类型:', sourceType);
  1215. console.log('📊 来源 ID:', sourceId || '无');
  1216. console.log('📊 扫码次数:', scanCount);
  1217. if (ownerId) console.log('📊 老板 ID:', ownerId);
  1218. if (employeeId) console.log('📊 员工 ID:', employeeId);
  1219. if (partnerId) console.log('📊 合作伙伴 ID:', partnerId);
  1220. if (userId) console.log('📊 推广员 ID:', userId);
  1221. if (productId) console.log('📊 产品 ID:', productId);
  1222. if (caseId) console.log('📊 案例 ID:', caseId);
  1223. if (planId) {
  1224. console.log('📊 方案 ID:', planId);
  1225. console.log('📊 分享用户 ID:', shareUserId || '无');
  1226. }
  1227. console.log('📊 ===========================================');
  1228. // 设置店铺 ID 到全局和本地存储
  1229. wx.setStorageSync('storeId', storeId);
  1230. getApp().globalData.storeId = storeId;
  1231. // 保存来源信息到本地存储(用于后续统计或绑定关系)
  1232. if (sourceId) {
  1233. wx.setStorageSync('scan_from_sourceType', sourceType);
  1234. wx.setStorageSync('scan_from_sourceId', sourceId);
  1235. console.log('✅ 已记录来源信息:', sourceType, sourceId);
  1236. }
  1237. // 检查用户是否已登录
  1238. const currentUser = Parse.User.current();
  1239. console.log('👤 [登录检查] 检查用户登录状态...');
  1240. console.log(' - 当前用户:', currentUser ? currentUser.id : '未登录');
  1241. console.log(' - 手机号:', currentUser?.get('mobile') || '无');
  1242. if (currentUser) {
  1243. // 用户已登录,立即记录扫码统计
  1244. console.log('✅ [已登录] 用户已登录,立即记录扫码统计');
  1245. await this.recordScanStatistics({
  1246. storeId,
  1247. sourceType,
  1248. sourceId,
  1249. ownerId,
  1250. employeeId,
  1251. partnerId,
  1252. userId,
  1253. productId,
  1254. scanCount,
  1255. scanTimestamp: Date.now()
  1256. });
  1257. } else {
  1258. // 用户未登录,保存扫码信息,等登录后再记录
  1259. console.log('⚠️ [未登录] 用户未登录,保存扫码信息待登录后记录');
  1260. const pendingData = {
  1261. storeId,
  1262. sourceType,
  1263. sourceId,
  1264. ownerId,
  1265. employeeId,
  1266. partnerId,
  1267. userId,
  1268. productId,
  1269. scanCount,
  1270. timestamp: Date.now()
  1271. };
  1272. console.log('💾 [待处理记录] 保存内容:', JSON.stringify(pendingData, null, 2));
  1273. wx.setStorageSync('pending_scan_record', pendingData);
  1274. console.log('✅ [保存成功] 待处理扫码记录已保存');
  1275. }
  1276. // 如果有产品ID,直接跳转到产品详情的 H5 页面
  1277. if (productId) {
  1278. console.log('🎯 检测到产品ID,跳转到产品详情页');
  1279. await this.redirectToProductDetail(storeId, productId, partnerId);
  1280. return;
  1281. }
  1282. // 如果有案例ID,直接跳转到案例详情的 H5 页面
  1283. if (caseId) {
  1284. console.log('🎯 检测到案例ID,跳转到案例详情页');
  1285. await this.redirectToCaseDetail(storeId, caseId, partnerId);
  1286. return;
  1287. }
  1288. // 如果有方案ID,跳转到门店首页并传递方案ID
  1289. if (planId) {
  1290. console.log('🎯 检测到方案ID,跳转到门店首页展示方案');
  1291. await this.redirectToStoreWithPlan(storeId, planId, shareUserId, partnerId);
  1292. return;
  1293. }
  1294. // 获取默认首页路径并跳转
  1295. let url = getApp().globalData.rootPage || getApp().globalData.defaultTabBar.list[0].pagePath;
  1296. url += `?storeId=${storeId}`;
  1297. // 如果有产品ID,添加到URL参数中
  1298. if (productId) {
  1299. url += `&productId=${productId}`;
  1300. }
  1301. // 如果有异业合作伙伴ID,添加到URL参数中传回web-view
  1302. if (partnerId) {
  1303. url += `&partnerId=${partnerId}`;
  1304. }
  1305. console.log('✅ 跳转到店铺页面:', url);
  1306. wx.redirectTo({
  1307. url: url,
  1308. fail: (err) => {
  1309. console.error('❌ 跳转失败:', err);
  1310. // 如果 redirectTo 失败,尝试 reLaunch
  1311. wx.reLaunch({
  1312. url: url,
  1313. fail: (err2) => {
  1314. console.error('❌ reLaunch 也失败:', err2);
  1315. }
  1316. });
  1317. }
  1318. });
  1319. } catch (error) {
  1320. console.error('❌ 跳转到店铺页面失败:', error);
  1321. }
  1322. },
  1323. /**
  1324. * 跳转到活动页面
  1325. */
  1326. async redirectToActivityPage() {
  1327. try {
  1328. const activityId = wx.getStorageSync('scan_activityId');
  1329. // 清除临时存储
  1330. wx.removeStorageSync('scan_activityId');
  1331. wx.removeStorageSync('need_activity_redirect');
  1332. if (!activityId) {
  1333. console.error('❌ 缺少 activityId 参数,无法跳转');
  1334. return;
  1335. }
  1336. console.log('===========================================');
  1337. console.log('======= 扫码进入活动页面 =======');
  1338. console.log('活动 ID (activityId):', activityId);
  1339. console.log('===========================================');
  1340. // 保存活动ID
  1341. wx.setStorageSync('activityId', activityId);
  1342. // 获取默认首页路径并跳转(活动页面可能需要根据实际路由调整)
  1343. let url = getApp().globalData.rootPage || getApp().globalData.defaultTabBar.list[0].pagePath;
  1344. url += `?activityId=${activityId}`;
  1345. console.log('✅ 跳转到活动页面:', url);
  1346. wx.redirectTo({
  1347. url: url,
  1348. fail: (err) => {
  1349. console.error('❌ 跳转失败:', err);
  1350. wx.reLaunch({
  1351. url: url,
  1352. fail: (err2) => {
  1353. console.error('❌ reLaunch 也失败:', err2);
  1354. }
  1355. });
  1356. }
  1357. });
  1358. } catch (error) {
  1359. console.error('❌ 跳转到活动页面失败:', error);
  1360. }
  1361. },
  1362. /**
  1363. * 跳转到产品详情的 H5 页面
  1364. * @param {string} storeId - 店铺 ID
  1365. * @param {string} productId - 产品 ID
  1366. * @param {string} partnerId - 可选的合作伙伴 ID
  1367. */
  1368. async redirectToProductDetail(storeId, productId, partnerId = null) {
  1369. try {
  1370. console.log('===========================================');
  1371. console.log('======= 跳转到产品详情页 =======');
  1372. console.log('店铺 ID:', storeId);
  1373. console.log('产品 ID:', productId);
  1374. if (partnerId) console.log('合作伙伴 ID:', partnerId);
  1375. console.log('===========================================');
  1376. const currentUser = Parse.User.current();
  1377. const token = currentUser ? currentUser.getSessionToken() : null;
  1378. // 构建产品详情的 H5 URL(不要在这里编码,后面统一编码)
  1379. let h5Url = `https://app.fmode.cn/dev/pobingfeng/owner/nav/products?storeId=${storeId}`;
  1380. // 如果有 token,添加到 URL
  1381. if (token) {
  1382. h5Url += `&token=${token}`;
  1383. } else {
  1384. // 如果没有 token,使用游客模式
  1385. h5Url += `&guestMode=true`;
  1386. }
  1387. // 添加产品ID(不要在这里编码,避免双重编码)
  1388. h5Url += `&productId=${productId}`;
  1389. // 如果有合作伙伴ID,也添加到URL中
  1390. if (partnerId) {
  1391. h5Url += `&partnerId=${partnerId}`;
  1392. }
  1393. console.log('🌐 H5 URL:', h5Url);
  1394. // 编码 URL(统一在这里编码一次)
  1395. const encodedUrl = encodeURIComponent(h5Url);
  1396. // 构建 web-view 页面路径
  1397. let webViewPath = `/common-page/pages/web-view/index?path=${encodedUrl}&storeId=${storeId}`;
  1398. console.log('📄 web-view 页面路径:', webViewPath.substring(0, 100) + '...');
  1399. console.log('===========================================');
  1400. wx.redirectTo({
  1401. url: webViewPath,
  1402. success: () => {
  1403. console.log('✅ 跳转到产品详情页成功');
  1404. },
  1405. fail: (err) => {
  1406. console.error('❌ 跳转失败:', err);
  1407. // 如果 redirectTo 失败,尝试 reLaunch
  1408. wx.reLaunch({
  1409. url: webViewPath,
  1410. fail: (err2) => {
  1411. console.error('❌ reLaunch 也失败:', err2);
  1412. }
  1413. });
  1414. }
  1415. });
  1416. } catch (error) {
  1417. console.error('❌ 跳转到产品详情页失败:', error);
  1418. }
  1419. },
  1420. /**
  1421. * 跳转到案例详情的 H5 页面
  1422. * @param {string} storeId - 店铺 ID
  1423. * @param {string} caseId - 案例 ID
  1424. * @param {string} partnerId - 可选的合作伙伴 ID
  1425. */
  1426. async redirectToCaseDetail(storeId, caseId, partnerId = null) {
  1427. try {
  1428. console.log('===========================================');
  1429. console.log('======= 跳转到案例详情页 =======');
  1430. console.log('店铺 ID:', storeId);
  1431. console.log('案例 ID:', caseId);
  1432. if (partnerId) console.log('合作伙伴 ID:', partnerId);
  1433. console.log('===========================================');
  1434. const currentUser = Parse.User.current();
  1435. const token = currentUser ? currentUser.getSessionToken() : null;
  1436. // 构建案例详情的 H5 URL(不要在这里编码,后面统一编码)
  1437. let h5Url = `https://app.fmode.cn/dev/pobingfeng/owner/nav/cases?storeId=${storeId}`;
  1438. // 如果有 token,添加到 URL
  1439. if (token) {
  1440. h5Url += `&token=${token}`;
  1441. } else {
  1442. // 如果没有 token,使用游客模式
  1443. h5Url += `&guestMode=true`;
  1444. }
  1445. // 添加案例ID(不要在这里编码,避免双重编码)
  1446. h5Url += `&caseId=${caseId}`;
  1447. // 如果有合作伙伴ID,也添加到URL中
  1448. if (partnerId) {
  1449. h5Url += `&partnerId=${partnerId}`;
  1450. }
  1451. console.log('🌐 H5 URL:', h5Url);
  1452. // 编码 URL(统一在这里编码一次)
  1453. const encodedUrl = encodeURIComponent(h5Url);
  1454. // 构建 web-view 页面路径
  1455. let webViewPath = `/common-page/pages/web-view/index?path=${encodedUrl}&storeId=${storeId}`;
  1456. console.log('📄 web-view 页面路径:', webViewPath.substring(0, 100) + '...');
  1457. console.log('===========================================');
  1458. wx.redirectTo({
  1459. url: webViewPath,
  1460. success: () => {
  1461. console.log('✅ 跳转到案例详情页成功');
  1462. },
  1463. fail: (err) => {
  1464. console.error('❌ 跳转失败:', err);
  1465. // 如果 redirectTo 失败,尝试 reLaunch
  1466. wx.reLaunch({
  1467. url: webViewPath,
  1468. fail: (err2) => {
  1469. console.error('❌ reLaunch 也失败:', err2);
  1470. }
  1471. });
  1472. }
  1473. });
  1474. } catch (error) {
  1475. console.error('❌ 跳转到案例详情页失败:', error);
  1476. }
  1477. },
  1478. /**
  1479. * 跳转到门店首页并展示方案
  1480. * @param {string} storeId - 店铺 ID
  1481. * @param {string} planId - 方案 ID
  1482. * @param {string} shareUserId - 分享方案的用户 ID
  1483. * @param {string} partnerId - 可选的合作伙伴 ID
  1484. */
  1485. async redirectToStoreWithPlan(storeId, planId, shareUserId = null, partnerId = null) {
  1486. try {
  1487. console.log('===========================================');
  1488. console.log('======= 跳转到门店首页展示方案 =======');
  1489. console.log('店铺 ID:', storeId);
  1490. console.log('方案 ID:', planId);
  1491. if (shareUserId) console.log('分享用户 ID:', shareUserId);
  1492. if (partnerId) console.log('合作伙伴 ID:', partnerId);
  1493. console.log('===========================================');
  1494. const currentUser = Parse.User.current();
  1495. const token = currentUser ? currentUser.getSessionToken() : null;
  1496. // 构建门店首页的 H5 URL,带上方案ID
  1497. let h5Url = `https://app.fmode.cn/dev/pobingfeng/owner/nav/home?storeId=${storeId}`;
  1498. // 如果有 token,添加到 URL
  1499. if (token) {
  1500. h5Url += `&token=${token}`;
  1501. } else {
  1502. // 如果没有 token,使用游客模式
  1503. h5Url += `&guestMode=true`;
  1504. }
  1505. // 添加方案ID(使用planId参数名)
  1506. h5Url += `&planId=${planId}`;
  1507. // 如果有分享用户ID,也添加到URL中
  1508. if (shareUserId) {
  1509. h5Url += `&shareUserId=${shareUserId}`;
  1510. }
  1511. // 如果有合作伙伴ID,也添加到URL中
  1512. if (partnerId) {
  1513. h5Url += `&partnerId=${partnerId}`;
  1514. }
  1515. console.log('🌐 H5 URL:', h5Url);
  1516. // 编码 URL
  1517. const encodedUrl = encodeURIComponent(h5Url);
  1518. // 构建 web-view 页面路径
  1519. let webViewPath = `/common-page/pages/web-view/index?path=${encodedUrl}&storeId=${storeId}`;
  1520. console.log('📄 web-view 页面路径:', webViewPath.substring(0, 100) + '...');
  1521. console.log('===========================================');
  1522. wx.redirectTo({
  1523. url: webViewPath,
  1524. success: () => {
  1525. console.log('✅ 跳转到门店首页展示方案成功');
  1526. },
  1527. fail: (err) => {
  1528. console.error('❌ 跳转失败:', err);
  1529. // 如果 redirectTo 失败,尝试 reLaunch
  1530. wx.reLaunch({
  1531. url: webViewPath,
  1532. fail: (err2) => {
  1533. console.error('❌ reLaunch 也失败:', err2);
  1534. }
  1535. });
  1536. }
  1537. });
  1538. } catch (error) {
  1539. console.error('❌ 跳转到门店首页展示方案失败:', error);
  1540. }
  1541. },
  1542. /**
  1543. * 记录扫码统计信息
  1544. * @param {Object} params - 扫码参数对象
  1545. * @param {string} params.storeId - 店铺 ID
  1546. * @param {string} params.sourceType - 来源类型 (employee/owner/partner/promoter/store)
  1547. * @param {string} params.sourceId - 来源 ID
  1548. * @param {string} params.ownerId - 老板 ID
  1549. * @param {string} params.employeeId - 员工 ID
  1550. * @param {string} params.partnerId - 合作伙伴 ID
  1551. * @param {string} params.userId - 推广员 ID
  1552. * @param {string} params.productId - 产品 ID
  1553. * @param {string} params.scanCount - 扫码次数
  1554. */
  1555. async recordScanStatistics(params) {
  1556. try {
  1557. console.log('📝 ===========================================');
  1558. console.log('📝 [扫码统计] 开始记录扫码统计');
  1559. console.log('📝 ===========================================');
  1560. const {
  1561. storeId,
  1562. sourceType,
  1563. sourceId,
  1564. ownerId,
  1565. employeeId,
  1566. partnerId,
  1567. userId,
  1568. productId,
  1569. scanCount,
  1570. scanTimestamp
  1571. } = params;
  1572. console.log('📋 [统计参数] 接收到的参数:');
  1573. console.log(' - storeId:', storeId);
  1574. console.log(' - sourceType:', sourceType);
  1575. console.log(' - sourceId:', sourceId || '无');
  1576. console.log(' - ownerId:', ownerId || '无');
  1577. console.log(' - employeeId:', employeeId || '无');
  1578. console.log(' - partnerId:', partnerId || '无');
  1579. console.log(' - userId:', userId || '无');
  1580. console.log(' - productId:', productId || '无');
  1581. console.log(' - scanCount:', scanCount || '0');
  1582. // 检查用户是否已登录
  1583. const currentUser = Parse.User.current();
  1584. if (!currentUser) {
  1585. console.log('⚠️ [未登录] 用户未登录,不记录扫码统计');
  1586. return;
  1587. }
  1588. console.log('👤 [用户信息] 当前登录用户:');
  1589. console.log(' - 用户ID:', currentUser.id);
  1590. console.log(' - 手机号:', currentUser.get('mobile') || '无');
  1591. // 如果没有来源信息,不记录统计
  1592. if (!sourceId && !ownerId && !employeeId && !partnerId && !userId) {
  1593. console.log('ℹ️ [跳过] 无来源信息,跳过统计记录');
  1594. return;
  1595. }
  1596. const hasMobile = !!currentUser.get('mobile');
  1597. const scanTs = typeof scanTimestamp === 'number' && scanTimestamp > 0 ? scanTimestamp : Date.now();
  1598. if (partnerId && !hasMobile) {
  1599. const pendingData = {
  1600. storeId,
  1601. sourceType,
  1602. sourceId,
  1603. ownerId,
  1604. employeeId,
  1605. partnerId,
  1606. userId,
  1607. productId,
  1608. scanCount,
  1609. timestamp: scanTs
  1610. };
  1611. try {
  1612. const existingPending = wx.getStorageSync('pending_scan_record');
  1613. if (!existingPending || existingPending.partnerId !== partnerId || existingPending.storeId !== storeId) {
  1614. wx.setStorageSync('pending_scan_record', pendingData);
  1615. }
  1616. } catch (e) {
  1617. try {
  1618. wx.setStorageSync('pending_scan_record', pendingData);
  1619. } catch (e2) {}
  1620. }
  1621. console.log('⏳ [异业扫码] 用户未绑定手机号,延后到注册登录后再统计');
  1622. return;
  1623. }
  1624. console.log('✅ [开始记录] 用户已登录且有来源信息,开始记录');
  1625. // 创建扫码记录
  1626. const ScanRecord = Parse.Object.extend('ScanRecord');
  1627. const record = new ScanRecord();
  1628. record.set('company', {
  1629. __type: 'Pointer',
  1630. className: 'Company',
  1631. objectId: getApp().globalData.company
  1632. });
  1633. record.set('storeId', storeId);
  1634. record.set('sourceType', sourceType || 'unknown');
  1635. record.set('scanCount', parseInt(scanCount) || 0);
  1636. record.set('scanTime', new Date());
  1637. console.log('📦 [记录内容] 准备保存到 ScanRecord 表:');
  1638. console.log(' - company:', getApp().globalData.company);
  1639. console.log(' - storeId:', storeId);
  1640. console.log(' - sourceType:', sourceType || 'unknown');
  1641. console.log(' - scanCount:', parseInt(scanCount) || 0);
  1642. console.log(' - scanTime:', new Date().toISOString());
  1643. // 根据来源类型设置对应的ID
  1644. if (ownerId) {
  1645. record.set('ownerId', ownerId);
  1646. console.log(' - ownerId:', ownerId);
  1647. }
  1648. if (employeeId) {
  1649. record.set('employeeId', employeeId);
  1650. console.log(' - employeeId:', employeeId);
  1651. }
  1652. if (partnerId) {
  1653. record.set('partnerId', partnerId);
  1654. console.log(' - partnerId:', partnerId);
  1655. }
  1656. if (userId) {
  1657. record.set('userId', userId);
  1658. console.log(' - userId:', userId);
  1659. }
  1660. if (productId) {
  1661. record.set('productId', productId);
  1662. console.log(' - productId:', productId);
  1663. }
  1664. // 记录扫码用户
  1665. record.set('user', {
  1666. __type: 'Pointer',
  1667. className: '_User',
  1668. objectId: currentUser.id
  1669. });
  1670. console.log(' - user:', currentUser.id);
  1671. await record.save();
  1672. console.log('✅ [保存成功] 扫码记录已保存到数据库');
  1673. console.log(' - 记录ID:', record.id);
  1674. // 如果存在异业合作伙伴ID,处理异业绑定和扫码次数
  1675. if (partnerId) {
  1676. try {
  1677. console.log('🤝 ===========================================');
  1678. console.log('🤝 [异业处理] 开始处理异业扫码逻辑');
  1679. console.log('🤝 用户ID:', currentUser.id);
  1680. console.log('🤝 异业ID:', partnerId);
  1681. console.log('🤝 门店ID:', storeId);
  1682. console.log('🤝 ===========================================');
  1683. // 检查用户是否已经绑定了异业合作伙伴
  1684. const userBoundPartner = currentUser.get('Partner');
  1685. const userBoundPartnerId = userBoundPartner ? userBoundPartner.id : null;
  1686. console.log('🔍 [绑定检查] 用户已绑定的异业ID:', userBoundPartnerId || '无');
  1687. // 规则1:如果用户已绑定其他异业,不处理当前异业的扫码
  1688. if (userBoundPartnerId && userBoundPartnerId !== partnerId) {
  1689. console.log('⚠️ [规则1] 用户已绑定其他异业,不处理当前异业的扫码');
  1690. console.log(' - 已绑定异业:', userBoundPartnerId);
  1691. console.log(' - 当前扫码异业:', partnerId);
  1692. console.log('🤝 ===========================================');
  1693. return;
  1694. }
  1695. // 规则2:检查该用户在该门店是否已经扫过该异业的码
  1696. console.log('🔍 [重复检查] 检查是否已扫过该异业的码...');
  1697. const ScanRecord = Parse.Object.extend('ScanRecord');
  1698. const scanQuery = new Parse.Query(ScanRecord);
  1699. scanQuery.equalTo('user', {
  1700. __type: 'Pointer',
  1701. className: '_User',
  1702. objectId: currentUser.id
  1703. });
  1704. scanQuery.equalTo('partnerId', partnerId);
  1705. scanQuery.equalTo('storeId', storeId);
  1706. scanQuery.equalTo('partnerNewUserCounted', true);
  1707. const existingScan = await scanQuery.first();
  1708. if (existingScan) {
  1709. console.log('⚠️ [规则2] 该用户在该门店已计入“异业新用户扫码”,不重复计数');
  1710. console.log(' - 计入时间:', existingScan.get('scanTime'));
  1711. console.log(' - 记录ID:', existingScan.id);
  1712. console.log('🤝 ===========================================');
  1713. return;
  1714. }
  1715. let partnerNewUserQualified = false;
  1716. try {
  1717. const createdAtMs = currentUser.createdAt instanceof Date ? currentUser.createdAt.getTime() : 0;
  1718. const diffMs = createdAtMs ? Math.abs(scanTs - createdAtMs) : Number.MAX_SAFE_INTEGER;
  1719. partnerNewUserQualified = diffMs <= 10 * 60 * 1000;
  1720. } catch (e) {
  1721. partnerNewUserQualified = false;
  1722. }
  1723. if (!partnerNewUserQualified) {
  1724. console.log('ℹ️ [规则2] 非“扫码注册登录的新用户”,不计入异业扫码次数');
  1725. console.log('🤝 ===========================================');
  1726. return;
  1727. }
  1728. record.set('partnerNewUserCounted', true);
  1729. console.log('✅ [首次计入] 该用户在该门店首次计入“异业新用户扫码”');
  1730. // 规则3:如果用户还没有绑定异业,绑定当前异业
  1731. if (!userBoundPartnerId) {
  1732. console.log('🔗 [规则3] 用户首次扫异业码,开始绑定异业合作伙伴');
  1733. console.log(' - 异业ID:', partnerId);
  1734. currentUser.set('Partner', {
  1735. __type: 'Pointer',
  1736. className: 'Partner',
  1737. objectId: partnerId
  1738. });
  1739. await currentUser.save();
  1740. console.log('✅ [绑定成功] 异业绑定成功');
  1741. } else {
  1742. console.log('ℹ️ [已绑定] 用户已绑定该异业,无需重复绑定');
  1743. }
  1744. // 规则4:为该异业在该门店增加扫码次数
  1745. console.log('📊 [规则4] 开始更新异业扫码次数...');
  1746. const Partner = Parse.Object.extend('Partner');
  1747. const partnerQuery = new Parse.Query(Partner);
  1748. const partnerObj = await partnerQuery.get(partnerId);
  1749. if (partnerObj) {
  1750. console.log('📋 [异业信息] 找到异业合作伙伴记录');
  1751. console.log(' - 异业ID:', partnerObj.id);
  1752. console.log(' - 异业名称:', partnerObj.get('name') || '未设置');
  1753. // 获取或初始化门店扫码次数映射
  1754. let storeScans = partnerObj.get('storeScans') || {};
  1755. const oldCount = storeScans[storeId] || 0;
  1756. // 为该门店的扫码次数 +1
  1757. storeScans[storeId] = oldCount + 1;
  1758. partnerObj.set('storeScans', storeScans);
  1759. // 同时更新总扫码次数
  1760. const oldTotalCount = partnerObj.get('scanCount') || 0;
  1761. partnerObj.increment('scanCount', 1);
  1762. await partnerObj.save();
  1763. console.log('✅ [更新成功] 异业扫码次数已更新');
  1764. console.log(' - 门店ID:', storeId);
  1765. console.log(' - 该门店扫码次数: %d → %d', oldCount, storeScans[storeId]);
  1766. console.log(' - 总扫码次数: %d → %d', oldTotalCount, oldTotalCount + 1);
  1767. } else {
  1768. console.error('❌ [错误] 未找到异业合作伙伴记录');
  1769. }
  1770. if (record && record.get && record.get('partnerNewUserCounted') === true) {
  1771. await record.save();
  1772. }
  1773. console.log('🤝 ===========================================');
  1774. } catch (e) {
  1775. console.error('❌ [异业处理失败] 处理异业合作伙伴绑定失败:', e);
  1776. console.error(' - 错误信息:', e.message);
  1777. console.error(' - 错误堆栈:', e.stack);
  1778. console.log('🤝 ===========================================');
  1779. }
  1780. }
  1781. } catch (error) {
  1782. console.warn('⚠️ 保存扫码记录失败:', error);
  1783. // 不影响主流程
  1784. }
  1785. }
  1786. });
  1787. function normalizeCnMobile(value) {
  1788. if (value === null || value === undefined) return '';
  1789. const digits = String(value).replace(/\D/g, '');
  1790. if (!digits) return '';
  1791. const last11 = digits.length >= 11 ? digits.slice(-11) : digits;
  1792. if (/^1\d{10}$/.test(last11)) return last11;
  1793. if (/^1\d{10}$/.test(digits)) return digits;
  1794. return '';
  1795. }
  1796. function addCnMobileToSet(set, value) {
  1797. const m = normalizeCnMobile(value);
  1798. if (m) set.add(m);
  1799. }
  1800. function addCnMobilesFromValue(set, value) {
  1801. if (!value) return;
  1802. if (Array.isArray(value)) {
  1803. value.forEach((v) => addCnMobileToSet(set, v));
  1804. return;
  1805. }
  1806. addCnMobileToSet(set, value);
  1807. }
  1808. function addCnMobilesFromParseObject(set, parseObj, keys) {
  1809. if (!parseObj || typeof parseObj.get !== 'function') return;
  1810. keys.forEach((k) => {
  1811. try {
  1812. const v = parseObj.get(k);
  1813. addCnMobilesFromValue(set, v);
  1814. } catch (e) {}
  1815. });
  1816. }
  1817. async function addCnMobileFromUserPointer(set, Parse, pointer) {
  1818. if (!pointer) return;
  1819. if (typeof pointer.get === 'function') {
  1820. addCnMobileToSet(set, pointer.get('mobile'));
  1821. addCnMobileToSet(set, pointer.get('phone'));
  1822. }
  1823. const pointerId = pointer && pointer.id ? pointer.id : null;
  1824. if (!pointerId) return;
  1825. try {
  1826. const q = new Parse.Query('_User');
  1827. q.select('mobile');
  1828. const u = await q.get(pointerId);
  1829. if (u) addCnMobileToSet(set, u.get('mobile'));
  1830. } catch (e) {}
  1831. }
  1832. function collectUserMobiles(currentUser) {
  1833. const set = new Set();
  1834. addCnMobileToSet(set, currentUser && typeof currentUser.get === 'function' ? currentUser.get('mobile') : null);
  1835. addCnMobileToSet(set, currentUser && typeof currentUser.get === 'function' ? currentUser.get('phone') : null);
  1836. try {
  1837. const storageMobile = wx.getStorageSync('user_mobile');
  1838. addCnMobileToSet(set, storageMobile);
  1839. } catch (e) {}
  1840. return Array.from(set);
  1841. }
  1842. function setSample(set, limit) {
  1843. const arr = [];
  1844. let i = 0;
  1845. for (const v of set) {
  1846. arr.push(v);
  1847. i++;
  1848. if (i >= (limit || 5)) break;
  1849. }
  1850. return arr;
  1851. }
  1852. function intersectArrSet(arr, set) {
  1853. return arr.filter((v) => set.has(v));
  1854. }
  1855. async function collectPartnerPhonesForStore(Parse, storeId) {
  1856. const phones = new Set();
  1857. const mobiles = new Set();
  1858. try {
  1859. if (!storeId) return { phones, mobiles };
  1860. const currentUser = Parse.User.current();
  1861. const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
  1862. ? currentUser.getSessionToken()
  1863. : null;
  1864. const requestOptions = sessionToken ? { sessionToken } : undefined;
  1865. const storePointer = new Parse.Object('ShopStore'); storePointer.id = storeId;
  1866. const q = new Parse.Query('Partner');
  1867. q.equalTo('store', storePointer);
  1868. q.limit(200);
  1869. q.select('phone', 'mobile');
  1870. const list = await q.find(requestOptions);
  1871. if (list && list.length) {
  1872. list.forEach((p) => {
  1873. addCnMobileToSet(phones, p.get('phone'));
  1874. addCnMobileToSet(mobiles, p.get('mobile'));
  1875. });
  1876. }
  1877. } catch (e) {
  1878. console.warn('⚠️ [traffic] Partner 查询失败:', e?.code, e?.message || e);
  1879. }
  1880. return { phones, mobiles };
  1881. }
  1882. async function collectTrafficWhitelistMobiles(Parse, storeId) {
  1883. const set = new Set();
  1884. const mobileKeys = [
  1885. 'mobile',
  1886. 'phone'
  1887. ];
  1888. const userPointerKeys = [
  1889. 'owner',
  1890. 'boss',
  1891. 'admin',
  1892. 'manager',
  1893. 'user',
  1894. 'ownerUser',
  1895. 'bossUser',
  1896. 'adminUser',
  1897. 'managerUser',
  1898. 'createdBy'
  1899. ];
  1900. try {
  1901. if (storeId) {
  1902. const q = new Parse.Query('ShopStore');
  1903. q.equalTo('objectId', storeId);
  1904. q.select(...mobileKeys, ...userPointerKeys);
  1905. userPointerKeys.forEach((k) => q.include(k));
  1906. const storeObj = await q.first();
  1907. if (storeObj) {
  1908. addCnMobilesFromParseObject(set, storeObj, mobileKeys);
  1909. for (const k of userPointerKeys) {
  1910. await addCnMobileFromUserPointer(set, Parse, storeObj.get(k));
  1911. }
  1912. }
  1913. }
  1914. } catch (e) {}
  1915. const userStaffPhoneKeys = ['mobile', 'phone'];
  1916. try {
  1917. if (storeId) {
  1918. const flags = getApp().globalData || {};
  1919. const disabled = !!flags.disableUserStaffQuery || wx.getStorageSync('disableUserStaffQuery') === true;
  1920. if (disabled) {
  1921. console.log('⚠️ [traffic] userStaff 查询已禁用(检测到类不存在/无权限)');
  1922. } else {
  1923. const currentUser = Parse.User.current();
  1924. const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
  1925. ? currentUser.getSessionToken()
  1926. : null;
  1927. const requestOptions = sessionToken ? { sessionToken } : undefined;
  1928. const companyId = getApp().globalData.company;
  1929. const companyPointer = new Parse.Object('Company'); companyPointer.id = companyId;
  1930. const q = new Parse.Query('UserStaff');
  1931. q.equalTo('company', companyPointer);
  1932. q.limit(200);
  1933. q.select(...userStaffPhoneKeys);
  1934. const list = await q.find(requestOptions);
  1935. if (list && list.length) {
  1936. list.forEach((staff) => addCnMobilesFromParseObject(set, staff, userStaffPhoneKeys));
  1937. }
  1938. }
  1939. }
  1940. } catch (e) {
  1941. console.warn('⚠️ [traffic] userStaff 查询失败:', e?.code, e?.message || e);
  1942. try {
  1943. const msg = (e?.message || '').toLowerCase();
  1944. if (
  1945. e?.code === 119 ||
  1946. msg.includes('non-existent class') ||
  1947. msg.includes('class: userstaff') ||
  1948. msg.includes('class: userstaff') ||
  1949. msg.includes('class: userstaff')
  1950. ) {
  1951. getApp().globalData.disableUserStaffQuery = true;
  1952. wx.setStorageSync('disableUserStaffQuery', true);
  1953. console.warn('⚠️ [traffic] 已禁用后续 userStaff 查询(类不存在或无权限)');
  1954. }
  1955. } catch (_) {}
  1956. }
  1957. const partnerPhoneKeys = ['mobile', 'phone'];
  1958. try {
  1959. if (storeId) {
  1960. const currentUser = Parse.User.current();
  1961. const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
  1962. ? currentUser.getSessionToken()
  1963. : null;
  1964. const requestOptions = sessionToken ? { sessionToken } : undefined;
  1965. const storePointer = new Parse.Object('ShopStore'); storePointer.id = storeId;
  1966. const q = new Parse.Query('Partner');
  1967. q.equalTo('store', storePointer);
  1968. q.limit(200);
  1969. q.select(...partnerPhoneKeys);
  1970. const list = await q.find(requestOptions);
  1971. if (list && list.length) {
  1972. list.forEach((p) => addCnMobilesFromParseObject(set, p, partnerPhoneKeys));
  1973. }
  1974. }
  1975. } catch (e) {
  1976. console.warn('⚠️ [traffic] Partner 查询失败:', e?.code, e?.message || e);
  1977. }
  1978. return set;
  1979. }
  1980. async function isTrafficExemptForStore(Parse, storeId, currentUser) {
  1981. try {
  1982. if (!currentUser) return false;
  1983. if (currentUser.get('trafficExempt') === true) return true;
  1984. const userNumbers = collectUserMobiles(currentUser);
  1985. const whitelist = await collectTrafficWhitelistMobiles(Parse, storeId);
  1986. const partnerSets = await collectPartnerPhonesForStore(Parse, storeId);
  1987. console.log('🧾 [traffic] 白名单判定开始:', {
  1988. storeId,
  1989. userId: currentUser.id,
  1990. userNumbers
  1991. });
  1992. console.log('🧾 [traffic] Partner 集合:', {
  1993. phoneCount: partnerSets.phones.size,
  1994. phoneSample: setSample(partnerSets.phones, 5),
  1995. mobileCount: partnerSets.mobiles.size,
  1996. mobileSample: setSample(partnerSets.mobiles, 5)
  1997. });
  1998. const partnerPhoneMatches = intersectArrSet(userNumbers, partnerSets.phones);
  1999. if (partnerPhoneMatches.length) {
  2000. console.log('🧾 [traffic] 命中 Partner.phone:', {
  2001. matches: partnerPhoneMatches
  2002. });
  2003. return true;
  2004. }
  2005. const partnerMobileMatches = intersectArrSet(userNumbers, partnerSets.mobiles);
  2006. if (partnerMobileMatches.length) {
  2007. console.log('🧾 [traffic] 命中 Partner.mobile:', {
  2008. matches: partnerMobileMatches
  2009. });
  2010. return true;
  2011. }
  2012. console.log('🧾 [traffic] 聚合白名单:', {
  2013. size: whitelist.size,
  2014. sample: setSample(whitelist, 10)
  2015. });
  2016. const whitelistMatches = intersectArrSet(userNumbers, whitelist);
  2017. if (whitelistMatches.length) {
  2018. console.log('🧾 [traffic] 命中聚合白名单:', {
  2019. matches: whitelistMatches
  2020. });
  2021. return true;
  2022. }
  2023. console.log('🧾 [traffic] 白名单未命中');
  2024. return false;
  2025. } catch (e) {
  2026. console.warn('⚠️ [traffic] 白名单判定失败,按非白名单处理:', e?.message || e);
  2027. return false;
  2028. }
  2029. }
  2030. async function decrementStoreTrafficImpl(Parse, storeId) {
  2031. const currentUser = Parse.User.current();
  2032. const sessionToken = currentUser && typeof currentUser.getSessionToken === 'function'
  2033. ? currentUser.getSessionToken()
  2034. : null;
  2035. const requestOptions = sessionToken ? { sessionToken } : undefined;
  2036. console.log('🧾 [traffic] 开始扣减 ShopStore.traffic');
  2037. console.log('🧾 [traffic] storeId:', storeId);
  2038. console.log('🧾 [traffic] userId:', currentUser ? currentUser.id : '无用户');
  2039. console.log('🧾 [traffic] hasSessionToken:', sessionToken ? 'true' : 'false');
  2040. const storeQuery = new Parse.Query('ShopStore');
  2041. const store = await storeQuery.get(storeId, requestOptions);
  2042. if (!store) {
  2043. console.error('🧾 [traffic] 未找到门店记录, storeId:', storeId);
  2044. throw new Error('未找到门店记录');
  2045. }
  2046. const trafficValue = store.get('traffic');
  2047. const trafficNumber = Number(trafficValue);
  2048. const traffic = Number.isFinite(trafficNumber) ? trafficNumber : 0;
  2049. const next = Math.max(traffic - 1, 0);
  2050. console.log('🧾 [traffic] before trafficValue:', trafficValue);
  2051. console.log('🧾 [traffic] parsed traffic:', traffic);
  2052. console.log('🧾 [traffic] next:', next);
  2053. if (next !== traffic || trafficValue === undefined) {
  2054. store.set('traffic', next);
  2055. const saved = await store.save(null, requestOptions);
  2056. console.log('🧾 [traffic] 保存成功 storeObjectId:', saved && saved.id ? saved.id : store.id);
  2057. }
  2058. console.log('🧾 [traffic] 结束扣减 ShopStore.traffic');
  2059. }
  2060. async function isInternalVisit(Parse, storeId, currentUser, ids) {
  2061. try {
  2062. if (!currentUser) return false;
  2063. const { ownerId, employeeId, partnerId } = ids || {};
  2064. const roles = Array.isArray(currentUser.get('roles')) ? currentUser.get('roles') : [];
  2065. const roleSet = new Set(roles.map(r => String(r).toLowerCase()));
  2066. const isOwnerRole = roleSet.has('owner') || roleSet.has('boss');
  2067. const isEmployeeRole = roleSet.has('employee') || roleSet.has('staff') || roleSet.has('sales');
  2068. const isAdminStaffRole = roleSet.has('userstaff') || roleSet.has('admin_staff') || roleSet.has('manager_staff');
  2069. const isPartnerRole = roleSet.has('partner') || roleSet.has('partner_admin') || roleSet.has('partner_staff');
  2070. if (partnerId && isPartnerRole) return true;
  2071. if (employeeId && isEmployeeRole) return true;
  2072. if (isAdminStaffRole) return true;
  2073. const storeQuery = new Parse.Query('ShopStore');
  2074. const store = await storeQuery.get(storeId);
  2075. const storeOwner = store ? store.get('user') : null;
  2076. if (storeOwner && storeOwner.id === currentUser.id) return true;
  2077. if (ownerId && isOwnerRole) return true;
  2078. if (employeeId && currentUser.id === employeeId) return true;
  2079. if (ownerId && currentUser.id === ownerId) return true;
  2080. return false;
  2081. } catch (e) {
  2082. return false;
  2083. }
  2084. }