index.js 93 KB

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