加入购物车
+
拼团购买
立即购买
+
@@ -320,6 +374,18 @@ import {
} from "@/api/member.js";
import { addCartGoods } from "@/api/cart.js";
import playIcon from "@/assets/iconfont/play.svg";
+/** 卡密商品(E_COUPON)判定与库存工具,见 @/constants/goodsType */
+import {
+ isECoupon,
+ getECouponStock,
+ getECouponMaxBuyNum,
+ createEmptyPromotionMap,
+ resolveECouponCartType,
+ eCouponAfterSaleHint,
+ E_COUPON_CART_BUY_NOW,
+ E_COUPON_CART_PINTUAN,
+ E_COUPON_CART_POINTS,
+} from "@/constants/goodsType";
export default {
name: "ShowGoods",
@@ -362,15 +428,11 @@ export default {
specList: [],
}, // sku详情
goodsSpecList: this.detail.specs, // 商品spec
- promotionMap: {
- // 活动状态
- SECKILL: null,
- FULL_DISCOUNT: null,
- COUPON: [],
- }, // 促销活动
+ promotionMap: createEmptyPromotionMap(),
formatList: [], // 选择商品品类的数组
loading: false, // 立即购买loading
loading1: false, // 加入购物车loading
+ loadingPintuan: false,
isCollected: false, // 是否收藏
};
},
@@ -397,15 +459,32 @@ export default {
})
: [];
},
+ /** 卡密商品:禁加购,仅立即购买;库存读 quantity(M-02 起支持促销) */
+ isECouponGoods() {
+ return isECoupon(this.skuDetail.goodsType);
+ },
+ displayStock() {
+ if (this.isECouponGoods) {
+ return getECouponStock(this.skuDetail);
+ }
+ return Number(this.skuDetail.quantity) || 0;
+ },
quantityMax() {
+ if (this.isECouponGoods) {
+ return getECouponMaxBuyNum(this.skuDetail);
+ }
const qty = Number(this.skuDetail.quantity) || 0;
return qty > 0 ? qty : 1;
},
isOutOfStock() {
+ if (this.isECouponGoods) {
+ return getECouponStock(this.skuDetail) <= 0;
+ }
return (Number(this.skuDetail.quantity) || 0) <= 0;
},
},
methods: {
+ eCouponAfterSaleHint,
// 初始化video
initVideo(){
if(!this.goodsVideo ){
@@ -425,17 +504,20 @@ export default {
syncCountWithQuantity() {
- const qty = Number(this.skuDetail.quantity) || 0;
+ const qty = this.isECouponGoods
+ ? getECouponStock(this.skuDetail)
+ : Number(this.skuDetail.quantity) || 0;
if (qty <= 0) {
this.count = 1;
return;
}
- if (this.wholesaleList && this.wholesaleList.length > 0) {
+ if (!this.isECouponGoods && this.wholesaleList && this.wholesaleList.length > 0) {
this.count = Math.min(Math.max(this.wholesaleList[0].num, 1), qty);
return;
}
- if (this.count > qty) {
- this.count = qty;
+ const max = this.isECouponGoods ? getECouponMaxBuyNum(this.skuDetail) : qty;
+ if (this.count > max) {
+ this.count = max;
} else if (this.count < 1) {
this.count = 1;
}
@@ -471,6 +553,11 @@ export default {
},
addShoppingCartBtn() {
+ // FR-B-01:E_COUPON 禁止加购物车(后端亦返回 CARD_KEY_E_COUPON_CART_FORBIDDEN)
+ if (this.isECouponGoods) {
+ Message.warning("电子卡券请使用立即购买");
+ return;
+ }
// 添加购物车
const params = {
num: this.count,
@@ -500,20 +587,32 @@ export default {
});
},
buyNow() {
- // 立即购买
+ this.startCheckout(E_COUPON_CART_BUY_NOW, "loading1");
+ },
+ pintuanBuy() {
+ this.startCheckout(E_COUPON_CART_PINTUAN, "loadingPintuan");
+ },
+ pointPay() {
+ this.startCheckout(E_COUPON_CART_POINTS, "loading");
+ },
+ startCheckout(action, loadingKey) {
const params = {
num: this.count,
skuId: this.skuDetail.id,
cartType: "BUY_NOW",
};
- // 虚拟商品购买
if (this.skuDetail.goodsType === "VIRTUAL_GOODS") {
params.cartType = "VIRTUAL";
+ } else if (this.isECouponGoods) {
+ params.cartType = resolveECouponCartType({
+ routeWay: this.$route.query.way,
+ action,
+ });
}
- this.loading1 = true;
+ this[loadingKey] = true;
addCartGoods(params)
.then((res) => {
- this.loading1 = false;
+ this[loadingKey] = false;
if (res.success) {
this.$router.push({
path: "/pay",
@@ -524,7 +623,7 @@ export default {
}
})
.catch(() => {
- this.loading1 = false;
+ this[loadingKey] = false;
});
},
async collect() {
@@ -602,16 +701,20 @@ export default {
});
},
promotion() {
- this.promotionMap = {SECKILL: null, FULL_DISCOUNT: null, COUPON: []};
- // 格式化促销活动,返回当前促销的对象
- if (!this.detail.promotionMap) return false;
- let keysArr = Object.keys(this.detail.promotionMap);
+ this.promotionMap = createEmptyPromotionMap();
+ if (!this.detail || !this.detail.promotionMap) return false;
+ const keysArr = Object.keys(this.detail.promotionMap);
if (keysArr.length === 0) return false;
for (let i = 0; i < keysArr.length; i++) {
- let key = keysArr[i].split("-")[0];
+ const key = keysArr[i].split("-")[0];
if (key === "COUPON") {
- this.promotionMap[key].push(this.detail.promotionMap[keysArr[i]]);
+ if (!Array.isArray(this.promotionMap.COUPON)) {
+ this.promotionMap.COUPON = [];
+ }
+ this.promotionMap.COUPON.push(this.detail.promotionMap[keysArr[i]]);
+ } else if (Object.prototype.hasOwnProperty.call(this.promotionMap, key)) {
+ this.promotionMap[key] = this.detail.promotionMap[keysArr[i]];
} else {
this.promotionMap[key] = this.detail.promotionMap[keysArr[i]];
}
@@ -630,8 +733,8 @@ export default {
},
},
mounted() {
- // 用户登录才会判断是否收藏
- if (this.Cookies.getItem("userInfo")) {
+ // 用户登录才会判断是否收藏(须同时有 token,避免残留 userInfo 触发 401 跳转登录)
+ if (this.Cookies.getItem("accessToken") && this.Cookies.getItem("userInfo")) {
isCollection("GOODS", this.skuDetail.id).then((res) => {
if (res.success && res.result) {
this.isCollected = true;
@@ -958,6 +1061,22 @@ export default {
border-top: 1px dotted $border_color;
}
+.e-coupon-after-sale-notice {
+ width: fit-content;
+ max-width: 420px;
+ flex: 0 0 auto;
+ padding: 4px 10px;
+
+ :deep(.el-alert__content) {
+ padding: 0;
+ }
+
+ :deep(.el-alert__title) {
+ font-size: 12px;
+ line-height: 1.5;
+ }
+}
+
.add-buy-car-row {
margin-top: 25px;
diff --git a/buyer/src/constants/goodsType.js b/buyer/src/constants/goodsType.js
new file mode 100644
index 00000000..70f27872
--- /dev/null
+++ b/buyer/src/constants/goodsType.js
@@ -0,0 +1,208 @@
+/**
+ * 卡密商品(E_COUPON / 电子卡券)— 买家端公共常量与工具
+ *
+ * 与 VIRTUAL_GOODS(核销型虚拟)区分:E_COUPON 常规促销走 BUY_NOW,
+ * 特殊活动走 PINTUAN / POINTS / KANJIA;禁止 CART / VIRTUAL。
+ * 需求:lilishop/docs/requirements/card-key-goods-v4.md · M-02
+ *
+ * @author Mike
+ * @date 2026-07-31
+ */
+export const E_COUPON_GOODS_TYPE = "E_COUPON";
+export const E_COUPON_ORDER_TYPE = "E_COUPON";
+
+/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
+export const E_COUPON_MAX_BUY_NUM = 999;
+
+/** 常规立即购买(秒杀/满减/券/限时/第N件等) */
+export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
+export const E_COUPON_CART_PINTUAN = "PINTUAN";
+export const E_COUPON_CART_POINTS = "POINTS";
+export const E_COUPON_CART_KANJIA = "KANJIA";
+
+export function isECoupon(goodsType) {
+ return goodsType === E_COUPON_GOODS_TYPE;
+}
+
+export function isECouponOrder(orderType) {
+ return orderType === E_COUPON_ORDER_TYPE;
+}
+
+/** 详情页提示:卡密交付后不支持买家售后(FR-B-04) */
+export const E_COUPON_AFTER_SALE_HINT =
+ "卡密交付后不支持买家售后,请确认商品信息后再购买。";
+
+export function eCouponAfterSaleHint() {
+ return E_COUPON_AFTER_SALE_HINT;
+}
+
+/** 可售库存:E_COUPON 读 SKU quantity(由卡池 syncSkuStock 同步) */
+export function getECouponStock(sku) {
+ if (!sku) return 0;
+ return Number(sku.quantity) || 0;
+}
+
+export function getECouponMaxBuyNum(sku) {
+ const stock = getECouponStock(sku);
+ if (stock <= 0) return 1;
+ return Math.min(stock, E_COUPON_MAX_BUY_NUM);
+}
+
+/**
+ * 解析 E_COUPON 结算 cartType / way
+ * @param {object} opts
+ * @param {string} [opts.routeWay] 路由 query.way
+ * @param {'BUY_NOW'|'PINTUAN'|'POINTS'|'KANJIA'} [opts.action] 用户点击的购买入口
+ */
+export function resolveECouponCartType({ routeWay, action } = {}) {
+ if (action === E_COUPON_CART_PINTUAN) return E_COUPON_CART_PINTUAN;
+ if (action === E_COUPON_CART_POINTS) return E_COUPON_CART_POINTS;
+ if (action === E_COUPON_CART_KANJIA) return E_COUPON_CART_KANJIA;
+ if (action === E_COUPON_CART_BUY_NOW) return E_COUPON_CART_BUY_NOW;
+ const way = routeWay ? String(routeWay).toUpperCase() : "";
+ if (way === "KANJIA") return E_COUPON_CART_KANJIA;
+ if (way === "PINTUAN") return E_COUPON_CART_PINTUAN;
+ if (way === "POINT" || way === "POINTS") return E_COUPON_CART_POINTS;
+ return E_COUPON_CART_BUY_NOW;
+}
+
+/** 详情页 promotionMap 初始结构(切换 SKU 时完整重置) */
+export function createEmptyPromotionMap() {
+ return {
+ SECKILL: null,
+ FULL_DISCOUNT: null,
+ COUPON: [],
+ DISCOUNT: null,
+ NTH: null,
+ PINTUAN: null,
+ KANJIA: null,
+ };
+}
+
+/** 卡密履约状态(与后端 CardKeyFulfillStatusEnum 一致) */
+export const CARD_KEY_FULFILL_STATUS = {
+ PENDING: "PENDING",
+ DELIVERED: "DELIVERED",
+ FAILED: "FAILED",
+ NOT_APPLICABLE: "NOT_APPLICABLE",
+};
+
+export const CARD_KEY_FULFILL_STATUS_TEXT = {
+ PENDING: "待发放",
+ DELIVERED: "已发放",
+ FAILED: "发放失败",
+ NOT_APPLICABLE: "不适用",
+};
+
+export function cardKeyFulfillAlertType(status) {
+ const map = {
+ PENDING: "info",
+ DELIVERED: "success",
+ FAILED: "warning",
+ };
+ return map[status] || "info";
+}
+
+export function formatCardKeyFulfillStatus(status) {
+ return CARD_KEY_FULFILL_STATUS_TEXT[status] || status || "—";
+}
+
+export function resolveCardKeyFulfillMessage(line) {
+ if (line && line.message) return line.message;
+ return formatCardKeyFulfillStatus(line?.status);
+}
+
+export function isCardKeyFulfillApplicable(item) {
+ if (!item) return false;
+ const status = item.cardKeyFulfillStatus;
+ if (status) return status !== CARD_KEY_FULFILL_STATUS.NOT_APPLICABLE;
+ if (item.cardKeyDelivered != null || (item.cardKeys && item.cardKeys.length)) return true;
+ return item.goodsType === E_COUPON_GOODS_TYPE;
+}
+
+function normalizeCardKeyFulfillLine(source) {
+ const status =
+ source.cardKeyFulfillStatus ||
+ (source.cardKeyDelivered
+ ? CARD_KEY_FULFILL_STATUS.DELIVERED
+ : CARD_KEY_FULFILL_STATUS.PENDING);
+ const cardKeys =
+ status === CARD_KEY_FULFILL_STATUS.DELIVERED && source.cardKeyDelivered
+ ? source.cardKeys || []
+ : [];
+ return {
+ key: source.sn || source.orderSn || source.id || source.skuId,
+ goodsName: source.goodsName,
+ status,
+ message: source.cardKeyFulfillMessage,
+ cardKeys,
+ isGift: !!source.isGift,
+ };
+}
+
+/** 汇总当前订单 orderItems 中的卡密履约行(不含满赠子单;赠品卡密在赠品子单详情单独查看) */
+export function collectCardKeyFulfillLines(orderItems = []) {
+ const lines = [];
+ (orderItems || []).forEach((item) => {
+ if (!isCardKeyFulfillApplicable(item)) return;
+ lines.push(normalizeCardKeyFulfillLine(item));
+ });
+ return lines;
+}
+
+export function orderHasCardKeySection({ orderItems, isECouponOrder } = {}) {
+ if (isECouponOrder) return true;
+ if (collectCardKeyFulfillLines(orderItems).length > 0) return true;
+ return (orderItems || []).some(
+ (item) =>
+ item.goodsType === E_COUPON_GOODS_TYPE ||
+ (Array.isArray(item.cardKeys) && item.cardKeys.length > 0)
+ );
+}
+
+export function flattenCardKeyFulfillLines(lines, withGoodsName = false) {
+ const rows = [];
+ (lines || []).forEach((line) => {
+ if (line.status !== CARD_KEY_FULFILL_STATUS.DELIVERED) return;
+ (line.cardKeys || []).forEach((ck) => {
+ rows.push(withGoodsName ? { ...ck, goodsName: line.goodsName } : { ...ck });
+ });
+ });
+ return rows;
+}
+
+/** 当前订单是否含已交付卡密(仅 orderItems,不含满赠子单) */
+export function orderHasCardKeyContent(orderItems) {
+ return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems)).length > 0;
+}
+
+/** 扁平化当前订单 orderItems 卡密 */
+export function flattenOrderCardKeys(orderItems, withGoodsName = false) {
+ const lines = collectCardKeyFulfillLines(orderItems);
+ if (lines.some((line) => line.status)) {
+ return flattenCardKeyFulfillLines(lines, withGoodsName);
+ }
+ const rows = [];
+ (orderItems || []).forEach((item) => {
+ if (!item.cardKeyDelivered || !item.cardKeys || !item.cardKeys.length) return;
+ item.cardKeys.forEach((ck) => {
+ rows.push(withGoodsName ? { ...ck, goodsName: item.goodsName } : { ...ck });
+ });
+ });
+ return rows;
+}
+
+/** createTrade 后是否无需进入收银台(积分单 / 0 元 / 服务端已标记) */
+export function shouldSkipPaymentPage(tradeResult, way) {
+ if (!tradeResult) return false;
+ if (way === E_COUPON_CART_POINTS) return true;
+ if (tradeResult.needPay === false || tradeResult.paid === true) return true;
+ const price =
+ tradeResult.price != null
+ ? Number(tradeResult.price)
+ : tradeResult.flowPrice != null
+ ? Number(tradeResult.flowPrice)
+ : null;
+ if (price != null && !Number.isNaN(price) && price <= 0) return true;
+ return false;
+}
diff --git a/buyer/src/main.js b/buyer/src/main.js
index ebfcc214..7c284225 100644
--- a/buyer/src/main.js
+++ b/buyer/src/main.js
@@ -13,6 +13,7 @@ import storage from "@/plugins/storage";
import { fetchAndApplyTheme } from "@/utils/theme";
import { getThemeSetting } from "@/api/common.js";
import config from "@/config";
+import { openLink } from "@/utils/buyerLogin";
const { aMapSecurityJsCode, inputMaxLength } = config;
@@ -36,12 +37,7 @@ app.config.globalProperties.Cookies = storage;
app.config.globalProperties.$inputMaxLength = inputMaxLength;
app.config.globalProperties.linkTo = function (url) {
- if (!url) return;
- if (url.substr(0, 1) === "/") {
- window.open(location.origin + url, "_blank");
- } else {
- window.open(url, "_blank");
- }
+ openLink(url, router);
};
app.config.globalProperties.connectCs = function (
diff --git a/buyer/src/pages/Cart.vue b/buyer/src/pages/Cart.vue
index 98b5b567..938cef40 100644
--- a/buyer/src/pages/Cart.vue
+++ b/buyer/src/pages/Cart.vue
@@ -248,6 +248,7 @@ import Promotion from "@/components/goodsDetail/Promotion";
import Search from "@/components/Search";
import * as APICart from "@/api/cart";
import * as APIMember from "@/api/member";
+import { isECoupon } from "@/constants/goodsType";
export default {
name: "Cart",
beforeRouteEnter(to, from, next) {
@@ -407,11 +408,20 @@ export default {
},
// 跳转支付页面
pay() {
- if (this.checkedNum) {
- this.$router.push({ path: "/pay", query: { way: "CART" } });
- } else {
+ if (!this.checkedNum) {
Message.warning("请至少选择一件商品");
+ return;
}
+ const hasECoupon = this.cartList.some((shop) =>
+ (shop.skuList || []).some(
+ (goods) => goods.checked && isECoupon(goods.goodsSku && goods.goodsSku.goodsType)
+ )
+ );
+ if (hasECoupon) {
+ Message.warning("电子卡券请使用立即购买,不可通过购物车结算");
+ return;
+ }
+ this.$router.push({ path: "/pay", query: { way: "CART" } });
},
// 展示优惠券
showCoupon(storeId, index) {
diff --git a/buyer/src/pages/GoodsDetail.vue b/buyer/src/pages/GoodsDetail.vue
index ecaf8060..cf281726 100644
--- a/buyer/src/pages/GoodsDetail.vue
+++ b/buyer/src/pages/GoodsDetail.vue
@@ -140,8 +140,8 @@ export default {
if (!this.goodsMsg.data.intro) {
this.goodsMsg.data.intro = ''
}
- // 判断是否收藏
- if (this.Cookies.getItem("userInfo")) {
+ // 判断是否收藏(须同时有 token,避免未登录/ token 失效时误跳登录页)
+ if (this.Cookies.getItem("accessToken") && this.Cookies.getItem("userInfo")) {
isStoreCollection("STORE", this.goodsMsg.data.storeId).then((res) => {
if (res.success && res.result) {
this.storeCollected = true;
diff --git a/buyer/src/pages/GoodsList.vue b/buyer/src/pages/GoodsList.vue
index 6cd061a3..8644a133 100644
--- a/buyer/src/pages/GoodsList.vue
+++ b/buyer/src/pages/GoodsList.vue
@@ -80,9 +80,16 @@
>
自营
+
+
+ 电子卡券
+
虚拟
@@ -118,6 +125,7 @@ import { Message } from "@/utils/message";
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import GoodsClassNav from "@/components/nav/GoodsClassNav";
import * as apiGoods from "@/api/goods";
+import { isBuyerLoggedIn, redirectToLogin } from "@/utils/buyerLogin";
export default {
name: "GoodsList",
beforeRouteEnter(to, from, next) {
@@ -194,12 +202,15 @@ export default {
this.getGoodsList();
},
goGoodsDetail(skuId, goodsId) {
- // 跳转商品详情
- let routeUrl = this.$router.resolve({
+ const location = {
path: "/goodsDetail",
query: { skuId, goodsId },
- });
- window.open(routeUrl.href, "_blank");
+ };
+ if (!isBuyerLoggedIn()) {
+ redirectToLogin(this.$router, location);
+ return;
+ }
+ window.open(this.$router.resolve(location).href, "_blank");
},
goShopPage(id) {
if (!id) {
@@ -281,13 +292,24 @@ export default {
}
.goods-show-tag {
- height: 18px;
- width: 32px;
- line-height: 14px;
- white-space: nowrap;
- text-align: center;
+ display: inline-flex;
align-items: center;
- padding: 0 3px;
+ justify-content: center;
+ height: 18px;
+ min-width: 32px;
+ width: auto;
+ line-height: 1;
+ white-space: nowrap;
+ padding: 0 4px;
+ box-sizing: border-box;
+
+ :deep(.el-tag__content) {
+ line-height: 1;
+ }
+}
+
+.goods-show-tag-ecoupon {
+ padding: 0 5px;
}
.goods-show-tag-self {
diff --git a/buyer/src/pages/Login.vue b/buyer/src/pages/Login.vue
index 98cb7762..da9db604 100644
--- a/buyer/src/pages/Login.vue
+++ b/buyer/src/pages/Login.vue
@@ -197,6 +197,7 @@ export default {
mobile: "",
},
verifyStatus: false, // 是否图片验证通过
+ codeSent: false, // 是否已成功发送短信验证码
ruleInline: {
// 验证规则
username: [{required: true, message: "请输入用户名"}],
@@ -237,7 +238,15 @@ export default {
if (this.type) {
this.$refs.verify?.init();
} else {
- let data = JSON.parse(JSON.stringify(this.formSms));
+ if (!this.verifyStatus) {
+ Message.warning("请先完成安全验证");
+ return;
+ }
+ if (!this.codeSent) {
+ Message.warning("请先获取短信验证码");
+ return;
+ }
+ const data = JSON.parse(JSON.stringify(this.formSms));
apiLogin.smsLogin(data).then((res) => {
this.hideVerify();
if (res.success) {
@@ -262,7 +271,7 @@ export default {
} else {
Message.error(res.message);
}
- });
+ }).catch(() => {});
}
}
});
@@ -285,6 +294,7 @@ export default {
sendSms(params).then((res) => {
if (res.success) {
Message.success("验证码发送成功");
+ this.codeSent = true;
let that = this;
this.interval = setInterval(() => {
that.time--;
@@ -292,6 +302,7 @@ export default {
that.time = 60;
that.codeMsg = "重新发送";
that.verifyStatus = false;
+ that.codeSent = false;
clearInterval(that.interval);
} else {
that.codeMsg = that.time;
@@ -300,7 +311,7 @@ export default {
} else {
Message.warning(res.message);
}
- });
+ }).catch(() => {});
}
},
verifyChange(con) {
@@ -453,11 +464,19 @@ export default {
this.$refs.formSms.resetFields();
}
this.verifyStatus = false;
+ this.codeSent = false;
this.hideVerify();
clearInterval(this.interval);
this.codeMsg = "发送验证码";
this.time = 60;
},
+ "formSms.mobile"() {
+ this.codeSent = false;
+ this.verifyStatus = false;
+ clearInterval(this.interval);
+ this.codeMsg = "发送验证码";
+ this.time = 60;
+ },
},
};
diff --git a/buyer/src/pages/home/orderCenter/MyOrder.vue b/buyer/src/pages/home/orderCenter/MyOrder.vue
index 79b5f296..96e456ba 100644
--- a/buyer/src/pages/home/orderCenter/MyOrder.vue
+++ b/buyer/src/pages/home/orderCenter/MyOrder.vue
@@ -72,8 +72,8 @@
取消订单
去支付
确认收货
-
-
+ 申请售后