From 1ae7ab3036455bb45756360ca660921945907d3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B0=E9=A6=99=E7=90=AA?= <624506849@qq.com>
Date: Fri, 31 Jul 2026 13:44:56 +0800
Subject: [PATCH] =?UTF-8?q?feat(buyer):=20=E6=B7=BB=E5=8A=A0=E7=94=B5?=
=?UTF-8?q?=E5=AD=90=E5=8D=A1=E5=88=B8=EF=BC=88E=5FCOUPON=EF=BC=89?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=20-=20=E6=96=B0=E5=A2=9E=20E=5FCOUPON=20?=
=?UTF-8?q?=E5=95=86=E5=93=81=E7=B1=BB=E5=9E=8B=EF=BC=8C=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E7=94=B5=E5=AD=90=E5=8D=A1=E5=88=B8=E7=9A=84=E5=B1=95=E7=A4=BA?=
=?UTF-8?q?=E4=B8=8E=E8=B4=AD=E4=B9=B0=E9=80=BB=E8=BE=91=20-=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E5=95=86=E5=93=81=E8=AF=A6=E6=83=85=E9=A1=B5=EF=BC=8C?=
=?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BF=83=E9=94=80=E3=80=81=E5=BA=93=E5=AD=98?=
=?UTF-8?q?=E5=92=8C=E8=B4=AD=E4=B9=B0=E6=8C=89=E9=92=AE=E9=80=BB=E8=BE=91?=
=?UTF-8?q?=E4=BB=A5=E9=80=82=E5=BA=94=E7=94=B5=E5=AD=90=E5=8D=A1=E5=88=B8?=
=?UTF-8?q?=20-=20=E5=A2=9E=E5=8A=A0=E5=8D=A1=E5=AF=86=E4=BF=A1=E6=81=AF?=
=?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=B8=8E=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=95=86=E5=AE=B6=E7=AB=AF=E5=8D=A1?=
=?UTF-8?q?=E6=B1=A0=E7=AE=A1=E7=90=86=20-=20=E6=9B=B4=E6=96=B0=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E9=A1=B5=E9=9D=A2=E4=BB=A5=E5=A4=84=E7=90=86=E7=94=B5?=
=?UTF-8?q?=E5=AD=90=E5=8D=A1=E5=88=B8=E7=9A=84=E7=89=B9=E6=AE=8A=E9=80=BB?=
=?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=A6=82=E5=85=8D=E5=9C=B0=E5=9D=80=E3=80=81?=
=?UTF-8?q?=E9=9A=90=E8=97=8F=E4=BC=98=E6=83=A0=E5=88=B8=E7=AD=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/components/goodsDetail/ShowGoods.vue | 72 ++-
buyer/src/constants/goodsType.js | 39 ++
buyer/src/pages/GoodsList.vue | 32 +-
buyer/src/pages/home/orderCenter/MyOrder.vue | 8 +-
.../pages/home/orderCenter/OrderDetail.vue | 148 +++++-
buyer/src/pages/payment/Pay.vue | 27 +-
buyer/src/pages/payment/PayMent.vue | 23 +-
manager/src/constants/cardKey.js | 18 +
manager/src/views/goods/goods-info/goods.vue | 5 +-
manager/src/views/order/order/orderDetail.vue | 91 +++-
seller/src/api/cardKey.js | 65 +++
seller/src/constants/cardKey.js | 41 ++
seller/src/router/router.js | 7 +
.../src/views/goods/card-key/cardKeyPool.vue | 485 ++++++++++++++++++
seller/src/views/goods/goods-seller/goods.vue | 39 +-
.../goods-seller/goodsOperationFirst.vue | 16 +
.../goods/goods-seller/goodsOperationSec.vue | 145 +++++-
seller/src/views/order/order/orderDetail.vue | 91 +++-
.../views/order/receiptManager/receipt.vue | 2 +-
19 files changed, 1269 insertions(+), 85 deletions(-)
create mode 100644 buyer/src/constants/goodsType.js
create mode 100644 manager/src/constants/cardKey.js
create mode 100644 seller/src/api/cardKey.js
create mode 100644 seller/src/constants/cardKey.js
create mode 100644 seller/src/views/goods/card-key/cardKeyPool.vue
diff --git a/buyer/src/components/goodsDetail/ShowGoods.vue b/buyer/src/components/goodsDetail/ShowGoods.vue
index 74cc4f6c..44500b89 100644
--- a/buyer/src/components/goodsDetail/ShowGoods.vue
+++ b/buyer/src/components/goodsDetail/ShowGoods.vue
@@ -44,7 +44,14 @@
-
+
-
+
优 惠 券
@@ -159,7 +166,7 @@
-
+
促 销
@@ -238,14 +245,16 @@
:precision="0"
@blur="changeCount"
>
- 库存{{ skuDetail.quantity }}
+ 库存{{ displayStock }}
@@ -279,16 +288,16 @@
加入购物车
立即购买
@@ -320,6 +329,12 @@ 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,
+} from "@/constants/goodsType";
export default {
name: "ShowGoods",
@@ -397,11 +412,27 @@ export default {
})
: [];
},
+ /** 卡密商品:禁加购/禁促销,仅立即购买,库存用 poolStock(FR-B-01 / P-06) */
+ 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;
},
},
@@ -425,17 +456,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 +505,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,
@@ -506,10 +545,11 @@ export default {
skuId: this.skuDetail.id,
cartType: "BUY_NOW",
};
- // 虚拟商品购买
+ // 虚拟商品:cartType=VIRTUAL(核销码流程)
if (this.skuDetail.goodsType === "VIRTUAL_GOODS") {
params.cartType = "VIRTUAL";
}
+ // E_COUPON 卡密商品:cartType 须保持 BUY_NOW,勿用 VIRTUAL(FR-B-01 / EC-24)
this.loading1 = true;
addCartGoods(params)
.then((res) => {
diff --git a/buyer/src/constants/goodsType.js b/buyer/src/constants/goodsType.js
new file mode 100644
index 00000000..883c1fcb
--- /dev/null
+++ b/buyer/src/constants/goodsType.js
@@ -0,0 +1,39 @@
+/**
+ * 卡密商品(E_COUPON / 电子卡券)— 买家端公共常量与工具
+ *
+ * 与 VIRTUAL_GOODS(核销型虚拟)区分:E_COUPON 仅立即购买(BUY_NOW),
+ * 支付后自动发卡,订单终态 COMPLETED;可售库存优先读 poolStock。
+ * 需求:lilishop/docs/requirements/card-key-goods-v4.md
+ *
+ * @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(poolStock, 999)) */
+export const E_COUPON_MAX_BUY_NUM = 999;
+
+export function isECoupon(goodsType) {
+ return goodsType === E_COUPON_GOODS_TYPE;
+}
+
+export function isECouponOrder(orderType) {
+ return orderType === E_COUPON_ORDER_TYPE;
+}
+
+/** 可售库存:优先 poolStock */
+export function getECouponStock(sku) {
+ if (!sku) return 0;
+ const pool = sku.poolStock;
+ if (pool !== undefined && pool !== null) {
+ return Number(pool) || 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);
+}
diff --git a/buyer/src/pages/GoodsList.vue b/buyer/src/pages/GoodsList.vue
index 6cd061a3..95445f80 100644
--- a/buyer/src/pages/GoodsList.vue
+++ b/buyer/src/pages/GoodsList.vue
@@ -80,9 +80,16 @@
>
自营
+
+
+ 电子卡券
+
虚拟
@@ -281,13 +288,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/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 @@
取消订单
去支付
确认收货
-
-
+ 申请售后
@@ -141,8 +141,8 @@
取消订单
去支付
确认收货
-
-
+ 申请售后
diff --git a/buyer/src/pages/home/orderCenter/OrderDetail.vue b/buyer/src/pages/home/orderCenter/OrderDetail.vue
index 7389b04a..03fbecae 100644
--- a/buyer/src/pages/home/orderCenter/OrderDetail.vue
+++ b/buyer/src/pages/home/orderCenter/OrderDetail.vue
@@ -30,7 +30,7 @@
>取消订单
查看物流
-
+
核验码:{{ order.order.verificationCode }}
@@ -51,7 +51,7 @@
>
-
+
收货人信息
收货人:{{ order.order.consigneeName }}
手机号码:{{ $filters.secrecyMobile( order.order.consigneeMobile ) }}
@@ -70,7 +70,7 @@
支付方式:{{ order.paymentMethodValue }}
付款状态:{{ order.payStatusValue }}
-
+
配送信息
配送方式:{{ order.deliveryMethodValue }}
配送状态:{{ order.deliverStatusValue }}
@@ -132,6 +132,37 @@
未开发票
+
+
+
卡密信息
+
+
+
+ 查看卡密
+
+
+
+
+
+
+
+
+
+ 复制
+
+
+
+ 复制全部卡密
+
+
+
+
@@ -144,8 +175,8 @@
货号 |
单价 |
数量 |
-
退款状态 |
-
实际退款金额 |
+
退款状态 |
+
实际退款金额 |
小计 |
操作 |
@@ -170,8 +201,8 @@
{{ goods.id }} |
{{ $filters.unitPrice(goods.goodsPrice, "¥") }} |
{{ goods.num }} |
-
{{refundPriceList(goods.isRefund)}} |
-
{{ $filters.unitPrice(goods.refundPrice, "¥") }} |
+
{{refundPriceList(goods.isRefund)}} |
+
{{ $filters.unitPrice(goods.refundPrice, "¥") }} |
{{ $filters.unitPrice((goods.goodsPrice * goods.num), "¥") }} |
商品总价:{{ $filters.unitPrice(order.order.goodsPrice, "¥") }}
-
+
运费:+{{ $filters.unitPrice(order.order.freightPrice, "¥") }}
@@ -319,6 +351,19 @@
取消
+
+
+
+
+ 卡号:{{ row.cardNo }}
+ 卡密:{{ row.cardSecret }}
+ 复制
+
+
+ 复制全部
+ 关闭
+
+
@@ -333,6 +378,12 @@ import {
getPackage
} from "@/api/order.js";
import { afterSaleReason, receiptDetail } from "@/api/member";
+import { isECouponOrder as checkECouponOrder } from "@/constants/goodsType";
+
+/**
+ * 订单详情:E_COUPON 展示 orderItems[].cardKeys,屏蔽售后/物流(FR-B-03 / FR-B-04)。
+ * 移动端已完成订单用 Dialog 查看卡密(原型 P-09 / D-01)。
+ */
export default {
name: "order-detail",
data() {
@@ -340,6 +391,7 @@ export default {
order: {}, // 订单详情数据
progressList: [], // 订单流程
logistics: "", // 物流数据
+ cardKeyDialogVisible: false,
cancelParams: {
// 取消售后参数
orderSn: "",
@@ -352,7 +404,57 @@ export default {
logisticsModal: false,
};
},
+ computed: {
+ /** 电子卡券订单:无物流/核验码,卡密数据来自 API-B-01 orderItems[].cardKeys */
+ isECouponOrder() {
+ return checkECouponOrder(this.order?.order?.orderType);
+ },
+ isVirtualOrder() {
+ return this.order?.order?.orderType === "VIRTUAL";
+ },
+ isNonPhysicalOrder() {
+ return this.isVirtualOrder || this.isECouponOrder;
+ },
+ ecouponCardKeyDelivered() {
+ return (this.order.orderItems || []).some((item) => item.cardKeyDelivered);
+ },
+ ecouponCardKeyRows() {
+ const rows = [];
+ (this.order.orderItems || []).forEach((item) => {
+ (item.cardKeys || []).forEach((ck) => {
+ rows.push({ ...ck });
+ });
+ });
+ return rows;
+ },
+ },
methods: {
+ copyCardKey(row) {
+ const text = `卡号:${row.cardNo || ""}\n卡密:${row.cardSecret || ""}`;
+ this.copyText(text);
+ },
+ copyAllCardKeys() {
+ const text = this.ecouponCardKeyRows
+ .map((row, i) => `${i + 1}. 卡号:${row.cardNo || ""} 卡密:${row.cardSecret || ""}`)
+ .join("\n");
+ this.copyText(text || "");
+ },
+ copyText(text) {
+ if (!text) return;
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => {
+ Message.success("已复制到剪贴板");
+ });
+ } else {
+ const ta = document.createElement("textarea");
+ ta.value = text;
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy");
+ document.body.removeChild(ta);
+ Message.success("已复制到剪贴板");
+ }
+ },
isVatSpecialReceipt (receipt) {
if (!receipt) return false;
const rt = receipt.receiptType != null ? String(receipt.receiptType).trim() : "";
@@ -660,6 +762,32 @@ table {
color: $theme_color;
}
}
+
+.ecoupon-card-keys-mobile {
+ display: none;
+}
+
+.ecoupon-card-keys-pc {
+ display: block;
+}
+
+.card-key-mobile-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #eee;
+ p {
+ margin: 4px 0;
+ word-break: break-all;
+ }
+}
+
+@media (max-width: 768px) {
+ .ecoupon-card-keys-mobile {
+ display: block;
+ }
+ .ecoupon-card-keys-pc {
+ display: none;
+ }
+}
/** 订单进度条 */
.progress {
margin: 15px 0;
diff --git a/buyer/src/pages/payment/Pay.vue b/buyer/src/pages/payment/Pay.vue
index 81cf558e..3c515d10 100644
--- a/buyer/src/pages/payment/Pay.vue
+++ b/buyer/src/pages/payment/Pay.vue
@@ -18,7 +18,8 @@
-
+
+
收货人信息
管理收货人地址
@@ -94,7 +95,7 @@
-
+
配送方式
@@ -166,8 +167,8 @@
编辑
-
-
+
+
-
+
使用礼品卡
@@ -328,6 +329,7 @@ import {
} from "@/api/cart";
import { getStoreAddress } from "@/api/shopentry.js"
import { canUseCouponList } from "@/api/member.js";
+import { isECoupon } from "@/constants/goodsType";
export default {
name: "Pay",
@@ -377,6 +379,14 @@ export default {
);
return Number.isFinite(n) ? n : 0;
},
+ /** 电子卡券结算:免地址、隐藏优惠券/礼品卡/发票(M-01 / O-11) */
+ isECouponCheckout() {
+ return isECoupon(this.goodsType);
+ },
+ /** 虚拟型结算(核销虚拟 + 电子卡券):均不需要收货地址 */
+ isVirtualLikeCheckout() {
+ return this.goodsType === "VIRTUAL_GOODS" || this.isECouponCheckout;
+ },
},
data() {
return {
@@ -854,10 +864,15 @@ export default {
query: { orderType: "TRADE", sn: res.result.sn },
});
}
+ } else if (res.message) {
+ Message.warning(res.message);
}
})
- .catch(() => {
+ .catch((err) => {
Spin.hide();
+ if (err?.message && !err?.data?.message) {
+ Message.error(err.message);
+ }
});
},
// 优惠券可用范围
diff --git a/buyer/src/pages/payment/PayMent.vue b/buyer/src/pages/payment/PayMent.vue
index 4a23be0b..732158cd 100644
--- a/buyer/src/pages/payment/PayMent.vue
+++ b/buyer/src/pages/payment/PayMent.vue
@@ -102,14 +102,21 @@ export default {
title: '支付确认',
content: '确认使用余额支付吗?',
onOk: () => {
- pay(params).then(res => {
- if (res.success) {
- Message.success(res.message)
- this.$router.push('/payDone');
- } else {
- Message.warning(res.message)
- }
- })
+ return pay(params)
+ .then((res) => {
+ if (res.success) {
+ Message.success(res.message || '支付成功');
+ this.$router.push('/payDone');
+ } else {
+ Message.warning(res.message || '支付失败');
+ }
+ })
+ .catch((err) => {
+ // 支付失败兜底:业务错误(如 CARD_KEY_STORE_SELL_FORBIDDEN)通常已由 axios 拦截器提示
+ if (err?.message && !err?.data?.message) {
+ Message.error(err.message);
+ }
+ });
}
});
} else {
diff --git a/manager/src/constants/cardKey.js b/manager/src/constants/cardKey.js
new file mode 100644
index 00000000..7f9cd660
--- /dev/null
+++ b/manager/src/constants/cardKey.js
@@ -0,0 +1,18 @@
+/**
+ * 卡密商品(E_COUPON)— 平台端订单展示用常量
+ *
+ * 平台不管理卡池明文(S-04);本模块仅用于订单详情卡密状态文案。
+ *
+ * @author Mike
+ * @date 2026-07-31
+ */
+/** 卡密状态文案(与后端 CardKeyStatusEnum 一致) */
+export const CARD_KEY_STATUS_TEXT = {
+ UNUSED: "未使用",
+ ALLOCATED: "已分配",
+ VOIDED: "已作废",
+};
+
+export function formatCardKeyStatus(status) {
+ return CARD_KEY_STATUS_TEXT[status] || status || "—";
+}
diff --git a/manager/src/views/goods/goods-info/goods.vue b/manager/src/views/goods/goods-info/goods.vue
index 9600dece..d8638d1a 100644
--- a/manager/src/views/goods/goods-info/goods.vue
+++ b/manager/src/views/goods/goods-info/goods.vue
@@ -53,6 +53,8 @@
>
+
+
@@ -385,7 +387,8 @@ export default {
goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
- return "电子卡券";
+ if (v === "E_COUPON") return "电子卡券"; // 卡密商品
+ return v || "—";
},
marketEnableText(v) {
if (v === "DOWN") return "下架";
diff --git a/manager/src/views/order/order/orderDetail.vue b/manager/src/views/order/order/orderDetail.vue
index ab28264c..e0fb7a85 100644
--- a/manager/src/views/order/order/orderDetail.vue
+++ b/manager/src/views/order/order/orderDetail.vue
@@ -8,7 +8,7 @@
订单取消
收款
订单日志
- 打印发货单
+ 打印发货单
@@ -90,7 +90,7 @@
-
+
收货信息:
{{ orderInfo.order.consigneeName }}
@@ -165,7 +165,7 @@
-->
-
+
配送方式:
{{ orderInfo.deliveryMethodValue }}
@@ -217,6 +217,42 @@
+
+
+
+ 卡密信息
+
+
+
+
+
+
+
+
+
+ {{ formatCardKeyStatus(row.status || 'ALLOCATED') }}
+
+
+
+
+ 复制
+
+
+
+
+
-
@@ -280,7 +316,7 @@
{{item.promotionName}}:|
¥{{ unitPrice(item.discountPrice) }}
-->
- -
+
-
运费:
{{
unitPrice(orderInfo.order.freightPrice, "¥")
@@ -440,6 +476,7 @@ import vueQr from "vue-qr";
import { printElement } from "@/utils/print";
import { ElMessage, ElMessageBox } from "element-plus";
import { unitPrice, clientTypeWay } from "@/utils/filters";
+import { formatCardKeyStatus } from "@/constants/cardKey";
export default {
name: "orderList",
components: {
@@ -447,6 +484,35 @@ export default {
multipleMap,
"vue-qr": vueQr,
},
+ computed: {
+ /** 电子卡券订单:无物流区,卡密来自 orderItems[].cardKeys */
+ isECouponOrder() {
+ const t = this.orderInfo?.order?.orderType;
+ return t === "E_COUPON" || this.$route.query.orderType === "E_COUPON";
+ },
+ isVirtualOrder() {
+ const t = this.orderInfo?.order?.orderType;
+ return t === "VIRTUAL" || this.$route.query.orderType === "VIRTUAL";
+ },
+ isNonPhysicalOrder() {
+ return this.isVirtualOrder || this.isECouponOrder;
+ },
+ ecouponCardKeyDelivered() {
+ return (this.data || []).some((item) => item.cardKeyDelivered);
+ },
+ ecouponCardKeyRows() {
+ const rows = [];
+ (this.data || []).forEach((item) => {
+ (item.cardKeys || []).forEach((ck) => {
+ rows.push({
+ ...ck,
+ goodsName: item.goodsName,
+ });
+ });
+ });
+ return rows;
+ },
+ },
data () {
return {
typeList: [],
@@ -546,6 +612,23 @@ export default {
methods: {
unitPrice,
clientTypeWay,
+ formatCardKeyStatus,
+ copyCardKey(row) {
+ const text = `卡号:${row.cardNo || ""}\n卡密:${row.cardSecret || ""}`;
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => {
+ ElMessage.success("已复制到剪贴板");
+ });
+ } else {
+ const ta = document.createElement("textarea");
+ ta.value = text;
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy");
+ document.body.removeChild(ta);
+ ElMessage.success("已复制到剪贴板");
+ }
+ },
getPromotionText(row) {
let resultText = "";
if (row && row.promotionType) {
diff --git a/seller/src/api/cardKey.js b/seller/src/api/cardKey.js
new file mode 100644
index 00000000..fcb436dc
--- /dev/null
+++ b/seller/src/api/cardKey.js
@@ -0,0 +1,65 @@
+/**
+ * 卡密商品(E_COUPON)— 商家端卡池 HTTP 封装
+ *
+ * Base Path:/store/goods/card-key(axios 已带 /store 前缀)
+ * 对应 card-key-goods-api.md API-S-01a ~ S-07;导出为同步文件流(S-08)。
+ *
+ * @author Mike
+ * @date 2026-07-31
+ */
+import {
+ getRequest,
+ postRequestWithNoForm,
+ putRequest,
+ uploadFileRequest,
+} from "@/libs/axios";
+import { downloadBlob } from "@/utils/downloadBlob";
+
+/** API-S-01a 下载卡密导入模板(同步文件流) */
+export const downloadImportTemplateBlob = () => {
+ return getRequest("/goods/card-key/import/template", {}, "blob");
+};
+
+export const downloadImportTemplate = async () => {
+ const blob = await downloadImportTemplateBlob();
+ downloadBlob(blob, "card-key-import-template.xlsx");
+};
+
+/** API-S-01 批量导入卡密 */
+export const importCardKey = (skuId, file) => {
+ const formData = new FormData();
+ formData.append("skuId", skuId);
+ formData.append("file", file);
+ return uploadFileRequest("/goods/card-key/import", formData);
+};
+
+/** API-S-02 单条新增卡密(后端 @RequestBody,须 application/json) */
+export const addCardKey = (data) => {
+ return postRequestWithNoForm("/goods/card-key/add", data);
+};
+
+/** API-S-03 卡池分页列表 */
+export const getCardKeyList = (params) => {
+ return getRequest("/goods/card-key/list", params);
+};
+
+/** API-S-04 作废卡密 */
+export const voidCardKey = (id) => {
+ return putRequest(`/goods/card-key/void/${id}`);
+};
+
+/** API-S-05 卡池状态统计 */
+export const getCardKeyStats = (skuId) => {
+ return getRequest(`/goods/card-key/stats/${skuId}`);
+};
+
+/** API-S-07 卡池导出(同步文件流) */
+export const exportCardKeyBlob = (params) => {
+ return getRequest("/goods/card-key/export", params, "blob");
+};
+
+export const exportCardKey = async (params, skuId) => {
+ const blob = await exportCardKeyBlob(params);
+ const ts = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
+ downloadBlob(blob, `card-key-${skuId || "export"}-${ts}.xlsx`);
+};
diff --git a/seller/src/constants/cardKey.js b/seller/src/constants/cardKey.js
new file mode 100644
index 00000000..99c78493
--- /dev/null
+++ b/seller/src/constants/cardKey.js
@@ -0,0 +1,41 @@
+/**
+ * 卡密商品(E_COUPON)— 商家端常量
+ *
+ * 卡池状态 UNUSED / ALLOCATED / VOIDED;Tab 与列表筛选、标签色映射。
+ * 需求:card-key-goods-api.md §2.1~2.3
+ *
+ * @author Mike
+ * @date 2026-07-31
+ */
+/** 卡密状态枚举(与后端 CardKeyStatusEnum 一致) */
+export const CARD_KEY_STATUS = {
+ UNUSED: "UNUSED",
+ ALLOCATED: "ALLOCATED",
+ VOIDED: "VOIDED",
+};
+
+export const CARD_KEY_STATUS_TEXT = {
+ UNUSED: "未使用",
+ ALLOCATED: "已分配",
+ VOIDED: "已作废",
+};
+
+export const CARD_KEY_STATUS_TAG = {
+ UNUSED: "success",
+ ALLOCATED: "info",
+ VOIDED: "danger",
+};
+
+/** Tab:全部 + 各状态 */
+export const CARD_KEY_STATUS_TABS = [
+ { key: "", label: "全部" },
+ { key: CARD_KEY_STATUS.UNUSED, label: "未使用" },
+ { key: CARD_KEY_STATUS.ALLOCATED, label: "已分配" },
+ { key: CARD_KEY_STATUS.VOIDED, label: "已作废" },
+];
+
+export const E_COUPON_GOODS_TYPE = "E_COUPON";
+
+export function formatCardKeyStatus(status) {
+ return CARD_KEY_STATUS_TEXT[status] || status || "—";
+}
diff --git a/seller/src/router/router.js b/seller/src/router/router.js
index 36580cbd..a4249cde 100644
--- a/seller/src/router/router.js
+++ b/seller/src/router/router.js
@@ -64,6 +64,13 @@ export const otherRouter = {
name: "goods-draft-operation-edit",
component: () => import("@/views/goods/goods-seller/goodsOperation.vue")
},
+ /** 卡密商品(E_COUPON)卡池管理;query: skuId(必填), goodsId, goodsName */
+ {
+ path: "card-key-pool",
+ title: "卡池管理",
+ name: "card-key-pool",
+ component: () => import("@/views/goods/card-key/cardKeyPool.vue")
+ },
{
path: "add-coupon",
title: "店铺优惠券",
diff --git a/seller/src/views/goods/card-key/cardKeyPool.vue b/seller/src/views/goods/card-key/cardKeyPool.vue
new file mode 100644
index 00000000..a5e7598e
--- /dev/null
+++ b/seller/src/views/goods/card-key/cardKeyPool.vue
@@ -0,0 +1,485 @@
+
+
+
+
+
+
+
+ 缺少 SKU 参数,请从商品列表「卡池管理」进入。
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 批量导入
+
+ 单条新增
+
+ 导出
+
+
+
+
+
+
+ {{ (searchForm.pageNumber - 1) * searchForm.pageSize + $index + 1 }}
+
+
+
+
+
+
+
+ {{ formatStatus(row.status) }}
+
+
+
+
+
+
+
+
+
+ 作废
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+ Excel 模板:第 1 行表头,第 2 行起为数据;列 A 卡号、列 B 卡密;单次最多 10,000 行。
+
+
+ 下载导入模板
+
+
+ 将文件拖到此处,或点击上传
+
+
+ 关闭
+
+
+
+
+
+ 成功 {{ importResult.successCount || 0 }} 条,失败 {{ importResult.failCount || 0 }} 条
+
+
+
+
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
diff --git a/seller/src/views/goods/goods-seller/goods.vue b/seller/src/views/goods/goods-seller/goods.vue
index a90649e6..6ef45bd4 100644
--- a/seller/src/views/goods/goods-seller/goods.vue
+++ b/seller/src/views/goods/goods-seller/goods.vue
@@ -53,6 +53,8 @@
>
+
+
@@ -144,7 +146,7 @@
-
+
上架
@@ -156,6 +158,11 @@
|
编辑
+
+
+ |
+ 卡池管理
+
@@ -192,6 +199,7 @@
import {
getGoodsListData,
getGoodsNumerData,
+ getQueryGoodsIdGoodsList,
upGoods,
lowGoods,
} from "@/api/goods";
@@ -248,7 +256,34 @@ export default {
goodsTypeText(v) {
if (v === "PHYSICAL_GOODS") return "实物商品";
if (v === "VIRTUAL_GOODS") return "虚拟商品";
- return "电子卡券";
+ if (v === "E_COUPON") return "电子卡券"; // 卡密商品
+ return v || "—";
+ },
+ /** 跳转卡池管理;单 SKU 商品默认取第一个 SKU(card-key-pool 需 skuId) */
+ async goCardKeyPool(row) {
+ let skuId = row.skuId;
+ if (!skuId) {
+ try {
+ const res = await getQueryGoodsIdGoodsList(row.id);
+ if (res.success && res.result?.length) {
+ skuId = res.result[0].id;
+ } else {
+ this.$message.warning("该商品暂无 SKU,请先完善商品规格");
+ return;
+ }
+ } catch {
+ this.$message.error("获取商品规格失败");
+ return;
+ }
+ }
+ this.$router.push({
+ path: "/card-key-pool",
+ query: {
+ skuId,
+ goodsId: row.id,
+ goodsName: row.goodsName,
+ },
+ });
},
marketEnableText(v) {
if (v === "DOWN") return "下架";
diff --git a/seller/src/views/goods/goods-seller/goodsOperationFirst.vue b/seller/src/views/goods/goods-seller/goodsOperationFirst.vue
index b4f620e6..8eadd63e 100644
--- a/seller/src/views/goods/goods-seller/goodsOperationFirst.vue
+++ b/seller/src/views/goods/goods-seller/goodsOperationFirst.vue
@@ -84,6 +84,7 @@ export default {
data() {
return {
selectGoodsType: false, // 展示选择商品分类modal
+ /** 商品类型选项;E_COUPON 为卡密商品(与 VIRTUAL_GOODS 核销型区分,见 FR-S-01) */
goodsTypeWay: [
{
title: "实物商品",
@@ -99,6 +100,13 @@ export default {
type: "VIRTUAL_GOODS",
check: false,
},
+ {
+ title: "电子卡券",
+ img: goodsType2Img,
+ desc: "卡密自动发卡,无需物流",
+ type: "E_COUPON", // goodsType;库存由卡池同步,非手动填写
+ check: false,
+ },
],
// 商品分类选择数组
category: [
@@ -230,4 +238,12 @@ export default {
gap: 12px;
width: 100%;
}
+
+.content-goods-publish {
+ .goods-category li.activeClass {
+ background-color: #409eff;
+ border-color: #409eff;
+ color: #fff;
+ }
+}
diff --git a/seller/src/views/goods/goods-seller/goodsOperationSec.vue b/seller/src/views/goods/goods-seller/goodsOperationSec.vue
index ec94a0ae..80de05ab 100644
--- a/seller/src/views/goods/goods-seller/goodsOperationSec.vue
+++ b/seller/src/views/goods/goods-seller/goodsOperationSec.vue
@@ -44,7 +44,7 @@
@@ -52,7 +52,7 @@
批发型
- 虚拟型
+ {{ isECouponGoods ? "电子卡券" : "虚拟型" }}
kg
+
+ {{ baseInfoForm.goodsUnit || "" }}
+
-
+
商品物流信息
@@ -513,7 +521,7 @@
@selected="(list) => { selectedImage = list }"
/>
- 取消
+ 取消
确定
@@ -552,6 +560,22 @@ export default {
type: Object,
},
},
+ computed: {
+ /** 卡密商品:库存只读展示 poolStock,提交时 quantity 传 0(FR-S-01 / P-02) */
+ isECouponGoods() {
+ return this.baseInfoForm.goodsType === "E_COUPON";
+ },
+ isVirtualGoods() {
+ return this.baseInfoForm.goodsType === "VIRTUAL_GOODS";
+ },
+ isVirtualLikeGoods() {
+ return this.isVirtualGoods || this.isECouponGoods;
+ },
+ /** 仅实物需要运费模板;E_COUPON 强制 templateId=0(§6.1) */
+ needsLogistics() {
+ return this.baseInfoForm.goodsType === "PHYSICAL_GOODS";
+ },
+ },
data() {
// 表单验证项,商品价格
const checkPrice = (rule, value, callback) => {
@@ -759,11 +783,32 @@ export default {
}
},
methods: {
+ /** E_COUPON 库存列只读展示,无卡密时显示 0 */
+ resolvePoolStockDisplay(row) {
+ if (row.poolStock != null && row.poolStock !== "") {
+ return row.poolStock;
+ }
+ if (row.quantity != null && row.quantity !== "") {
+ return row.quantity;
+ }
+ return 0;
+ },
+ /** E_COUPON 提交固定传 0,真实库存由卡池同步 */
+ resolveSubmitQuantity(sku) {
+ if (this.isECouponGoods) {
+ return 0;
+ }
+ return sku.quantity;
+ },
+ defaultSkuQuantity() {
+ return this.isECouponGoods ? 0 : "";
+ },
draggableItemKey(item) {
return item;
},
// 选择图片modal
handleCLickImg(val, index) {
+ this.selectedImage = [];
this.picModelFlag = true;
this.selectedFormBtnName = val;
this.$nextTick(() => {
@@ -793,22 +838,60 @@ export default {
desc: "视频大小不能超过10MB",
});
},
+ parseOssSelectionUrl(item) {
+ if (!item) return "";
+ if (typeof item === "string") {
+ const index = item.indexOf(",");
+ return index >= 0 ? item.slice(index + 1) : item;
+ }
+ return item.url || "";
+ },
// 图片选择后回调
callbackSelected(val) {
this.picModelFlag = false;
- if (val && this.selectedFormBtnName == 'selectedSkuImages') {
- this.selectedSku.images.push(val);
+ if (!val?.url) {
+ return;
+ }
+ if (this.selectedFormBtnName === "selectedSkuImages") {
+ if (!this.selectedSku.images) {
+ this.selectedSku.images = [];
+ }
+ this.selectedSku.images.push(val.url);
} else {
this.baseInfoForm[this.selectedFormBtnName].push(val.url);
}
-
},
confirmUrls() {
- if (this.selectedImage && this.selectedFormBtnName == 'selectedSkuImages') {
- this.selectedSku.images = [...this.selectedSku.images, ...this.selectedImage];
- } else {
- this.baseInfoForm[this.selectedFormBtnName] = [...this.baseInfoForm[this.selectedFormBtnName], ...this.selectedImage];
+ const urls = (this.selectedImage || [])
+ .map((item) => this.parseOssSelectionUrl(item))
+ .filter(Boolean);
+ if (!urls.length) {
+ this.$Message.warning("请选择图片");
+ return;
}
+ if (this.selectedFormBtnName === "selectedSkuImages") {
+ if (!this.selectedSku.images) {
+ this.selectedSku.images = [];
+ }
+ urls.forEach((url) => {
+ if (this.selectedSku.images.length < 5) {
+ this.selectedSku.images.push(url);
+ }
+ });
+ } else if (this.selectedFormBtnName === "goodsGalleryFiles") {
+ urls.forEach((url) => {
+ if (this.baseInfoForm.goodsGalleryFiles.length < 5) {
+ this.baseInfoForm.goodsGalleryFiles.push(url);
+ }
+ });
+ } else if (this.selectedFormBtnName) {
+ const target = this.baseInfoForm[this.selectedFormBtnName];
+ if (Array.isArray(target)) {
+ urls.forEach((url) => target.push(url));
+ }
+ }
+ this.selectedImage = [];
+ this.picModelFlag = false;
},
// 局部刷新
refresh(v) {
@@ -1255,6 +1338,7 @@ export default {
price: e.price,
// cost: e.cost,
quantity: e.quantity,
+ poolStock: e.poolStock,
// alertQuantity: e.alertQuantity,
weight: e.weight,
};
@@ -1724,14 +1808,20 @@ export default {
...combination,
id: existingCombination.id || "",
sn: existingCombination.sn || "",
- quantity: existingCombination.quantity || "",
+ quantity: this.isECouponGoods
+ ? this.resolvePoolStockDisplay(existingCombination)
+ : (existingCombination.quantity || ""),
+ poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || "",
weight: existingCombination.weight || ""
};
} else {
// 新组合使用默认值
- return combination;
+ return {
+ ...combination,
+ quantity: this.defaultSkuQuantity(),
+ };
}
});
this.baseInfoForm.regeneratorSkuFlag = true;
@@ -1779,14 +1869,20 @@ export default {
...combination,
id: existingCombination.id || "",
sn: existingCombination.sn || "",
- quantity: existingCombination.quantity || "",
+ quantity: this.isECouponGoods
+ ? this.resolvePoolStockDisplay(existingCombination)
+ : (existingCombination.quantity || ""),
+ poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || "",
weight: existingCombination.weight || ""
};
} else {
// 新组合使用默认值
- return combination;
+ return {
+ ...combination,
+ quantity: this.defaultSkuQuantity(),
+ };
}
});
@@ -1833,7 +1929,7 @@ export default {
// 有重量的情况
if (
- this.baseInfoForm.goodsType !== "VIRTUAL_GOODS" &&
+ this.needsLogistics &&
this.baseInfoForm.salesModel !== "WHOLESALE"
) {
pushData.push({
@@ -1843,7 +1939,7 @@ export default {
}
pushData.push(
{
- title: "库存",
+ title: this.isECouponGoods ? "卡池可用数" : "库存",
slot: "quantity",
},
// {
@@ -1878,7 +1974,10 @@ export default {
...combination,
id: existingCombination.id || "",
sn: existingCombination.sn || "",
- quantity: existingCombination.quantity || "",
+ quantity: this.isECouponGoods
+ ? this.resolvePoolStockDisplay(existingCombination)
+ : (existingCombination.quantity || ""),
+ poolStock: existingCombination.poolStock,
cost: existingCombination.cost || "",
price: existingCombination.price || (this.baseInfoForm.salesModel === 'WHOLESALE' && this.wholesaleData.length > 0 ? this.wholesaleData[0].price : ""),
weight: existingCombination.weight || ""
@@ -1889,7 +1988,7 @@ export default {
...combination,
id: "",
sn: "",
- quantity: "",
+ quantity: this.defaultSkuQuantity(),
cost: "",
price: this.baseInfoForm.salesModel === 'WHOLESALE' && this.wholesaleData.length > 0 ? this.wholesaleData[0].price : "",
weight: ""
@@ -2153,6 +2252,10 @@ export default {
return;
}
if (submit.templateId === "") submit.templateId = 0;
+ if (this.isECouponGoods) {
+ submit.templateId = 0;
+ submit.salesModel = "RETAIL";
+ }
let flag = false;
let paramValue = "";
@@ -2175,7 +2278,7 @@ export default {
let skuCopy = {
cost: 1,
price: sku.price,
- quantity: sku.quantity,
+ quantity: this.resolveSubmitQuantity(sku),
// alertQuantity: sku.alertQuantity,
sn: sku.sn,
images: [],
diff --git a/seller/src/views/order/order/orderDetail.vue b/seller/src/views/order/order/orderDetail.vue
index 5364d3d8..c1c1cb13 100644
--- a/seller/src/views/order/order/orderDetail.vue
+++ b/seller/src/views/order/order/orderDetail.vue
@@ -27,7 +27,7 @@
@click="toPrint"
>打印电子面单
-
+
收货信息:
{{ orderInfo.order.consigneeName }}
@@ -204,7 +204,7 @@
-->
-
+
配送方式:
{{ orderInfo.deliveryMethodValue }}
@@ -256,6 +256,42 @@
+
+
+
+ 卡密信息
+
+
+
+
+
+
+
+
+
+ {{ formatCardKeyStatus(row.status || 'ALLOCATED') }}
+
+
+
+
+ 复制
+
+
+
+
+
-
@@ -319,7 +355,7 @@
{{item.promotionName}}:|
¥{{ $filters.unitPrice(item.discountPrice) }}
-->
- -
+
-
运费:
{{
$filters.unitPrice(orderInfo.order.freightPrice, "¥")
@@ -694,6 +730,7 @@ import * as RegExp from "@/libs/RegExp.js";
import multipleMap from "@/views/my-components/map/multiple-map";
import vueQr from "vue-qr";
import { printElement } from "@/utils/print";
+import { formatCardKeyStatus } from "@/constants/cardKey";
export default {
name: "orderDetail",
components: {
@@ -814,11 +851,38 @@ export default {
};
},
computed: {
+ /** 电子卡券订单:隐藏物流/发货/售后,展示 orderItems[].cardKeys(FR-S-04 / FR-S-05) */
+ isECouponOrder() {
+ const t = this.orderInfo?.order?.orderType;
+ return t === "E_COUPON" || this.$route.query.orderType === "E_COUPON";
+ },
+ isVirtualOrder() {
+ const t = this.orderInfo?.order?.orderType;
+ return t === "VIRTUAL" || this.$route.query.orderType === "VIRTUAL";
+ },
+ isNonPhysicalOrder() {
+ return this.isVirtualOrder || this.isECouponOrder;
+ },
+ ecouponCardKeyDelivered() {
+ return (this.data || []).some((item) => item.cardKeyDelivered);
+ },
+ ecouponCardKeyRows() {
+ const rows = [];
+ (this.data || []).forEach((item) => {
+ (item.cardKeys || []).forEach((ck) => {
+ rows.push({
+ ...ck,
+ goodsName: item.goodsName,
+ });
+ });
+ });
+ return rows;
+ },
canPartDelivery() {
if (!this.allowOperation.ship) return false;
const status = this.orderInfo.order && this.orderInfo.order.orderStatus;
return (
- this.$route.query.orderType !== "VIRTUAL" &&
+ !this.isNonPhysicalOrder &&
["UNDELIVERED", "PARTS_DELIVERED"].includes(status) &&
this.deliverableOrderItems.length > 0
);
@@ -845,6 +909,23 @@ export default {
},
},
methods: {
+ formatCardKeyStatus,
+ copyCardKey(row) {
+ const text = `卡号:${row.cardNo || ""}\n卡密:${row.cardSecret || ""}`;
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => {
+ this.$Message.success("已复制到剪贴板");
+ });
+ } else {
+ const ta = document.createElement("textarea");
+ ta.value = text;
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand("copy");
+ document.body.removeChild(ta);
+ this.$Message.success("已复制到剪贴板");
+ }
+ },
getPromotionText(row) {
let resultText = "";
if (row && row.promotionType) {
diff --git a/seller/src/views/order/receiptManager/receipt.vue b/seller/src/views/order/receiptManager/receipt.vue
index dca4f7db..1ff78aa6 100644
--- a/seller/src/views/order/receiptManager/receipt.vue
+++ b/seller/src/views/order/receiptManager/receipt.vue
@@ -196,7 +196,7 @@ export default {
receiptModalMode: "detail",
uploadFileUrl: uploadFile,
accessToken: {},
- receiptUploadData: { directoryPath: "receipt" },
+ receiptUploadData: { directoryPath: "default" }, // OSS 上传目录(与后端存储配置一致)
currentReceipt: {},
selectedReceiptRow: null,
searchForm: {
|