diff --git a/buyer/src/api/goods.js b/buyer/src/api/goods.js index 76fe0d03..1cba631b 100644 --- a/buyer/src/api/goods.js +++ b/buyer/src/api/goods.js @@ -33,12 +33,13 @@ export function goodsDetail (goodsId) { }); } -// id获取商品Sku详情 +// id获取商品Sku详情(支持未登录浏览,登录后可选携带 token) export function goodsSkuDetail (params) { return request({ url: `/buyer/goods/goods/sku/${params.goodsId}/${params.skuId}`, method: Method.GET, - needToken: true, + needToken: false, + optionalToken: true, params }); } diff --git a/buyer/src/api/pay.js b/buyer/src/api/pay.js index 0b0d2e02..12246a6e 100644 --- a/buyer/src/api/pay.js +++ b/buyer/src/api/pay.js @@ -24,12 +24,13 @@ export function tradeDetail (params) { * @param payClient 调起方式 可用值:APP,NATIVE,JSAPI,H5 * @param sn 订单编号 */ -export function pay (params) { +export function pay (params, config = {}) { return request({ url: `/buyer/payment/cashier/pay/${params.paymentMethod}/${params.paymentClient}`, needToken: true, method: Method.GET, - params + params, + skipMessage: !!config.skipMessage, }); } diff --git a/buyer/src/api/shopentry.js b/buyer/src/api/shopentry.js index 05bdf3d1..0e5fb90e 100644 --- a/buyer/src/api/shopentry.js +++ b/buyer/src/api/shopentry.js @@ -46,7 +46,8 @@ export function applyThird (params) { export function getDetailById (id) { return request({ url: `/buyer/store/store/get/detail/${id}`, - needToken: true, + needToken: false, + optionalToken: true, method: Method.GET }) } diff --git a/buyer/src/components/goodsDetail/ShowGoods.vue b/buyer/src/components/goodsDetail/ShowGoods.vue index 98a81c86..2e74b747 100644 --- a/buyer/src/components/goodsDetail/ShowGoods.vue +++ b/buyer/src/components/goodsDetail/ShowGoods.vue @@ -259,7 +259,6 @@ -
@@ -339,6 +338,14 @@ @click="buyNow" >立即购买 + @@ -374,6 +381,7 @@ import { getECouponMaxBuyNum, createEmptyPromotionMap, resolveECouponCartType, + eCouponAfterSaleHint, E_COUPON_CART_BUY_NOW, E_COUPON_CART_PINTUAN, E_COUPON_CART_POINTS, @@ -476,6 +484,7 @@ export default { }, }, methods: { + eCouponAfterSaleHint, // 初始化video initVideo(){ if(!this.goodsVideo ){ @@ -724,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; @@ -1052,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 index 73f3bfe6..70f27872 100644 --- a/buyer/src/constants/goodsType.js +++ b/buyer/src/constants/goodsType.js @@ -28,6 +28,14 @@ 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; @@ -132,23 +140,19 @@ function normalizeCardKeyFulfillLine(source) { }; } -/** 汇总 E_COUPON 主单行 + 满赠子单摘要的履约行 */ -export function collectCardKeyFulfillLines(orderItems = [], giftSummaries = []) { +/** 汇总当前订单 orderItems 中的卡密履约行(不含满赠子单;赠品卡密在赠品子单详情单独查看) */ +export function collectCardKeyFulfillLines(orderItems = []) { const lines = []; (orderItems || []).forEach((item) => { if (!isCardKeyFulfillApplicable(item)) return; lines.push(normalizeCardKeyFulfillLine(item)); }); - (giftSummaries || []).forEach((gift) => { - lines.push(normalizeCardKeyFulfillLine({ ...gift, isGift: true })); - }); return lines; } -export function orderHasCardKeySection({ orderItems, giftSummaries, isECouponOrder } = {}) { +export function orderHasCardKeySection({ orderItems, isECouponOrder } = {}) { if (isECouponOrder) return true; - if (Array.isArray(giftSummaries) && giftSummaries.length > 0) return true; - if (collectCardKeyFulfillLines(orderItems, []).length > 0) return true; + if (collectCardKeyFulfillLines(orderItems).length > 0) return true; return (orderItems || []).some( (item) => item.goodsType === E_COUPON_GOODS_TYPE || @@ -167,15 +171,14 @@ export function flattenCardKeyFulfillLines(lines, withGoodsName = false) { return rows; } -/** 订单是否含已交付或可展示的卡密行(含满赠 E_COUPON 子单行) */ -export function orderHasCardKeyContent(orderItems, giftSummaries) { - const lines = collectCardKeyFulfillLines(orderItems, giftSummaries); - return flattenCardKeyFulfillLines(lines).length > 0; +/** 当前订单是否含已交付卡密(仅 orderItems,不含满赠子单) */ +export function orderHasCardKeyContent(orderItems) { + return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems)).length > 0; } -/** 扁平化订单项卡密(仅 cardKeyDelivered 且含 cardKeys;兼容旧逻辑) */ -export function flattenOrderCardKeys(orderItems, withGoodsName = false, giftSummaries) { - const lines = collectCardKeyFulfillLines(orderItems, giftSummaries); +/** 扁平化当前订单 orderItems 卡密 */ +export function flattenOrderCardKeys(orderItems, withGoodsName = false) { + const lines = collectCardKeyFulfillLines(orderItems); if (lines.some((line) => line.status)) { return flattenCardKeyFulfillLines(lines, withGoodsName); } 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/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 95445f80..8644a133 100644 --- a/buyer/src/pages/GoodsList.vue +++ b/buyer/src/pages/GoodsList.vue @@ -125,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) { @@ -201,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) { diff --git a/buyer/src/pages/home/orderCenter/OrderDetail.vue b/buyer/src/pages/home/orderCenter/OrderDetail.vue index 10f1fcb4..1ce29e45 100644 --- a/buyer/src/pages/home/orderCenter/OrderDetail.vue +++ b/buyer/src/pages/home/orderCenter/OrderDetail.vue @@ -132,7 +132,7 @@
未开发票
- +

卡密信息

-
- {{ line.goodsName || (line.isGift ? "满赠电子卡券" : "电子卡券") }} -
- @@ -446,36 +436,25 @@ export default { isNonPhysicalOrder() { return this.isVirtualOrder || this.isECouponOrder; }, - /** 是否展示卡密区块(含满赠 E_COUPON 子单 giftECouponOrders) */ + /** 是否展示卡密区块(仅当前订单 orderItems) */ hasCardKeySection() { return orderHasCardKeySection({ orderItems: this.order.orderItems, - giftSummaries: this.order.giftECouponOrders, isECouponOrder: this.isECouponOrder, }); }, cardKeyFulfillLines() { - return collectCardKeyFulfillLines( - this.order.orderItems, - this.order.giftECouponOrders - ); + return collectCardKeyFulfillLines(this.order.orderItems); }, ecouponCardKeyRows() { - return flattenOrderCardKeys( - this.order.orderItems, - false, - this.order.giftECouponOrders - ); + return flattenOrderCardKeys(this.order.orderItems, false); }, }, methods: { cardKeyFulfillAlertType, resolveCardKeyFulfillMessage, lineCardKeyRows(line) { - return (line.cardKeys || []).map((ck) => ({ - ...ck, - goodsName: line.goodsName, - })); + return line.cardKeys || []; }, copyCardKey(row) { const text = `卡号:${row.cardNo || ""}\n卡密:${row.cardSecret || ""}`; @@ -821,12 +800,6 @@ table { border-top: 1px dashed #eee; } -.ecoupon-fulfill-title { - font-weight: 600; - color: #333; - margin-bottom: 8px; -} - .ecoupon-card-keys-pc { display: block; } diff --git a/buyer/src/pages/payment/ThirdPay.vue b/buyer/src/pages/payment/ThirdPay.vue index 97df0e77..619b2473 100644 --- a/buyer/src/pages/payment/ThirdPay.vue +++ b/buyer/src/pages/payment/ThirdPay.vue @@ -11,19 +11,31 @@
-
{{params.paymentMethod === 'ALIPAY' ? '支付宝支付' : '微信支付'}}
+
{{ payWayName }}支付
-
- +
+
正在生成支付二维码...
+
+

{{ payError }}

+

您可点击「重新支付」重试,或返回选择其他支付方式

+
+
-
- 请使用{{params.paymentMethod === 'ALIPAY' ? '支付宝' : '微信'}}扫码付款 +
+ 请使用{{ payWayName }}扫码付款

支付成功后自动跳转,如未跳转请点击按钮手动跳转。。。

- 重新支付 + 重新支付 支付成功
@@ -41,47 +53,79 @@ export default { components: { vueQr, Iphone }, data () { return { - qrcode: '', // 二维码 - params: this.$route.query, // 参数 - interval: null, // 定时器 - num: 0 // 商品数 + qrcode: '', + params: this.$route.query, + interval: null, + num: 0, + payLoading: false, + payError: '', }; }, - methods: { - // 获取支付二维码 - handlePay () { - const params = this.$route.query; - pay(params).then(res => { - if (res.success) { - this.qrcode = res.result; - this.num = 0; - this.interval = setInterval(this.callback, 5000); - } else { - Message.error(res.message) - } - }); + computed: { + payWayName () { + return this.params.paymentMethod === 'ALIPAY' ? '支付宝' : '微信'; }, - callback () { // 支付回调接口 - this.num++; - if (this.num >= 7) { + }, + methods: { + formatPayError (rawMessage) { + const message = (rawMessage || '').trim(); + if (!message || message === '支付业务异常,请稍后重试') { + return `${this.payWayName}支付暂不可用,请稍后重试或选择其他支付方式`; + } + return message; + }, + clearPayPoll () { + if (this.interval) { clearInterval(this.interval); this.interval = null; } - let params = JSON.parse(JSON.stringify(this.$route.query)); + }, + handlePay () { + this.clearPayPoll(); + const params = this.$route.query; + this.payLoading = true; + this.payError = ''; + this.qrcode = ''; + pay(params, { skipMessage: true }) + .then((res) => { + this.payLoading = false; + if (res.success && res.result) { + this.qrcode = res.result; + this.num = 0; + this.interval = setInterval(this.callback, 5000); + return; + } + this.payError = this.formatPayError(res.message); + Message.error(this.payError); + }) + .catch((err) => { + this.payLoading = false; + this.payError = this.formatPayError(err?.message || err?.data?.message); + Message.error(this.payError); + }); + }, + callback () { + this.num++; + if (this.num >= 7) { + this.clearPayPoll(); + } + const params = JSON.parse(JSON.stringify(this.$route.query)); delete params.paymentMethod; delete params.paymentClient; - payCallback(params).then(res => { + payCallback(params).then((res) => { if (res.result) { - clearInterval(this.interval); - this.interval = null; - this.$router.push({path: '/payDone', query: {orderType: this.$route.query.orderType}}); + this.clearPayPoll(); + this.$router.push({ path: '/payDone', query: { orderType: this.$route.query.orderType } }); } }); - } + }, }, mounted () { this.handlePay(); - } + }, + beforeUnmount () { + this.clearPayPoll(); + }, }; diff --git a/buyer/src/pages/promotion/PointMall.vue b/buyer/src/pages/promotion/PointMall.vue index 7fde0e01..8d390526 100644 --- a/buyer/src/pages/promotion/PointMall.vue +++ b/buyer/src/pages/promotion/PointMall.vue @@ -18,7 +18,7 @@ @click="goGoodsDetail(item.id)" >
- +
@@ -26,7 +26,7 @@
- {{ item.goodsSku.goodsName }} + {{ itemGoodsName(item) }}
已有{{ item.commentNum || 0 }}人评价 @@ -70,6 +70,13 @@ export default { this.getCate() }, methods: { + /** 列表 API 返回 PointsGoods(thumbnail/goodsName 在顶层);详情 API 才有 goodsSku */ + itemThumbnail (item) { + return item?.goodsSku?.thumbnail || item?.thumbnail || '' + }, + itemGoodsName (item) { + return item?.goodsSku?.goodsName || item?.goodsName || '' + }, getList () { // 获取列表 pointGoods(this.params).then(res => { if (res.success) { diff --git a/buyer/src/plugins/request.js b/buyer/src/plugins/request.js index f175af91..d1ac4296 100644 --- a/buyer/src/plugins/request.js +++ b/buyer/src/plugins/request.js @@ -70,7 +70,6 @@ service.interceptors.request.use( const accessToken = Storage.getItem("accessToken"); if (accessToken && (config.needToken || config.optionalToken)) { - config.headers["accessToken"] = accessToken; try { const jwtData = JSON.parse( decodeURIComponent( @@ -80,12 +79,23 @@ service.interceptors.request.use( ) ) ); - if (jwtData.exp < Math.round(new Date() / 1000)) { - refresh({ response: { config } }); + const expired = jwtData.exp < Math.round(new Date() / 1000); + if (expired) { + if (config.needToken) { + return refresh({ response: { config } }); + } + // 公开接口:丢弃过期 token,匿名继续请求,避免浏览商品时被引导登录 + Storage.removeItem("accessToken"); + Storage.removeItem("refreshToken"); + } else { + config.headers["accessToken"] = accessToken; } } catch { Storage.removeItem("accessToken"); Storage.removeItem("refreshToken"); + if (config.needToken) { + return refresh({ response: { config } }); + } } } diff --git a/buyer/src/utils/buyerLogin.js b/buyer/src/utils/buyerLogin.js new file mode 100644 index 00000000..9a8ffe5a --- /dev/null +++ b/buyer/src/utils/buyerLogin.js @@ -0,0 +1,59 @@ +/** + * 买家端登录态与商品详情跳转校验 + * + * @author Mike + * @date 2026-08-05 + */ +import storage from "@/plugins/storage"; + +export function isBuyerLoggedIn() { + const rawUserInfo = storage.getItem("userInfo"); + if (rawUserInfo) { + try { + return !!JSON.parse(rawUserInfo).username; + } catch { + return false; + } + } + return !!storage.getItem("accessToken"); +} + +export function parseInternalLink(url) { + const [path, search = ""] = url.split("?"); + const query = {}; + if (search) { + new URLSearchParams(search).forEach((value, key) => { + query[key] = value; + }); + } + return { path, query }; +} + +export function isGoodsDetailPath(path) { + return path === "/goodsDetail"; +} + +export function redirectToLogin(router, location) { + router.push({ + path: "/login", + query: { + rePath: location.path, + query: JSON.stringify(location.query || {}), + }, + }); +} + +/** 内部链接跳转;访问商品详情前校验登录(首页装修、列表等共用) */ +export function openLink(url, router) { + if (!url) return; + if (url.charAt(0) !== "/") { + window.open(url, "_blank"); + return; + } + const location = parseInternalLink(url); + if (!isBuyerLoggedIn() && isGoodsDetailPath(location.path)) { + redirectToLogin(router, location); + return; + } + window.open(router.resolve(url).href, "_blank"); +} diff --git a/manager/src/views/order/order/orderDetail.vue b/manager/src/views/order/order/orderDetail.vue index e0fb7a85..7c9b9e73 100644 --- a/manager/src/views/order/order/orderDetail.vue +++ b/manager/src/views/order/order/orderDetail.vue @@ -236,7 +236,6 @@ style="width: 100%" > - @@ -504,10 +503,7 @@ export default { const rows = []; (this.data || []).forEach((item) => { (item.cardKeys || []).forEach((ck) => { - rows.push({ - ...ck, - goodsName: item.goodsName, - }); + rows.push({ ...ck }); }); }); return rows; diff --git a/seller/src/constants/goodsType.js b/seller/src/constants/goodsType.js index da5c4792..6f1d4ab4 100644 --- a/seller/src/constants/goodsType.js +++ b/seller/src/constants/goodsType.js @@ -92,22 +92,19 @@ function normalizeCardKeyFulfillLine(source) { }; } -export function collectCardKeyFulfillLines(orderItems = [], giftSummaries = []) { +/** 汇总当前订单 orderItems 中的卡密履约行(不含满赠子单;赠品卡密在赠品子单详情单独查看) */ +export function collectCardKeyFulfillLines(orderItems = []) { const lines = []; (orderItems || []).forEach((item) => { if (!isCardKeyFulfillApplicable(item)) return; lines.push(normalizeCardKeyFulfillLine(item)); }); - (giftSummaries || []).forEach((gift) => { - lines.push(normalizeCardKeyFulfillLine({ ...gift, isGift: true })); - }); return lines; } -export function orderHasCardKeySection({ orderItems, giftSummaries, isECouponOrder } = {}) { +export function orderHasCardKeySection({ orderItems, isECouponOrder } = {}) { if (isECouponOrder) return true; - if (Array.isArray(giftSummaries) && giftSummaries.length > 0) return true; - if (collectCardKeyFulfillLines(orderItems, []).length > 0) return true; + if (collectCardKeyFulfillLines(orderItems).length > 0) return true; return (orderItems || []).some( (item) => item.goodsType === E_COUPON_GOODS_TYPE || @@ -126,13 +123,13 @@ export function flattenCardKeyFulfillLines(lines, withGoodsName = false) { return rows; } -/** 订单是否含已交付卡密(含满赠子单) */ -export function orderHasCardKeyContent(orderItems, giftSummaries) { - return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems, giftSummaries)).length > 0; +/** 当前订单是否含已交付卡密(仅 orderItems) */ +export function orderHasCardKeyContent(orderItems) { + return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems)).length > 0; } -export function flattenOrderCardKeys(orderItems, withGoodsName = false, giftSummaries) { - const lines = collectCardKeyFulfillLines(orderItems, giftSummaries); +export function flattenOrderCardKeys(orderItems, withGoodsName = false) { + const lines = collectCardKeyFulfillLines(orderItems); if (lines.some((line) => line.status)) { return flattenCardKeyFulfillLines(lines, withGoodsName); } diff --git a/seller/src/views/goods/card-key/cardKeyPool.vue b/seller/src/views/goods/card-key/cardKeyPool.vue index 7b3964af..398326fe 100644 --- a/seller/src/views/goods/card-key/cardKeyPool.vue +++ b/seller/src/views/goods/card-key/cardKeyPool.vue @@ -5,11 +5,29 @@