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 @@
{{ skuDetail.goodsName }} + 电子卡券 + + @@ -64,7 +71,7 @@
@@ -73,7 +80,7 @@

-
+
价     格 @@ -119,7 +126,7 @@
-
+
优 惠 券 @@ -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 @@ 编辑
- -
+ +
优惠券
@@ -203,7 +204,7 @@
-
+
使用礼品卡 @@ -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 @@ + + +
+

卡密信息

+ + + + + + + + + + + + + + +
+
  • @@ -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 @@ + + + + + + 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 @@ - + @@ -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 ? "电子卡券" : "虚拟型" }} + + +
-
+

商品物流信息

@@ -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 @@ + + +
+

卡密信息

+ + + + + + + + + + + + + + +
+
  • @@ -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: {