Browse Source

feat(seckill): 完善秒杀多规格配置与下单展示

- 支持后台秒杀商品按多规格展开配置,并保留规格名、原价和实际库存

- 在商城秒杀详情、购物车、结算和确认订单链路展示秒杀价并透传活动标记

- 区分秒杀与普通购物车行,避免同商品不同计价规则被合并
shizhongqi 1 week ago
parent
commit
f67938277f

+ 3 - 1
hdApp/src/admin/homePageConfig/seckill.vue

@@ -217,6 +217,7 @@ export default {
     },
     /** 将接口数据规范为表单结构 */
     normalizeForm(data) {
+      // specName 仅草稿/编辑页使用;多规格保存时每规格一条,goodsId 为子规格 id
       const goods = Array.isArray(data.goods) ? data.goods.map((g) => ({
         goodsId: g.goodsId || 0,
         price: g.price || '',
@@ -226,7 +227,8 @@ export default {
         name: g.name || '',
         cover: g.cover || '',
         originPrice: g.originPrice || 0,
-        realStock: g.realStock !== undefined ? g.realStock : (g.stock || 0)
+        realStock: g.realStock !== undefined ? g.realStock : (g.stock || 0),
+        specName: g.specName || ''
       })) : []
       const startTime = parseInt(data.startTime, 10) || 0
       const endTime = parseInt(data.endTime, 10) || 0

+ 241 - 72
hdApp/src/admin/homePageConfig/seckillGoodsEdit.vue

@@ -1,56 +1,86 @@
 <!--
   秒杀商品编辑页
   从秒杀配置页跳转,单选活动商品并填写秒杀价/库存/限购;通过 seckillDraft 与列表页同步。
+  单规格:一套表单;多规格:按规格数动态生成多套表单,保存时每规格各写一条秒杀商品(goodsId 用子规格 id)。
 -->
 <template>
   <view class="page-container">
     <view class="module-com input-line-wrap form-card">
       <tui-list-cell class="line-cell" :arrow="true" :hover="true" @click="selectGoods">
         <view class="tui-title required">活动商品</view>
-        <view class="tui-input picker-text" :class="{ placeholder: !goodsForm.name }">
-          {{ goodsForm.name || '请选择活动商品' }}
+        <view class="tui-input picker-text" :class="{ placeholder: !goodsBase.name }">
+          {{ goodsBase.name || '请选择活动商品' }}
         </view>
       </tui-list-cell>
 
-      <tui-list-cell v-if="goodsForm.cover" class="line-cell" :hover="false">
-        <image class="preview-cover" :src="imgFullUrl(goodsForm.cover)" mode="aspectFill" />
+      <tui-list-cell v-if="goodsBase.cover" class="line-cell" :hover="false" :last="!specForms.length">
+        <image class="preview-cover" :src="imgFullUrl(goodsBase.cover)" mode="aspectFill" />
       </tui-list-cell>
+    </view>
+
+    <!-- 单规格一套表单;多规格按规格数动态多套,字段一致 -->
+    <view
+      v-for="(item, sIndex) in specForms"
+      :key="sIndex"
+      class="module-com input-line-wrap form-card"
+    >
+      <view v-if="isMultiSpec" class="spec-head">
+        <text class="spec-head-label">规格{{ sIndex + 1 }}</text>
+        <text class="spec-head-name">{{ item.specName || '未命名规格' }}</text>
+      </view>
 
       <tui-list-cell class="line-cell" :hover="false">
         <view class="tui-title required">秒杀价格</view>
         <input
-          v-model="goodsForm.price"
+          :value="item.price"
           class="tui-input"
           placeholder-class="phcolor"
           placeholder="请输入秒杀价格"
           type="digit"
+          :data-index="sIndex"
+          data-field="price"
+          @input="onSpecInput"
         />
       </tui-list-cell>
 
       <tui-list-cell class="line-cell" :hover="false">
         <view class="tui-title required">秒杀库存</view>
         <input
-          v-model="goodsForm.stock"
+          :value="item.stock"
           class="tui-input"
           placeholder-class="phcolor"
-          :placeholder="stockPlaceholder"
+          :placeholder="item.realStock > 0 ? '不超过实际库存' + item.realStock : '请输入秒杀库存'"
           type="number"
+          :data-index="sIndex"
+          data-field="stock"
+          @input="onSpecInput"
         />
       </tui-list-cell>
 
-      <tui-list-cell class="line-cell" :hover="false" :last="true">
+      <tui-list-cell class="line-cell" :hover="false">
         <view class="tui-title required">单人限购</view>
         <input
-          v-model="goodsForm.limit"
+          :value="item.limit"
           class="tui-input"
           placeholder-class="phcolor"
           placeholder="请输入单人限购数量"
           type="number"
+          :data-index="sIndex"
+          data-field="limit"
+          @input="onSpecInput"
         />
       </tui-list-cell>
-    </view>
 
-    <view v-if="goodsForm.realStock > 0" class="tip-text">商品实际库存:{{ goodsForm.realStock }}</view>
+      <!-- 补上商品原价格 -->
+      <tui-list-cell class="line-cell" :hover="false">
+        <view class="tui-title">商品原价格</view>
+        <view class="tui-input real-stock-text">{{ item.originPrice > 0 ? item.originPrice : '--' }}</view>
+      </tui-list-cell>
+      <tui-list-cell class="line-cell" :hover="false" :last="true">
+        <view class="tui-title">商品实际库存</view>
+        <view class="tui-input real-stock-text">{{ item.realStock > 0 ? item.realStock : '--' }}</view>
+      </tui-list-cell>
+    </view>
 
     <view class="bottom-bar">
       <button class="admin-button-com big default bottom-btn" @click="cancelFn">取消</button>
@@ -61,19 +91,28 @@
 
 <script>
 import TuiListCell from '@/components/plugin/list-cell'
+import { getGoodsDetail } from '@/api/goods'
 
 const DRAFT_KEY = 'seckillDraft'
 
-const EMPTY_GOODS = () => ({
+/** 空的商品基础信息(名称/封面等,多规格共用) */
+const EMPTY_BASE = () => ({
+  masterGoodsId: 0,
+  name: '',
+  cover: '',
+  status: 1
+})
+
+/** 空的规格填写表单(单规格时只有一项) */
+const EMPTY_SPEC_FORM = () => ({
   goodsId: 0,
+  specName: '',
   price: '',
   stock: '',
   limit: '',
-  status: 1,
-  name: '',
-  cover: '',
   originPrice: 0,
-  realStock: 0
+  realStock: 0,
+  cover: ''
 })
 
 export default {
@@ -83,15 +122,10 @@ export default {
     return {
       constant: this.$constant,
       goodsIndex: -1,
-      goodsForm: EMPTY_GOODS()
-    }
-  },
-  computed: {
-    stockPlaceholder() {
-      if (this.goodsForm.realStock > 0) {
-        return `不超过实际库存${this.goodsForm.realStock}`
-      }
-      return '请输入秒杀库存'
+      goodsBase: EMPTY_BASE(),
+      // 单规格 1 项;多规格按规格数展开
+      specForms: [EMPTY_SPEC_FORM()],
+      isMultiSpec: false
     }
   },
   onLoad() {
@@ -103,6 +137,7 @@ export default {
   methods: {
     /**
      * 解析路由 index,从 seckillDraft 加载待编辑商品
+     * 编辑态按单条展示(列表中每规格已是独立一条)
      */
     init() {
       this.goodsIndex = parseInt(this.option && this.option.index, 10)
@@ -111,9 +146,28 @@ export default {
       }
       const draft = uni.getStorageSync(DRAFT_KEY) || {}
       if (this.goodsIndex >= 0 && draft.goods && draft.goods[this.goodsIndex]) {
-        this.goodsForm = { ...EMPTY_GOODS(), ...draft.goods[this.goodsIndex] }
+        const item = draft.goods[this.goodsIndex]
+        this.goodsBase = {
+          masterGoodsId: item.goodsId || 0,
+          name: item.name || '',
+          cover: item.cover || '',
+          status: item.status == 1 ? 1 : 0
+        }
+        this.isMultiSpec = false
+        this.specForms = [{
+          goodsId: item.goodsId || 0,
+          specName: item.specName || '',
+          price: item.price !== undefined && item.price !== null ? item.price : '',
+          stock: item.stock !== undefined && item.stock !== null ? item.stock : '',
+          limit: item.limit !== undefined && item.limit !== null ? item.limit : '',
+          originPrice: item.originPrice || 0,
+          realStock: item.realStock || 0,
+          cover: item.cover || ''
+        }]
       } else {
-        this.goodsForm = EMPTY_GOODS()
+        this.goodsBase = EMPTY_BASE()
+        this.specForms = [EMPTY_SPEC_FORM()]
+        this.isMultiSpec = false
       }
     },
     imgFullUrl(path) {
@@ -122,70 +176,165 @@ export default {
       const base = (this.constant.imgUrl || '').replace(/\/$/, '')
       return `${base}/${String(path).replace(/^\//, '')}`
     },
-    /** 跳转花束商品单选页 */
+    /**
+     * 规格表单字段输入(小程序端 v-for 内不能直接 v-model 别名)
+     * 下标/字段名通过 data-index、data-field 传入,避免事件参数编译异常
+     */
+    onSpecInput(e) {
+      const dataset = (e && e.currentTarget && e.currentTarget.dataset) || {}
+      const index = parseInt(dataset.index, 10)
+      const field = dataset.field
+      const value = e && e.detail ? e.detail.value : ''
+      if (isNaN(index) || !field || !this.specForms[index]) return
+      this.$set(this.specForms[index], field, value)
+    },
+    /** 跳转花束商品单选页(列表为主商品 masterId=0) */
     selectGoods() {
+      const selectedId = this.goodsBase.masterGoodsId || (this.specForms[0] && this.specForms[0].goodsId) || ''
       uni.navigateTo({
-        url: `/admin/goods/ad-goods-select?mode=single&selectedIds=${this.goodsForm.goodsId || ''}`
+        url: `/admin/goods/ad-goods-select?mode=single&selectedIds=${selectedId}`
       })
     },
-    /** 接收商品选择结果,填充 goodsId/name/cover/originPrice/realStock */
+    /**
+     * 接收商品选择结果:拉详情判断是否多规格,动态生成填写表单
+     * 多规格时每条子规格用自己的 goodsId/库存/原价
+     */
     onGoodsSelected(payload) {
       const g = (payload.goodsList || [])[0]
       if (!g) return
-      this.goodsForm.goodsId = g.id
-      this.goodsForm.name = g.name || ''
-      this.goodsForm.cover = g.cover || ''
-      this.goodsForm.originPrice = g.price || 0
-      this.goodsForm.realStock = parseInt(g.stock, 10) || 0
+      this.goodsBase.masterGoodsId = g.id
+      this.goodsBase.name = g.name || ''
+      this.goodsBase.cover = g.cover || ''
+      // 先用列表快照填一套单规格表单,详情返回后再按规格覆盖
+      this.isMultiSpec = false
+      this.specForms = [{
+        goodsId: g.id,
+        specName: '',
+        price: '',
+        stock: '',
+        limit: '',
+        originPrice: g.price || 0,
+        realStock: parseInt(g.stock, 10) || 0,
+        cover: g.cover || ''
+      }]
+      this.loadGoodsSpecs(g.id)
+    },
+    /**
+     * 拉取商品详情,多规格则按 specList 展开多套表单
+     * @param {number} goodsId 主商品 id
+     */
+    loadGoodsSpecs(goodsId) {
+      if (!goodsId) return
+      getGoodsDetail({ id: goodsId }).then((res) => {
+        if (res.code !== 1 || !res.data) return
+        // 若用户已切换到其他商品,忽略过期回调
+        if (parseInt(goodsId, 10) !== parseInt(this.goodsBase.masterGoodsId, 10)) return
+        const data = res.data
+        const cover = data.shortCover || data.cover || this.goodsBase.cover || ''
+        this.goodsBase.name = data.name || this.goodsBase.name
+        this.goodsBase.cover = cover
+        const specList = Array.isArray(data.specList) ? data.specList : []
+        // 启用多规格且存在子规格:一套规格一张填写表单
+        if (Number(data.specEnabled) === 1 && specList.length > 0) {
+          this.isMultiSpec = true
+          this.specForms = specList.map((spec) => ({
+            goodsId: spec.id || 0,
+            specName: spec.specName || '',
+            price: '',
+            stock: '',
+            limit: '',
+            originPrice: spec.price || 0,
+            realStock: parseInt(spec.stock, 10) || 0,
+            cover: spec.shortCover || spec.cover || cover
+          }))
+          return
+        }
+        this.isMultiSpec = false
+        this.specForms = [{
+          goodsId: data.id || goodsId,
+          specName: '',
+          price: '',
+          stock: '',
+          limit: '',
+          originPrice: data.price || 0,
+          realStock: parseInt(data.stock, 10) || 0,
+          cover
+        }]
+      })
     },
     cancelFn() {
       uni.navigateBack()
     },
-    /** 校验后写回 seckillDraft 并返回列表页 */
+    /**
+     * 校验全部规格表单后写回 seckillDraft
+     * 多规格保存为多条(每规格 goodsId 为子商品 id,后端按该 id 校验实际库存)
+     */
     saveFn() {
-      const goodsId = parseInt(this.goodsForm.goodsId, 10) || 0
-      const price = parseFloat(this.goodsForm.price)
-      const stock = parseInt(this.goodsForm.stock, 10)
-      const limit = parseInt(this.goodsForm.limit, 10)
-      if (!goodsId) {
+      if (!this.goodsBase.masterGoodsId && !(this.specForms[0] && this.specForms[0].goodsId)) {
         this.$msg('请选择活动商品')
         return
       }
-      if (!price || price <= 0) {
-        this.$msg('请输入秒杀价格')
-        return
-      }
-      if (!stock || stock <= 0) {
-        this.$msg('请输入秒杀库存')
-        return
-      }
-      if (this.goodsForm.realStock > 0 && stock > this.goodsForm.realStock) {
-        this.$msg(`秒杀库存不能超过实际库存(${this.goodsForm.realStock})`)
-        return
-      }
-      if (!limit || limit <= 0) {
-        this.$msg('请输入单人限购')
-        return
+      const items = []
+      for (let i = 0; i < this.specForms.length; i++) {
+        const form = this.specForms[i]
+        const label = this.isMultiSpec
+          ? `规格「${form.specName || (i + 1)}」`
+          : ''
+        const goodsId = parseInt(form.goodsId, 10) || 0
+        const price = parseFloat(form.price)
+        const stock = parseInt(form.stock, 10)
+        const limit = parseInt(form.limit, 10)
+        if (!goodsId) {
+          this.$msg(label ? `${label}商品无效,请重新选择` : '请选择活动商品')
+          return
+        }
+        if (!price || price <= 0) {
+          this.$msg(label ? `请输入${label}秒杀价格` : '请输入秒杀价格')
+          return
+        }
+        if (!stock || stock <= 0) {
+          this.$msg(label ? `请输入${label}秒杀库存` : '请输入秒杀库存')
+          return
+        }
+        // 秒杀库存不得超过该规格实际库存
+        if (form.realStock > 0 && stock > form.realStock) {
+          this.$msg(
+            label
+              ? `${label}秒杀库存不能超过实际库存(${form.realStock})`
+              : `秒杀库存不能超过实际库存(${form.realStock})`
+          )
+          return
+        }
+        if (!limit || limit <= 0) {
+          this.$msg(label ? `请输入${label}单人限购` : '请输入单人限购')
+          return
+        }
+        // 列表展示名带规格后缀,便于区分同商品多规格
+        const displayName = form.specName
+          ? `${this.goodsBase.name}(${form.specName})`
+          : this.goodsBase.name
+        items.push({
+          goodsId,
+          price,
+          stock,
+          limit,
+          status: this.goodsBase.status == 1 ? 1 : 0,
+          name: displayName,
+          cover: form.cover || this.goodsBase.cover,
+          originPrice: parseFloat(form.originPrice) || 0,
+          realStock: form.realStock || 0,
+          specName: form.specName || ''
+        })
       }
       const draft = uni.getStorageSync(DRAFT_KEY) || { goods: [] }
       if (!Array.isArray(draft.goods)) {
         draft.goods = []
       }
-      const item = {
-        goodsId,
-        price,
-        stock,
-        limit,
-        status: this.goodsForm.status == 1 ? 1 : 0,
-        name: this.goodsForm.name,
-        cover: this.goodsForm.cover,
-        originPrice: parseFloat(this.goodsForm.originPrice) || 0,
-        realStock: this.goodsForm.realStock
-      }
+      // 编辑态:用本次结果替换原位置(多规格重选时可能 1 变 N)
       if (this.goodsIndex >= 0) {
-        this.$set(draft.goods, this.goodsIndex, item)
+        draft.goods.splice(this.goodsIndex, 1, ...items)
       } else {
-        draft.goods.push(item)
+        draft.goods.push(...items)
       }
       draft._goodsUpdated = true
       uni.setStorageSync(DRAFT_KEY, draft)
@@ -230,10 +379,30 @@ export default {
   background: #f5f5f5;
 }
 
-.tip-text {
-  padding: 20upx 30upx;
-  font-size: 24upx;
+.spec-head {
+  @include disFlex(center, flex-start);
+  padding: 24upx 30upx 8upx;
+}
+
+.spec-head-label {
+  flex-shrink: 0;
+  font-size: 26upx;
   color: $fontColor3;
+  margin-right: 12upx;
+}
+
+.spec-head-name {
+  font-size: 28upx;
+  color: #333;
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.real-stock-text {
+  text-align: right;
+  color: $fontColor2;
 }
 
 .bottom-bar {

+ 8 - 1
mallApp/src/components/home/activitySection.vue

@@ -132,7 +132,14 @@ export default {
       if (!g) return
       const id = g.id || g.goodsId
       if (!id) return
-      this.pageTo({ url: `/pages/goods/detail?id=${id}&account=${this.account}&hdId=${this.hdId}` })
+      let url = `/pages/goods/detail?id=${id}&account=${this.account}&hdId=${this.hdId}`
+      // 秒杀商品:把活动价/限购/结束时间带到详情页,详情页据此展示秒杀价并在加入购物车时打上活动标记;
+      // 实际下单价格仍由后端独立校验 Redis 秒杀配置核价,这里带的数据仅用于前端展示与标记
+      if (this.type === 'seckill') {
+        const endTime = parseInt(this.data && this.data.endTime, 10) || 0
+        url += `&activityType=seckill&activityPrice=${g.price}&activityLimit=${g.limit || 0}&activityStock=${g.stock || 0}&activityEndTime=${endTime}`
+      }
+      this.pageTo({ url })
     },
     goMore() {
       const title = this.data.title || (this.isGroupBuy ? '团购优惠' : '限时秒杀')

+ 14 - 2
mallApp/src/mixins/cgProduct.js

@@ -310,6 +310,8 @@ export default {
 			const itemProperty = this.getItemProperty(item);
 			if (eleProperty !== itemProperty) return false;
 			if (eleProperty === 0) {
+				// 秒杀与普通购买同商品同规格也必须分行,避免不同计价规则的购物车行被合并
+				if ((element.activityType || '') !== (item.activityType || '')) return false;
 				return String(element.id) === String(item.id)
 					&& String(element.specGoodsId || 0) === String(item.specGoodsId || 0);
 			}
@@ -375,7 +377,7 @@ export default {
 		},
 		getCartItemOriginalUnitPrice(item) {
 			if (this.isBouquetItem(item)) {
-				return 0;
+				return item.activityType === 'seckill' ? parseFloat(this.toSafeNumber(item.originPrice)) : 0;
 			}
 			if (this.isReachDiscountReached(item)) {
 				return parseFloat(this.toSafeNumber(item.price || item.bigPrice));
@@ -391,7 +393,10 @@ export default {
 		},
 		cartPriceShowStrike(item) {
 			if (this.isBouquetItem(item)) {
-				return false;
+				if (item.activityType !== 'seckill') return false;
+				const original = this.toSafeNumber(item.originPrice);
+				const current = this.toSafeNumber(item.price || item.bigPrice);
+				return original > current;
 			}
 			if (this.isReachDiscountReached(item)) {
 				const original = this.toSafeNumber(item.price || item.bigPrice);
@@ -460,6 +465,13 @@ export default {
 				bigUnit: '份',
 				smallUnit: '份'
 			};
+			// 秒杀商品:打上活动标记与原价,用于购物车/结算页展示划线价;
+			// 实际下单价格由后端独立核对活动配置核价,此处价格仅用于前端展示
+			if (goods.activityType === 'seckill') {
+				row.activityType = 'seckill';
+				row.activityEndTime = Number(goods.activityEndTime) || 0;
+				row.originPrice = Number(goods.originPrice) || 0;
+			}
 			let found = false;
 			for (let i = 0; i < list.length; i++) {
 				if (this._selectRowMatches(list[i], row)) {

+ 13 - 2
mallApp/src/pages/billing/affirmMix.vue

@@ -12,6 +12,7 @@
 										<text class="item-name">
 											<text class="item-property-tag" v-if="isBouquetItem(item)">花束</text>
 											<text class="item-property-tag item-property-tag--item" v-else>花材</text>
+											<text class="item-property-tag item-property-tag--seckill" v-if="item.activityType === 'seckill'">秒杀</text>
 											{{ item.itemName || item.name }}
 										</text>
 										<view class="item-price-block">
@@ -430,7 +431,9 @@ export default {
 			return list.map(item => {
 				if (this.isBouquetItem(item)) {
 					const p = parseFloat(item.price || item.bigPrice || 0) || 0
-					return { showStrike: false, main: `¥${p}`, strike: '' }
+					const showStrike = this.cartPriceShowStrike(item)
+					const strike = showStrike ? `¥${this.getCartItemOriginalUnitPrice(item)}` : ''
+					return { showStrike, main: `¥${p}`, strike }
 				}
 				return this.getLimitBuyPriceDisplayParts(item)
 			});
@@ -867,7 +870,7 @@ export default {
 			const product = (this.list || []).map(ele => {
 				if (this.isBouquetItem(ele)) {
 					const saleId = Number(ele.specGoodsId) > 0 ? Number(ele.specGoodsId) : Number(ele.id)
-					return {
+					const row = {
 						productId: saleId,
 						goodsId: Number(ele.goodsId || ele.id),
 						specGoodsId: Number(ele.specGoodsId) || 0,
@@ -875,6 +878,11 @@ export default {
 						unitType: 0,
 						property: 0
 					}
+					// 秒杀商品打上标记,后端据此独立核对活动库存/限购/价格后再计价,前端价格仅供展示
+					if (ele.activityType === 'seckill') {
+						row.activityType = 'seckill'
+					}
+					return row
 				}
 				let unitType = 0
 				let num = ele.bigCount
@@ -1220,6 +1228,9 @@ export default {
 							&--item {
 								background: #09C567;
 							}
+							&--seckill {
+								background: #ff7a00;
+							}
 						}
 						.item-price-block {
 							flex-shrink: 0;

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

@@ -111,8 +111,20 @@ export default {
       if (!selected) return false;
       const spec = selected.spec;
       const goods = {...this.info, ...(spec || {}), buyNum:this.buyNum};
+      // 秒杀商品:spec展开可能带回原价,需重新覆盖为秒杀展示价,并保留活动标记供下单页透传给后端核价
+      if (this.info.activityType === 'seckill') {
+        goods.activityType = 'seckill';
+        goods.price = this.info.price;
+        goods.originPrice = this.info.originPrice;
+        goods.activityEndTime = this.info.activityEndTime;
+        goods.activityLimit = this.info.activityLimit;
+      }
       uni.setStorageSync("buyGoodsDetil", goods);
-      this.$util.pageTo({url: "/pages/order/buy?hdId="+this.hdId+'&goodsId='+this.info.id+'&specGoodsId='+(spec ? spec.id : '')+'&goodsNum='+this.buyNum+'&selIndex='+this.selIndex+'&account='+this.account+'&freightType='+this.info.freightType,type: 2})
+      let url = "/pages/order/buy?hdId="+this.hdId+'&goodsId='+this.info.id+'&specGoodsId='+(spec ? spec.id : '')+'&goodsNum='+this.buyNum+'&selIndex='+this.selIndex+'&account='+this.account+'&freightType='+this.info.freightType
+      if (this.info.activityType === 'seckill') {
+        url += '&activityType=seckill'
+      }
+      this.$util.pageTo({url,type: 2})
     },
     addCartFn() {
       const selected = this.getSelectedSpec();

+ 112 - 0
mallApp/src/pages/goods/detail.vue

@@ -7,11 +7,19 @@
         :list="imgList"
       />
       <view class="shop-intro">
+        <view v-if="isSeckillActive" class="seckill-bar">
+          <view class="seckill-bar-left">
+            <text class="seckill-badge">秒杀价</text>
+            <text v-if="seckillCountdownText" class="seckill-countdown">{{ seckillCountdownText }}后结束</text>
+          </view>
+          <text v-if="activityLimit > 0" class="seckill-limit">每人限购{{ activityLimit }}件</text>
+        </view>
         <view class="app-price app-bold" v-if="data.priceType == 1">
           <text>¥</text>
           <text class="app-size-34">
             {{ data.price ? parseFloat(data.price).toFixed(2) : 0}}
           </text>
+          <text v-if="isSeckillActive && data.originPrice > 0" class="app-price-origin">¥{{ parseFloat(data.originPrice).toFixed(2) }}</text>
         </view>
         <view class="tui-pro-titbox">
           <view class="tui-pro-title app-size-32">{{ data.name }}</view>
@@ -129,6 +137,14 @@ export default {
       common_title: "",
       common_previewContent: [],
       shopInfo: {},
+      /** 秒杀活动信息:来自首页秒杀专区/更多列表跳转带过来的 query,仅用于展示与购物车标记;
+       * 实际下单价格由后端独立核对 Redis 秒杀配置,避免前端数据被篡改 */
+      activityType: '',
+      activityPrice: 0,
+      activityLimit: 0,
+      activityEndTime: 0,
+      seckillCountdownText: '',
+      _seckillTimer: null,
     };
   },
   onLoad() {
@@ -141,6 +157,9 @@ export default {
       this.loadGoodsDetail();
     });
   },
+  beforeDestroy() {
+    this.clearSeckillTimer();
+  },
   onPageScroll(e) {
     // console.log('Scroll position:', parseInt(e.scrollTop))
     // 通过 ref 调用子组件的方法
@@ -157,13 +176,71 @@ export default {
   },
   computed: {
     ...mapGetters({ shopUser: "getShopUser", loginInfo: "getLoginInfo" }),
+    /** 秒杀活动是否仍在有效期内且价格有效:三者都满足才按秒杀价展示/加购 */
+    isSeckillActive() {
+      if (this.activityType !== 'seckill') return false;
+      if (!(Number(this.activityPrice) > 0)) return false;
+      const endTime = Number(this.activityEndTime) || 0;
+      return endTime > 0 && endTime > Math.floor(Date.now() / 1000);
+    },
   },
   methods: {
+    /** 解析首页秒杀专区/更多列表带过来的活动参数 */
+    initActivityOption(option) {
+      this.activityType = (option && option.activityType) || '';
+      this.activityPrice = parseFloat(option && option.activityPrice) || 0;
+      this.activityLimit = parseInt(option && option.activityLimit, 10) || 0;
+      this.activityEndTime = parseInt(option && option.activityEndTime, 10) || 0;
+      if (this.activityType === 'seckill') {
+        this.restartSeckillCountdown();
+      }
+    },
+    clearSeckillTimer() {
+      if (this._seckillTimer) {
+        clearInterval(this._seckillTimer);
+        this._seckillTimer = null;
+      }
+    },
+    restartSeckillCountdown() {
+      this.clearSeckillTimer();
+      this.tickSeckillCountdown();
+      this._seckillTimer = setInterval(this.tickSeckillCountdown, 1000);
+    },
+    tickSeckillCountdown() {
+      const end = Number(this.activityEndTime) || 0;
+      const now = Math.floor(Date.now() / 1000);
+      if (end <= 0 || now >= end) {
+        this.seckillCountdownText = '';
+        this.clearSeckillTimer();
+        return;
+      }
+      let diff = end - now;
+      const h = Math.floor(diff / 3600);
+      diff -= h * 3600;
+      const m = Math.floor(diff / 60);
+      const s = diff % 60;
+      const pad = (n) => String(n).padStart(2, '0');
+      this.seckillCountdownText = `${pad(h)}:${pad(m)}:${pad(s)}`;
+    },
+    /** 用秒杀价覆盖详情展示价格,原价保留用于划线展示;仅覆盖展示层,实际下单价格由后端独立核价 */
+    applySeckillPriceIfNeeded() {
+      if (!this.isSeckillActive) return;
+      if (this.data.priceType != 1) return;
+      this.data = {
+        ...this.data,
+        originPrice: this.data.price,
+        price: this.activityPrice,
+        activityType: 'seckill',
+        activityEndTime: this.activityEndTime,
+        activityLimit: this.activityLimit,
+      };
+    },
     init() {
       if (!this.option || !this.option.id) {
         return;
       }
       this.initializeOption(this.option);
+      this.initActivityOption(this.option);
       this.hdId = this.option.hdId || 0;
       // 外链/跨小程序进入时可能只带 shopId(与花材详情分享一致),需映射为 account
       this.account = this.option.account || this.option.shopId || 0;
@@ -304,6 +381,7 @@ export default {
         res.data.stock = res.data.stock ? parseInt(res.data.stock) : 0;
         this.data = res.data;
         this.customId = res.data.customId;
+        this.applySeckillPriceIfNeeded();
 
         //商品描述
         this.title =
@@ -485,6 +563,40 @@ export default {
   .shop-intro {
     background: #fff;
     padding: 30upx 0 30upx 30upx;
+    .seckill-bar {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 10upx 20upx 10upx 0;
+      margin-bottom: 10upx;
+      .seckill-bar-left {
+        display: flex;
+        align-items: center;
+      }
+      .seckill-badge {
+        padding: 4upx 14upx;
+        font-size: 24upx;
+        font-weight: 700;
+        color: #fff;
+        background: linear-gradient(135deg, #FF8FA3, #FF4D6D);
+        border-radius: 20upx;
+      }
+      .seckill-countdown {
+        margin-left: 12upx;
+        font-size: 24upx;
+        color: #FF4D6D;
+      }
+      .seckill-limit {
+        font-size: 22upx;
+        color: #999;
+      }
+    }
+    .app-price-origin {
+      margin-left: 12upx;
+      font-size: 26upx;
+      color: #bbb;
+      text-decoration: line-through;
+    }
     .tui-pro-titbox {
       margin: 20upx 0 20upx;
       @include disFlex(center, space-between);

+ 17 - 8
mallApp/src/pages/goods/section-list.vue

@@ -56,7 +56,9 @@ export default {
   data() {
     return {
       moduleKey: '',
-      pageTitle: '商品列表'
+      pageTitle: '商品列表',
+      /** 秒杀活动结束时间,取自接口返回,用于点击商品时带给详情页 */
+      activityEndTime: 0
     }
   },
   computed: {
@@ -101,10 +103,15 @@ export default {
       return getSectionGoods(this.buildQuery())
         .then((res) => {
           this.completes(res)
-          // 后端返回的 title 优先用于导航栏(路由未带 title 时兜底)
-          if (res.code === 1 && res.data && res.data.title && !((this.option && this.option.title))) {
-            this.pageTitle = res.data.title
-            uni.setNavigationBarTitle({ title: this.pageTitle })
+          if (res.code === 1 && res.data) {
+            // 后端返回的 title 优先用于导航栏(路由未带 title 时兜底)
+            if (res.data.title && !((this.option && this.option.title))) {
+              this.pageTitle = res.data.title
+              uni.setNavigationBarTitle({ title: this.pageTitle })
+            }
+            if (this.isSeckill) {
+              this.activityEndTime = parseInt(res.data.endTime, 10) || 0
+            }
           }
         })
         .catch(() => {
@@ -129,9 +136,11 @@ export default {
       if (!item) return
       const id = item.id || item.goodsId
       if (!id) return
-      this.pageTo({
-        url: `/pages/goods/detail?id=${id}&account=${this.account}&hdId=${this.hdId}`
-      })
+      let url = `/pages/goods/detail?id=${id}&account=${this.account}&hdId=${this.hdId}`
+      if (this.isSeckill) {
+        url += `&activityType=seckill&activityPrice=${item.price}&activityLimit=${item.limit || 0}&activityStock=${item.stock || 0}&activityEndTime=${this.activityEndTime}`
+      }
+      this.pageTo({ url })
     }
   },
   async onPullDownRefresh() {

+ 7 - 1
mallApp/src/pages/home/cart.vue

@@ -48,7 +48,8 @@
           <view class="cart-item-body">
             <view class="cart-item-top">
               <view class="cart-item-title-wrap">
-                <text class="item-tag item-tag--hot" v-if="Number(item.discountPrice) > 0">热销</text>
+                <text class="item-tag item-tag--seckill" v-if="item.activityType === 'seckill'">秒杀</text>
+                <text class="item-tag item-tag--hot" v-else-if="Number(item.discountPrice) > 0">热销</text>
                 <text class="cart-item-name">{{ item.itemName || item.name }}</text>
               </view>
               <view class="cart-item-del" :data-index="index" @tap.stop="onRemoveItem">
@@ -888,6 +889,11 @@ export default {
   border: 1upx solid $fontPinkColor;
 }
 
+.item-tag--seckill {
+  color: #fff;
+  background: #FF4D6D;
+}
+
 .cart-item-name {
   flex: 1;
   min-width: 0;

+ 5 - 0
mallApp/src/pages/order/buy.vue

@@ -311,6 +311,11 @@ export default {
         isFreeDelivery: this.isFreeDelivery
       };
 
+      // 秒杀商品:告知后端按秒杀价独立核价(真实价格/库存/限购由后端回查Redis核实,不信任前端传的价格)
+      if (this.option.activityType === 'seckill' || this.goodsInfo.activityType === 'seckill') {
+        params.activityType = 'seckill'
+      }
+
       // 添加红包ID
       if(this.hasAvailableHb && this.selectedHbId && this.selectedHb){
         params.hbId = this.selectedHbId

+ 22 - 2
mallApp/src/pages/order/components/app-order-list2.vue

@@ -6,11 +6,14 @@
 					<image :src="info.cover" mode="aspectFill" style="width:130upx;height:130upx;"></image>
 				</div>
 				<div class="list-msg">
-					<div class="title">{{ info.name }}</div>
+					<div class="title"><text class="tag-seckill" v-if="info.activityType === 'seckill'">秒杀</text>{{ info.name }}</div>
 				</div>
 			</div>
 			<div class="list-right">
-				<div class="price">¥{{info.price ? parseFloat(info.price).toFixed(2) : 0}}</div>
+				<div class="price">
+					<text class="price-origin" v-if="info.activityType === 'seckill' && info.originPrice > info.price">¥{{parseFloat(info.originPrice).toFixed(2)}}</text>
+					¥{{info.price ? parseFloat(info.price).toFixed(2) : 0}}
+				</div>
 				<div class="num">X{{info.buyNum}}</div>
 			</div>
 		</block>
@@ -71,7 +74,24 @@ export default {
 		color: #252525;
 		.price {
 			margin-bottom: 70upx;
+			.price-origin {
+				color: #999;
+				text-decoration: line-through;
+				font-size: 24upx;
+				margin-right: 8upx;
+			}
 		}
 	}
 }
+.tag-seckill {
+	display: inline-block;
+	color: #fff;
+	background: #FF4D6D;
+	font-size: 22upx;
+	line-height: 1;
+	padding: 2upx 8upx;
+	border-radius: 6upx;
+	margin-right: 8upx;
+	vertical-align: middle;
+}
 </style>