Просмотр исходного кода

fix(mall): 补齐秒杀库存校验与旧入口跳转

- 加购、立即购买及购物车增量统一校验商品库存、秒杀库存和限购数量
- 详情页按购物车已有数量限制秒杀商品,并保留活动库存参数
- 花束分类和花材旧入口重定向至店铺首页并保留进店参数
shizhongqi 19 часов назад
Родитель
Сommit
b01f02955b

+ 81 - 7
mallApp/src/mixins/cgProduct.js

@@ -52,8 +52,6 @@ export default {
 		}
 	},
 
-	onUnload() {},
-
 	computed: {
 		...mapGetters(["getSelectInfo", "getLimitBuyInfo"]),
 		//当前页面选中的商品列表
@@ -417,6 +415,67 @@ export default {
 			}
 			return this.getLimitBuyPriceInfo(item).total;
 		},
+		/**
+		 * 购物车中与目标花束行匹配的已有数量(按 id + 规格 + 活动类型分行)
+		 * @param {Object} row 待匹配的购物车行结构
+		 * @returns {Number} 已有 bigCount 合计
+		 */
+		getMatchedBouquetCartCount(row) {
+			if (!row) return 0;
+			const list = Array.isArray(this.selectList) ? this.selectList : [];
+			let count = 0;
+			for (let i = 0; i < list.length; i++) {
+				if (this._selectRowMatches(list[i], row)) {
+					count += Number(list[i].bigCount) || 0;
+				}
+			}
+			return count;
+		},
+		/**
+		 * 校验花束本次数量是否超过商品库存 / 秒杀活动库存 / 秒杀限购。
+		 * 不计入客户历史已购(当前没有该数据);existingCount 用于加购合并购物车已有件数。
+		 * @param {Object} goods 带 stock / activityType / activityLimit / activityStock 的商品或购物车行
+		 * @param {Number} buyNum 本次数量
+		 * @param {Number} existingCount 购物车已有数量,立即购买传 0
+		 * @returns {boolean} 是否通过
+		 */
+		assertBouquetBuyQty(goods, buyNum, existingCount = 0) {
+			if (!goods) return false;
+			const num = Number(buyNum) || 0;
+			const exist = Number(existingCount) || 0;
+			const total = exist + num;
+			const stockRaw = goods.stock;
+			const hasStock = stockRaw !== undefined && stockRaw !== null && stockRaw !== '';
+			const stock = Number(stockRaw);
+			const stockSet = Number(goods.stockSet);
+			// 开启库存限制必须校验;未开启时仅在库存是有效数字时校验(避免列表商品缺 stock 被误拦)
+			if (stockSet === 1) {
+				if (!isFinite(stock) || !(stock > 0) || total > stock) {
+					this.$msg('库存不足哦');
+					return false;
+				}
+			} else if (hasStock && isFinite(stock)) {
+				if (!(stock > 0) || total > stock) {
+					this.$msg('库存不足哦');
+					return false;
+				}
+			}
+			if (goods.activityType === 'seckill') {
+				const actStockRaw = goods.activityStock;
+				const hasActStock = actStockRaw !== undefined && actStockRaw !== null && actStockRaw !== '';
+				const actStock = Number(actStockRaw) || 0;
+				if (hasActStock && (!(actStock > 0) || total > actStock)) {
+					this.$msg('库存不足哦');
+					return false;
+				}
+				const limit = Number(goods.activityLimit) || 0;
+				if (limit > 0 && total > limit) {
+					this.$msg('超过限购数量,每人限购' + limit + '件');
+					return false;
+				}
+			}
+			return true;
+		},
 		/**
 		 * 组装花束购物车行(property=0):仅做校验并构造行结构,不写入 Vuex/缓存。
 		 * 「立即购买」复用该行结构,保证与加购的计价、展示口径一致,同时不污染购物车。
@@ -443,11 +502,11 @@ export default {
 				this.$msg('商品还没有价格哦');
 				return null;
 			}
-			const stock = Number(saleGoods.stock != null ? saleGoods.stock : goods.stock);
-			if (stock <= 0 && Number(saleGoods.stockSet) === 1) {
-				this.$msg('库存不足哦');
-				return null;
-			}
+			const stockRaw = saleGoods.stock != null ? saleGoods.stock : goods.stock;
+			const stockNum = Number(stockRaw);
+			const stockSet = Number(saleGoods.stockSet != null ? saleGoods.stockSet : goods.stockSet) || 0;
+			// 未开启库存限制且没有有效库存时,用 9999 占位,与后端 stockSet=0 的写法一致
+			const stock = isFinite(stockNum) ? stockNum : (stockSet === 1 ? 0 : 9999);
 			const row = {
 				id: goods.id,
 				goodsId: goods.id,
@@ -465,6 +524,7 @@ export default {
 				freightType: goods.freightType,
 				weight: saleGoods.weight != null ? saleGoods.weight : (goods.weight || 0),
 				stock: stock,
+				stockSet: stockSet,
 				bigNum: stock > 0 ? stock : 9999,
 				smallNum: 0,
 				bigCount: buyNum,
@@ -479,6 +539,15 @@ export default {
 				row.activityType = 'seckill';
 				row.activityEndTime = Number(goods.activityEndTime) || 0;
 				row.originPrice = Number(goods.originPrice) || 0;
+				row.activityLimit = Number(goods.activityLimit) || 0;
+				// 仅在明确带了活动库存时写入,避免列表加购缺该字段时被当成售罄
+				if (goods.activityStock !== undefined && goods.activityStock !== null && goods.activityStock !== '') {
+					row.activityStock = Number(goods.activityStock) || 0;
+				}
+			}
+			// 立即购买按本次数量校验;加购合并数量在 addBouquetToCart 里再验一次
+			if (!this.assertBouquetBuyQty(row, buyNum, 0)) {
+				return null;
 			}
 			return row;
 		},
@@ -494,6 +563,11 @@ export default {
 				return false;
 			}
 			const buyNum = Number(row.bigCount) || 1;
+			const existing = this.getMatchedBouquetCartCount(row);
+			// 加购要合并购物车已有件数后再验库存/限购,避免分次加购绕过秒杀限购
+			if (!this.assertBouquetBuyQty(row, buyNum, existing)) {
+				return false;
+			}
 			const list = Array.isArray(this.selectList) ? [...this.selectList] : [];
 			let found = false;
 			for (let i = 0; i < list.length; i++) {

+ 29 - 0
mallApp/src/pages/goods/components/buy-foot.vue

@@ -2,6 +2,7 @@
   商品详情底部操作栏
   用途:左侧购物车/咨询入口 + 右侧加购/购买(或询价)
   使用方:pages/goods/detail
+  加购/购买点击时先拦商品库存与秒杀活动库存,不足则不打开选规格弹层
 -->
 <template>
 	<view class="app-footer">
@@ -47,6 +48,16 @@ export default {
         cartCount: {
             type: Number,
             default: 0
+        },
+        /** 是否为进行中的秒杀商品:用于点击时拦截秒杀库存 */
+        isSeckill: {
+            type: Boolean,
+            default: false
+        },
+        /** 秒杀活动库存;秒杀售罄时在点击加购/购买时直接拦截 */
+        activityStock: {
+            type: Number,
+            default: 0
         }
     },
     computed: {
@@ -56,10 +67,28 @@ export default {
         }
     },
 	methods: {
+        /**
+         * 点击加购/购买前的库存拦截:库存不足时不打开选规格弹层。
+         * 商品库存用 data.stock;秒杀另看 activityStock(售罄即拦)。
+         * @returns {boolean} 是否允许继续
+         */
+        assertStock() {
+            if (!(Number(this.stock) > 0)) {
+                this.$msg('库存不足哦')
+                return false
+            }
+            if (this.isSeckill && !(Number(this.activityStock) > 0)) {
+                this.$msg('库存不足哦')
+                return false
+            }
+            return true
+        },
 		buy() {
+            if (!this.assertStock()) return
 			this.$emit('buy')
 		},
         cart() {
+            if (!this.assertStock()) return
             this.$emit('cart')
         },
         /** 跳转购物车页,由父组件拼接 account/hdId 后导航 */

+ 94 - 1
mallApp/src/pages/goods/components/sel-popup.vue

@@ -1,3 +1,8 @@
+<!--
+  商品详情选规格/数量弹层
+  用途:加购或立即购买前选择规格与数量;秒杀会按限购、活动库存收紧可选上限
+  使用方:pages/goods/detail
+-->
 <template>
   <view class="sel-popup">
     <bottom-popup class="popup-wrap" :show="show" @close="close">
@@ -83,6 +88,26 @@ export default {
       type: String,
       default: 'buy',
     },
+    /** 是否为进行中的秒杀:用于限购与活动库存校验 */
+    isSeckill: {
+      type: Boolean,
+      default: false,
+    },
+    /** 秒杀每人限购件数;0 表示不限购 */
+    activityLimit: {
+      type: Number,
+      default: 0,
+    },
+    /** 秒杀活动库存;秒杀场景下与商品库存取更小值 */
+    activityStock: {
+      type: Number,
+      default: 0,
+    },
+    /** 购物车中该秒杀商品已有数量(不含历史已购);加购时从限购额度中扣减 */
+    cartSeckillCount: {
+      type: Number,
+      default: 0,
+    },
   },
   data() {
     return {
@@ -106,7 +131,25 @@ export default {
     },
     maxStock() {
       const spec = this.info.specEnabled == 1 && this.info.specList ? this.info.specList[this.selIndex] : this.info;
-      return Number(spec && spec.stockSet == 1 ? spec.stock : 9999) || 9999;
+      let max = Number(spec && spec.stockSet == 1 ? spec.stock : 9999) || 9999;
+      const goodsStock = Number(this.info && this.info.stock);
+      // 商品本身有库存时,数量上限不超过 data.stock(未开启库存限制时后端会写成 9999)
+      if (goodsStock > 0) {
+        max = Math.min(max, goodsStock);
+      }
+      if (this.isSeckill) {
+        const actStock = Number(this.activityStock) || 0;
+        if (actStock > 0) {
+          max = Math.min(max, actStock);
+        }
+        const limit = Number(this.activityLimit) || 0;
+        if (limit > 0) {
+          // 加购/立即购买都扣减购物车已有件数(不计历史已购);否则车内已达限购仍能再买
+          const remain = limit - (Number(this.cartSeckillCount) || 0);
+          max = Math.min(max, remain > 0 ? remain : 0);
+        }
+      }
+      return max > 0 ? max : 1;
     },
   },
   methods: {
@@ -145,8 +188,58 @@ export default {
         this.$msg("商品还没有价格哦");
         return null;
       }
+      if (!this.assertBuyQty(spec)) {
+        return null;
+      }
       return { spec, ok: true };
     },
+    /**
+     * 确认加购/立即购买前校验数量:商品库存、秒杀活动库存、秒杀限购。
+     * 不计入客户历史已购(当前没有该数据);加购会扣减购物车已有数量。
+     * @param {Object|null} spec 当前规格,无规格时用 info
+     * @returns {boolean} 是否通过
+     */
+    assertBuyQty(spec) {
+      const buyNum = Number(this.buyNum) || 0;
+      if (buyNum <= 0) {
+        this.$msg('请选择数量');
+        return false;
+      }
+      const sale = spec || this.info || {};
+      const goodsStock = Number(this.info && this.info.stock);
+      const specStock = Number(sale.stock);
+      const stockSet = Number(sale.stockSet != null ? sale.stockSet : (this.info && this.info.stockSet));
+      // 商品库存不足,或本次数量超过库存
+      if (!(goodsStock > 0) || (stockSet === 1 && !(specStock > 0))) {
+        this.$msg('库存不足哦');
+        return false;
+      }
+      if (stockSet === 1 && buyNum > specStock) {
+        this.$msg('库存不足哦');
+        return false;
+      }
+      if (goodsStock > 0 && buyNum > goodsStock) {
+        this.$msg('库存不足哦');
+        return false;
+      }
+      if (!this.isSeckill) {
+        return true;
+      }
+      const actStock = Number(this.activityStock) || 0;
+      if (!(actStock > 0) || buyNum > actStock) {
+        this.$msg('库存不足哦');
+        return false;
+      }
+      const limit = Number(this.activityLimit) || 0;
+      if (limit > 0) {
+        const used = Number(this.cartSeckillCount) || 0;
+        if (used + buyNum > limit) {
+          this.$msg('超过限购数量,每人限购' + limit + '件');
+          return false;
+        }
+      }
+      return true;
+    },
     /**
      * 立即购买:校验规格/价格后回传父组件
      * 父组件负责写入 Vuex 购物车并跳转混合结算页 affirmMix(弃用 order/buy)

+ 39 - 1
mallApp/src/pages/goods/detail.vue

@@ -89,6 +89,10 @@
       :hdId="hdId"
       :account="account"
       :mode="popupMode"
+      :is-seckill="isSeckillActive"
+      :activity-limit="activityLimit"
+      :activity-stock="activityStock"
+      :cart-seckill-count="currentSeckillCartCount"
       @change="changeNum"
       @changeSpec="changeSpec"
       @close="hidePopup"
@@ -103,6 +107,8 @@
       :stock="data.stock"
       :priceType="data.priceType"
       :cart-count="cartBadgeCount"
+      :is-seckill="isSeckillActive"
+      :activity-stock="activityStock"
       @goChat="goChat"
     />
   </view>
@@ -154,6 +160,8 @@ export default {
       activityType: '',
       activityPrice: 0,
       activityLimit: 0,
+      /** 秒杀活动库存,来自首页/更多列表跳转 query;售罄时在加购/购买入口拦截 */
+      activityStock: 0,
       activityEndTime: 0,
       seckillCountdownText: '',
       _seckillTimer: null,
@@ -218,6 +226,22 @@ export default {
       const count = this.allCount || {};
       return (Number(count.bigLength) || 0) + (Number(count.smallLength) || 0);
     },
+    /**
+     * 购物车中当前秒杀商品已选数量(不计客户历史已购,因暂无该数据)。
+     * 按主商品 id 汇总各规格,用于加购前判断是否已达限购。
+     */
+    currentSeckillCartCount() {
+      if (!this.isSeckillActive) return 0;
+      const goodsId = Number(this.data.masterId) > 0 ? Number(this.data.masterId) : Number(this.data.id);
+      if (!goodsId) return 0;
+      let count = 0;
+      (this.selectList || []).forEach((el) => {
+        if (el && el.activityType === 'seckill' && String(el.id) === String(goodsId)) {
+          count += Number(el.bigCount) || 0;
+        }
+      });
+      return count;
+    },
   },
   methods: {
     /** 解析首页秒杀专区/更多列表带过来的活动参数 */
@@ -225,6 +249,7 @@ export default {
       this.activityType = (option && option.activityType) || '';
       this.activityPrice = parseFloat(option && option.activityPrice) || 0;
       this.activityLimit = parseInt(option && option.activityLimit, 10) || 0;
+      this.activityStock = parseInt(option && option.activityStock, 10) || 0;
       this.activityEndTime = parseInt(option && option.activityEndTime, 10) || 0;
       if (this.activityType === 'seckill') {
         this.restartSeckillCountdown();
@@ -274,6 +299,7 @@ export default {
         activityType: 'seckill',
         activityEndTime: this.activityEndTime,
         activityLimit: this.activityLimit,
+        activityStock: this.activityStock,
       };
     },
     init() {
@@ -522,8 +548,20 @@ export default {
         type: 2,
       });
     },
+    /**
+     * 打开选规格/数量弹层。秒杀若购物车已达限购,加购和立即购买都在此拦截。
+     * 库存不足由底部栏点击时先行拦截;这里再拦限购是因为要看购物车已有数量。
+     */
     showPopup(mode = 'buy') {
-      this.popupMode = mode === 'cart' ? 'cart' : 'buy';
+      const popupMode = mode === 'cart' ? 'cart' : 'buy';
+      if (this.isSeckillActive && Number(this.activityLimit) > 0) {
+        if (this.currentSeckillCartCount >= this.activityLimit) {
+          this.$msg('超过限购数量,每人限购' + this.activityLimit + '件');
+          return;
+        }
+      }
+      this.buyNum = 1;
+      this.popupMode = popupMode;
       this.popupShow = true;
     },
     /** 拼混合结算页地址(与花材详情立即购买一致) */

+ 16 - 0
mallApp/src/pages/home/cart.vue

@@ -544,6 +544,10 @@ export default {
       }
       const big = Number(item.bigCount) || 0
       const small = this.isBouquetItem(item) ? 0 : (Number(item.smallCount) || 0)
+      // 花束:加数量时同步校验商品库存与秒杀限购,避免拖到「去结算」才提示
+      if (this.isBouquetItem(item) && !this.assertBouquetBuyQty(item, 1, big)) {
+        return
+      }
       this.updateCartQty(item, big + 1, small)
       if (this.$util.hitRemind) {
         this.$util.hitRemind()
@@ -661,6 +665,18 @@ export default {
         this.$msg('请选择要结算的商品')
         return
       }
+      // 秒杀限购兜底:正常应在加购/加数量时拦住;已在车内的超量行仍在此拦截,避免拖到结算页才失败
+      const seckillOver = list.find((item) => {
+        if (item.activityType !== 'seckill') {
+          return false
+        }
+        const limit = Number(item.activityLimit) || 0
+        return limit > 0 && (Number(item.bigCount) || 0) > limit
+      })
+      if (seckillOver) {
+        this.$msg('超过限购数量,每人限购' + seckillOver.activityLimit + '件')
+        return
+      }
       const ghsId = Number(this.shopAccount) || Number(uni.getStorageSync('account')) || 0
       if (!ghsId) {
         this.$msg('缺少店铺信息')

+ 49 - 25
mallApp/src/pages/home/category.vue

@@ -1,5 +1,8 @@
+<!--
+  花束分类页(旧入口)。太阳码/分享仍可能打开本页,进入后立刻转到店铺首页,避免先渲染旧分类再跳。
+-->
 <template>
-  <view class="app-main app-content">
+  <view class="app-main app-content" v-if="!redirectingToHome">
 
 		<view class="c_header" :class="myClass">
 			<view class="header_inside">
@@ -96,7 +99,7 @@ import AppWrapperEmpty from '@/components/app-wrapper-empty'
 import AppSearchModule from '@/components/item/module/app-search'
 import { getClass, getList } from '@/api/category'
 import { list,share } from '@/mixins'
-import { getInfo } from "@/api/shop"
+import { redirectToShopHome } from '@/utils/launchScene'
 export default {
   name: 'category',
   components: {
@@ -131,12 +134,20 @@ export default {
         { name: '仿真花', value: 1 },
         { name: '买绿植', value: 99 }
       ],
-      currentFilterTab: 0 // 当前选中的筛选标签
+      currentFilterTab: 0, // 当前选中的筛选标签
+      redirectingToHome: true // 默认不渲染旧页,进入即跳店铺首页
     }
   },
 	computed: {
     ...mapGetters({ loginInfo:"getLoginInfo" })
 	},
+  /**
+   * 全局 mixin 的 onLoad 之后执行。
+   * 未登录时 mixin 会先走 wxLogin 再 init,这里不等登录,立刻跳首页。
+   */
+  onLoad() {
+    this.goShopHome()
+  },
   onShareAppMessage(res) {
 		return {
 			title: '商城',
@@ -191,29 +202,42 @@ export default {
     loginSuccess(){
       this.init()
     },
+    /**
+     * 已登录时全局 mixin 会在页面 onLoad 前调用 init。
+     * 这里只跳店铺首页,不再拉分类数据,避免旧页闪一下。
+     */
+    goShopHome() {
+      if (this._redirectedToHome) {
+        return
+      }
+      this._redirectedToHome = true
+      this.redirectingToHome = true
+      redirectToShopHome(this.option)
+    },
     init(){
-      this.hdId = this.option.hdId ? this.option.hdId : 0
-      this.classInit()
-			getInfo({hdId:this.hdId}).then(res=>{
-				if(res.code == 1){
-					if(res.data.info && res.data.info.merchantName){
-						let sjName = res.data.info.merchantName
-						let shopName = res.data.info.shopName ? res.data.info.shopName : '首店'
-						let name = shopName == '首店' ? sjName : sjName+' '+shopName
-						this.shopName = name
-						this.shopImg = res.data.info.avatar
-					}
-
-					if(res.data.hd && Number(res.data.hd.id) > 0){
-						if(Number(this.hdId) == 0){
-							this.hdId = res.data.hd.id
-						}
-					}
-
-					//保存当前所在门店信息
-					uni.setStorageSync('currentShop', res.data.info)
-				}
-			})
+      // this.hdId = this.option.hdId ? this.option.hdId : 0
+      // this.classInit()
+			// getInfo({hdId:this.hdId}).then(res=>{
+			// 	if(res.code == 1){
+			// 		if(res.data.info && res.data.info.merchantName){
+			// 			let sjName = res.data.info.merchantName
+			// 			let shopName = res.data.info.shopName ? res.data.info.shopName : '首店'
+			// 			let name = shopName == '首店' ? sjName : sjName+' '+shopName
+			// 			this.shopName = name
+			// 			this.shopImg = res.data.info.avatar
+			// 		}
+
+			// 		if(res.data.hd && Number(res.data.hd.id) > 0){
+			// 			if(Number(this.hdId) == 0){
+			// 				this.hdId = res.data.hd.id
+			// 			}
+			// 		}
+
+			// 		//保存当前所在门店信息
+			// 		uni.setStorageSync('currentShop', res.data.info)
+			// 	}
+			// })
+      this.goShopHome()
     },
     // 搜索输入处理(防抖)
     searchFn() {

+ 30 - 4
mallApp/src/pages/item/item.vue

@@ -1,5 +1,8 @@
+<!--
+  花材选购页(旧入口)。太阳码/分享仍可能打开本页,进入后立刻转到店铺首页,避免先渲染旧花材页再跳。
+-->
 <template>
-	<view class="billing_box_bg">
+	<view class="billing_box_bg" v-if="!redirectingToHome">
 
 		<view class="c_header fadeIn">
 			<view class="header_inside">
@@ -153,6 +156,7 @@ import productMins from "@/mixins/cgProduct";
 import { COMMODITY_TYPE } from "@/utils/declare";
 import { getInfo } from "@/api/shop";
 import { getLimitBuyInfo } from "@/api/order";
+import { redirectToShopHome } from '@/utils/launchScene';
 export default {
 	name: "item",
 	components: {
@@ -204,11 +208,20 @@ export default {
 			shopName:'',
 			shopImg:'',
 			hdId:0,
-			autoLoad:false
+			autoLoad:false,
+			redirectingToHome: true // 默认不渲染旧页,进入即跳店铺首页
 		};
 	},
 	onPullDownRefresh() {
 	},
+	/**
+	 * 全局 mixin 的 onLoad 之后执行。
+	 * 未登录时 mixin 会先走 wxLogin 再 init,这里不等登录,立刻跳首页。
+	 */
+	onLoad(){
+		// this.getBaseInfo()
+		this.goShopHome()
+	},
 	computed: {
     	...mapGetters({ loginInfo:"getLoginInfo" })
 	},
@@ -303,7 +316,19 @@ export default {
 			return `超出限购${info.currentBuyNum + info.exceedNum}份`
 		},
 		loginSuccess() {
-			this.getBaseInfo()
+			this.goShopHome()
+		},
+		/**
+		 * 已登录时全局 mixin 会在页面 onLoad 前调用 init。
+		 * 这里只跳店铺首页,不再拉花材数据,避免旧页闪一下。
+		 */
+		goShopHome() {
+			if (this._redirectedToHome) {
+				return
+			}
+			this._redirectedToHome = true
+			this.redirectingToHome = true
+			redirectToShopHome(this.option)
 		},
 		//去特殊品种页,如小菊等
 		goToSpecialVariety(info){
@@ -336,7 +361,8 @@ export default {
       		this.$util.pageTo({url: "/pages/item/detail?id="+item.id+"&account="+account+'&hdId='+realHdId})
 		},
 		init(){
-			this.getBaseInfo()
+			// this.getBaseInfo()
+			this.goShopHome()
 		},
 		getBaseInfo(){
 			this.init = false

+ 22 - 0
mallApp/src/utils/launchScene.js

@@ -98,6 +98,28 @@ export function buildLaunchUrl(path, query) {
 	return parts.length ? normalizedPath + '?' + parts.join('&') : normalizedPath
 }
 
+/**
+ * 旧入口页(花束分类 / 花材)立刻转到店铺首页。
+ * 太阳码、分享链接仍可能打开 category / item,需带上 account、hdId、邀请人等参数,
+ * 避免进店后丢门店或拉新绑定。redirectTo 失败时用 reLaunch 兜底。
+ */
+export function redirectToShopHome(option) {
+	const query = option || {}
+	const url = buildLaunchUrl('/pages/home/index', {
+		account: query.account || uni.getStorageSync('account') || '',
+		hdId: query.hdId || uni.getStorageSync('hdId') || '',
+		inviterCustomId: query.inviterCustomId || '',
+		staffId: query.staffId || '',
+		token: query.token || ''
+	})
+	uni.redirectTo({
+		url,
+		fail() {
+			uni.reLaunch({ url })
+		}
+	})
+}
+
 /**
  * 写入 account / hdId / currentQuery(与原 globalMixins 一致)
  * hdId:仅当本次入参带有效值时写入;缺省不覆盖本地已有绑定,