diff --git a/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-cart.md b/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-cart.md
index 48309218f..db9ba28b7 100644
--- a/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-cart.md
+++ b/.crmeb/skills/crmeb-dev/references/05-用户端开发/user-cart.md
@@ -269,7 +269,7 @@ public function getCartStrategy(): array
| 产品类型 | cart_mode | cart_button | confirm_window | 说明 |
|---------|-----------|-------------|----------------|------|
| normal(实物) | cart | 1 | editable | 双按钮,规格弹窗可选 |
-| virtual(虚拟) | cart | 1 | editable | 双按钮 |
+| virtual(网盘) | direct | 0 | editable | 单按钮「立即购买」,选中规格后跳单品直购页(`/pages/center/order/create/index?product_id=&sku_unique=&product_type=virtual&num=`),不写购物车 |
| cardkey(卡密) | cart | 1 | readonly | 双按钮,规格只读高亮 |
### 7.2 cart_button 字段转换
@@ -281,7 +281,25 @@ public function getCartStrategy(): array
$storeInfo['cart_button'] = ($storeInfo['cart_strategy']['cart_mode'] ?? 'cart') === 'direct' ? 0 : 1;
```
-> 中台 `CenterProductServices` 共 5 处 cart_button 计算(`getDetail`/`buildDefaultPageConfig`/`getProducts`列表/`productDetail`/`getUserList`)需统一走 `getCartModeByType()` 辅助方法,避免带自定义表单的 normal 商品错误隐藏加入购物车按钮。
+> 中台 `CenterProductServices` 共 5 处 cart_button 计算(`getDetail`/`buildDefaultPageConfig`/`getProducts`列表/`productDetail`/`getUserList`)需统一走 `getCartModeByType()` 辅助方法。
+
+**自定义表单商品不参与购物车**:商品 `eb_product_center.custom_form` 非空时,上述各处一律按 `direct` 处理,并统一用 `CenterProductServices::hasCustomForm()` 判断(兼容 JSON 字符串、`[]`、已解码数组):
+
+| 环节 | 行为 |
+| --- | --- |
+| 商品详情 | `cart_strategy.cart_mode=direct`、`cart_button=0`,底部只显示「立即购买」 |
+| 商品列表(`getGoodsList`/`getUserList`) | 列表项 `cart_button=0`,前端卡片展示「立即购买」,点击整卡进详情,不再打开规格/加购弹窗 |
+| 加入购物车接口 | `StoreCartServices::setCart()` 在 `new=false`(加入购物车)时直接抛「该商品需填写自定义表单,请直接购买」;`new=true`(立即购买)不受限 |
+
+> 原因:自定义表单需在下单环节填写,购物车无法携带表单数据;表单收集与落库见 [product-type.md 附二](../03-系统商品说明/product-type.md)。
+
+**direct 模式商品不参与购物车**(网盘等按产品类型声明 `cart_mode=direct` 的商品):`StoreCartServices::setCart()` 在 `new=false` 时走 `isDirectCartMode()` 校验,直接抛「该商品不支持加入购物车,请直接购买」;`new=true`(立即购买)不受限,保证历史链路兼容。
+
+| 环节 | 行为 |
+| --- | --- |
+| 商品详情 | `cart_button=0`,底部单按钮「立即购买」;多规格首次点击先弹规格窗,再次点击跳单品直购页 |
+| 商品列表(`getProducts`/`getUserList`) | 列表项 `cart_button=0`,卡片展示「立即购买」,点击整卡进详情 |
+| 加入购物车接口 | `new=false` 时抛「该商品不支持加入购物车,请直接购买」 |
### 7.3 SKU 信息获取
diff --git a/backend/app/services/order/StoreCartServices.php b/backend/app/services/order/StoreCartServices.php
index b7d72fd85..52f466772 100644
--- a/backend/app/services/order/StoreCartServices.php
+++ b/backend/app/services/order/StoreCartServices.php
@@ -331,6 +331,14 @@ class StoreCartServices extends BaseService
public function setCart(int $uid, int $product_id, int $cart_num = 1, string $product_attr_unique = '', int $type = 0, bool $new = true, int $combination_id = 0, int $seckill_id = 0, int $bargain_id = 0, int $advance_id = 0)
{
if ($cart_num < 1) $cart_num = 1;
+ // 自定义表单商品需先填写表单再下单,购物车无法承载表单数据,禁止加入购物车(立即购买 new=true 不受限)
+ if (!$new && $this->hasCustomForm($product_id)) {
+ throw new ApiException('该商品需填写自定义表单,请直接购买');
+ }
+ // direct 加购模式的产品类型(如网盘商品)只能直接购买,禁止加入购物车(立即购买 new=true 不受限)
+ if (!$new && $this->isDirectCartMode($product_id)) {
+ throw new ApiException('该商品不支持加入购物车,请直接购买');
+ }
if ($type == 0) {
//检查限购
$this->checkLimit($uid, $product_id, $cart_num, $new);
@@ -410,6 +418,41 @@ class StoreCartServices extends BaseService
}
}
+ /**
+ * 判断商品是否配置了自定义表单
+ * 自定义表单需要在下单环节填写,购物车无法携带表单数据
+ * @param int $productId 商品ID
+ * @return bool
+ */
+ protected function hasCustomForm(int $productId): bool
+ {
+ try {
+ $productType = app()->make(CenterProductServices::class)->getProductType($productId);
+ $handler = app()->make(ProductTypeRegistry::class)->get($productType);
+ $extend = $handler->getDetailExtend($productId);
+ return !empty($extend['custom_form']);
+ } catch (\Throwable $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 判断商品所属产品类型是否为 direct 加购模式
+ * direct 模式(网盘等)只能直接下单走单品直购链路,不支持加入购物车
+ * @param int $productId 商品ID
+ * @return bool
+ */
+ protected function isDirectCartMode(int $productId): bool
+ {
+ try {
+ $productType = app()->make(CenterProductServices::class)->getProductType($productId);
+ $handler = app()->make(ProductTypeRegistry::class)->get($productType);
+ return ($handler->getCartStrategy()['cart_mode'] ?? 'cart') === 'direct';
+ } catch (\Throwable $e) {
+ return false;
+ }
+ }
+
/**移除购物车商品
* @param int $uid
* @param array $ids
diff --git a/backend/app/services/product/CenterProductServices.php b/backend/app/services/product/CenterProductServices.php
index 5881a6a47..63635e09e 100644
--- a/backend/app/services/product/CenterProductServices.php
+++ b/backend/app/services/product/CenterProductServices.php
@@ -936,6 +936,11 @@ class CenterProductServices extends BaseService
$storeInfo['selection_spec'] = $handler->getSelectionSpec();
$storeInfo['cart_strategy'] = $handler->getCartStrategy();
+ // 自定义表单商品必须填写表单后直接下单,不支持加入购物车,强制 direct 单按钮模式
+ if ($this->hasCustomForm($storeInfo['custom_form'] ?? [])) {
+ $storeInfo['cart_strategy']['cart_mode'] = 'direct';
+ }
+
// cart_button 由 cart_strategy.cart_mode 决定:direct 模式不展示加入购物车按钮
// 预售商品在模板中走独立分支,不影响此处判断
$storeInfo['cart_button'] = ($storeInfo['cart_strategy']['cart_mode'] ?? 'cart') === 'direct' ? 0 : 1;
@@ -1095,7 +1100,8 @@ class CenterProductServices extends BaseService
'text' => '立即购买',
'action' => 'submitOrder',
// cart_button 由 cart_strategy.cart_mode 决定:direct 模式不展示加入购物车按钮
- 'extra' => ['cart_button' => $this->getCartModeByType($productType) !== 'direct'],
+ // 自定义表单商品需填表后直接下单,同样不展示加入购物车
+ 'extra' => ['cart_button' => !$this->hasCustomForm($extensions['custom_form'] ?? []) && $this->getCartModeByType($productType) !== 'direct'],
],
'submit_api' => 'center/order/create',
];
@@ -1579,6 +1585,27 @@ class CenterProductServices extends BaseService
return $mode;
}
+ /**
+ * 判断商品是否配置了自定义表单
+ * 自定义表单商品需先填写表单才能下单,无法按普通购物车流程结算,因此不支持加入购物车
+ * @param mixed $customForm 中台 custom_form 原值(JSON 字符串或已解码数组)
+ * @return bool
+ */
+ protected function hasCustomForm($customForm): bool
+ {
+ if (empty($customForm)) {
+ return false;
+ }
+ if (is_array($customForm)) {
+ return !empty($customForm);
+ }
+ $decoded = json_decode((string)$customForm, true);
+ if (is_array($decoded)) {
+ return !empty($decoded);
+ }
+ return trim((string)$customForm) !== '';
+ }
+
/**
* 获取产品类型展示名称
* 通过 ProductTypeRegistry 咨询对应 Handler
@@ -1802,6 +1829,10 @@ class CenterProductServices extends BaseService
$item['product_type'] = $item['product_type'] ?? 'normal';
$item['type_name'] = $this->getProductTypeName($item['product_type']);
$item['logistics'] = [];
+ // 相对路径图片补全站点域名,与 getList 口径保持一致(image 主图、recommend_image 推荐图、slider_image 轮播图为数组)
+ $item['image'] = set_file_url($item['image'] ?? '');
+ $item['recommend_image'] = set_file_url($item['recommend_image'] ?? '');
+ $item['slider_image'] = set_file_url($item['slider_image'] ?? []);
// slider_image 由模型获取器自动解码
if (isset($item['cate_id']) && $item['cate_id'] !== '') {
$cateIds[] = $item['cate_id'];
@@ -1838,6 +1869,8 @@ class CenterProductServices extends BaseService
$item['attrs'][$key] = $items;
$item['attrs'][$key]['suk'] = implode(',', $items['detail']);
$item['attrs'][$key]['product_price'] = $items['price'];
+ // 规格图片同样补全站点域名
+ $item['attrs'][$key]['pic'] = set_file_url($items['pic'] ?? '');
$key += 1;
}
}
@@ -2436,7 +2469,8 @@ class CenterProductServices extends BaseService
if (!$this->vipIsOpen(!!$item['is_vip'], $vipStatus)) {
$item['vip_price'] = 0;
}
- $item['cart_button'] = $this->getCartModeByType($item['product_type'] ?? 'normal') === 'direct' ? 0 : 1;
+ // 自定义表单商品不支持加入购物车(前端据此点击直接进详情)
+ $item['cart_button'] = $this->hasCustomForm($item['custom_form'] ?? '') || $this->getCartModeByType($item['product_type'] ?? 'normal') === 'direct' ? 0 : 1;
if (count($item['star'])) {
$item['star'] = bcdiv((string)array_sum(array_column($item['star'], 'product_score')), (string)count($item['star']), 1);
} else {
@@ -2708,9 +2742,16 @@ class CenterProductServices extends BaseService
$productType = $this->getProductType($id);
$handler = app()->make(ProductTypeRegistry::class)->get($productType);
+ // 自定义表单商品不支持加入购物车,强制 direct 单按钮模式
+ $cartStrategy = $handler->getCartStrategy();
+ if ($this->hasCustomForm($storeInfo['custom_form'] ?? [])) {
+ $cartStrategy['cart_mode'] = 'direct';
+ }
+ $storeInfo['cart_strategy'] = $cartStrategy;
+
// cart_button 由 cart_strategy.cart_mode 决定:direct 模式不展示加入购物车按钮
// 预售商品在模板中走独立分支,不影响此处判断
- $storeInfo['cart_button'] = $this->getCartModeByType($productType) === 'direct' ? 0 : 1;
+ $storeInfo['cart_button'] = ($cartStrategy['cart_mode'] ?? 'cart') === 'direct' ? 0 : 1;
$specs = $handler->getDetailSpecs($id);
$productAttr = $specs['productAttr'] ?? [];
@@ -3704,6 +3745,16 @@ class CenterProductServices extends BaseService
//通过事件代理查询商品启用的 SKU 列表,避免 app 层直接引用 modules
$attrValueResults = event('ProductTypeDataProxy', ['product_type' => 'normal', 'action' => 'getShowListByProductId', 'args' => [(int)$id]]);
$attrValue = (is_array($attrValueResults) && $attrValueResults[0] !== null) ? $attrValueResults[0] : [];
+ // SKU 图片在库中为相对路径(/uploads/...),返回前补全站点域名,避免后台规格图裂图
+ $siteUrl = sys_config('site_url');
+ foreach ($attrValue as &$attrItem) {
+ foreach (['image', 'pic'] as $imageField) {
+ if (!empty($attrItem[$imageField]) && is_string($attrItem[$imageField])) {
+ $attrItem[$imageField] = set_file_url($attrItem[$imageField], $siteUrl);
+ }
+ }
+ }
+ unset($attrItem);
return compact('storeInfo', 'attrValue');
}
@@ -3995,6 +4046,7 @@ class CenterProductServices extends BaseService
'sort_order',
'is_vip',
'vip_price',
+ 'custom_form',
];
$list = $this->dao->getSearchList($where, $page, $limit, $field);
@@ -4005,12 +4057,12 @@ class CenterProductServices extends BaseService
$item['vip_price'] = '0.00';
$item['is_virtual'] = $this->isVirtualProduct($item['product_type'] ?? 'normal');
$item['presale'] = 0;
- $item['custom_form'] = '';
$item['min_qty'] = 1;
$item['label_list'] = '';
$item['store_info'] = '';
$item['star'] = '5.0';
- $item['cart_button'] = $this->getCartModeByType($item['product_type'] ?? 'normal') === 'direct' ? 0 : 1;
+ // 自定义表单商品不支持加入购物车(前端据此点击直接进详情)
+ $item['cart_button'] = $this->hasCustomForm($item['custom_form'] ?? '') || $this->getCartModeByType($item['product_type'] ?? 'normal') === 'direct' ? 0 : 1;
// 标记活动商品
if (!empty($item['activity'])) {
diff --git a/backend/modules/product/virtual/Handler.php b/backend/modules/product/virtual/Handler.php
index fc544b96e..c935b6146 100644
--- a/backend/modules/product/virtual/Handler.php
+++ b/backend/modules/product/virtual/Handler.php
@@ -675,14 +675,16 @@ class Handler extends BaseProductTypeHandler
}
/**
- * 加购策略契约(virtual 虚拟:双按钮 + 多品 + 默认文案)
+ * 加购策略契约(virtual 网盘:单按钮直接购买)
+ * 网盘商品支付后自动发放资源,不支持加入购物车:cart_mode=direct
+ * 中台据此下发 cart_button=0,列表/详情均只保留「立即购买」,下单走单品直购链路
* @return array
*/
public function getCartStrategy(): array
{
return [
- 'cart_mode' => 'cart',
- 'multi_item' => true,
+ 'cart_mode' => 'direct',
+ 'multi_item' => false,
'button_text' => '',
];
}
diff --git a/backend/modules/product/virtual/README.md b/backend/modules/product/virtual/README.md
index b39d36728..9dfb53bbb 100644
--- a/backend/modules/product/virtual/README.md
+++ b/backend/modules/product/virtual/README.md
@@ -71,6 +71,14 @@ virtual/
- 已发放后不支持售后
- 仅支持退款类型
+## 用户端加购策略
+
+网盘商品不支持加入购物车,只能直接购买:
+
+- `Handler::getCartStrategy()` 返回 `cart_mode=direct`、`multi_item=false`,中台据此下发 `cart_button=0`,列表与详情只展示「立即购买」。
+- 商品详情页点击「立即购买」跳单品直购页 `/pages/center/order/create/index?product_id=&sku_unique=&product_type=virtual&num=`,由 `CenterOrderController::confirm/create` 的 direct 链路下单,不写购物车(多规格首次点击先弹规格窗确认规格)。
+- `StoreCartServices::setCart()` 对 `cart_mode=direct` 的商品在 `new=false` 时抛「该商品不支持加入购物车,请直接购买」,防止其他入口绕过 UI 加购。
+
## 数据表
- `eb_pt_virtual` — 网盘商品扩展表
diff --git a/frontend/uni-app/src/components/goodClass/index.vue b/frontend/uni-app/src/components/goodClass/index.vue
index a498c65c7..326e7d8db 100644
--- a/frontend/uni-app/src/components/goodClass/index.vue
+++ b/frontend/uni-app/src/components/goodClass/index.vue
@@ -23,6 +23,10 @@
+
+
+ {{$t(`立即购买`)}}
+
{
isOpen.value = false;
@@ -1614,6 +1623,13 @@ function navigateToCreate(cartId, blockData) {
* @param {*} news 是否立即购买(truthy 跳下单页,falsy 提示添加成功)
*/
function submitCartAdd(params, news) {
+ // direct 模式(网盘等):跳过加购,直接进单品直购页
+ if (isDirectMode.value) {
+ isOpen.value = false;
+ attr.cartAttr = false;
+ goDirectBuy(params.uniqueId, params.cartNum);
+ return;
+ }
postCartAdd(params)
.then((res) => {
isOpen.value = false;
@@ -1887,18 +1903,52 @@ function goBuy() {
if (isLogin.value === false) {
toLogin();
} else {
- // direct 模式(预售/租赁/服务):跳过规格弹窗,直接用默认 SKU 下单
+ // direct 模式(网盘等):不经过购物车,选中规格后跳单品直购页
if (isDirectMode.value) {
- const defaultUnique = storeInfo.value.default_sku || storeInfo.value.spec_unique || (attr.productSelect && attr.productSelect.unique) || '';
- const params = buildCartParams(defaultUnique, 1, storeInfo.value.virtual_type);
- params.new = 1;
- submitCartAdd(params, true);
+ // 多规格首次点击先弹规格窗让用户确认规格,再次点击即用当前选中规格直购
+ if (hasMultiSpecValues() && !directSpecOpened.value) {
+ directSpecOpened.value = true;
+ attr.cartAttr = true;
+ isOpen.value = true;
+ return;
+ }
+ goDirectBuy(
+ (attr.productSelect && attr.productSelect.unique) ||
+ storeInfo.value.default_sku ||
+ storeInfo.value.spec_unique ||
+ '',
+ attr.productSelect.cart_num || 1
+ );
return;
}
// this.$refs.proSwiper.videoIsPause();
goCat(true);
}
}
+/**
+ * 是否存在多个可选规格值(多规格商品需让用户确认规格)
+ * @return {boolean}
+ */
+function hasMultiSpecValues() {
+ const productAttr = attr.productAttr || [];
+ return productAttr.some((item) => (item.attr_values || []).length > 1);
+}
+/**
+ * 跳转单品直购下单页(不写入购物车)
+ * @param {string} uniqueId SKU 唯一编码
+ * @param {number} cartNumArg 购买数量
+ */
+function goDirectBuy(uniqueId, cartNumArg) {
+ const pid = storeInfo.value.id || id.value;
+ const productType = storeInfo.value.product_type || 'virtual';
+ const num = Math.max(parseInt(cartNumArg, 10) || 1, 1);
+ const url =
+ `/pages/center/order/create/index?product_id=${pid}` +
+ `&sku_unique=${encodeURIComponent(uniqueId || '')}` +
+ `&product_type=${encodeURIComponent(productType)}` +
+ `&num=${num}`;
+ uni.navigateTo({ url });
+}
function open(data) {
showMenuIcon.value = data;
}
diff --git a/frontend/uni-app/src/pages/goods_details/mock/README.md b/frontend/uni-app/src/pages/goods_details/mock/README.md
index f12a30e54..705ffcaa6 100644
--- a/frontend/uni-app/src/pages/goods_details/mock/README.md
+++ b/frontend/uni-app/src/pages/goods_details/mock/README.md
@@ -222,7 +222,7 @@
| virtual_type | 0 | 1(网盘) | 2(卡密) |
| spec_type | 1(多规格) | 1(多规格) | 0(单规格) |
| requires_shipping | true | false | false |
-| cart_button | 1 | 1 | 1 |
+| cart_button | 1 | 0(direct 直购) | 1 |
| limit_num | 5 | 0 | 5 |
| unit_name | 件 | 份 | 张 |
diff --git a/frontend/uni-app/src/pages/goods_details/mock/virtual.json b/frontend/uni-app/src/pages/goods_details/mock/virtual.json
index ae88d4a45..ea8c24d56 100644
--- a/frontend/uni-app/src/pages/goods_details/mock/virtual.json
+++ b/frontend/uni-app/src/pages/goods_details/mock/virtual.json
@@ -112,11 +112,11 @@
"cart_unit": "item"
},
"cart_strategy": {
- "cart_mode": "cart",
- "multi_item": true,
+ "cart_mode": "direct",
+ "multi_item": false,
"button_text": ""
},
- "cart_button": 1
+ "cart_button": 0
},
"productAttr": [
{
diff --git a/frontend/uni-app/src/pages/goods_details/mock/virtual.jsonc b/frontend/uni-app/src/pages/goods_details/mock/virtual.jsonc
index 742e4c97f..ec5dc8cc5 100644
--- a/frontend/uni-app/src/pages/goods_details/mock/virtual.jsonc
+++ b/frontend/uni-app/src/pages/goods_details/mock/virtual.jsonc
@@ -107,13 +107,13 @@
},
// ===== 加购策略 =====
- // cart_strategy: 加购按钮策略(网盘产品使用cart双按钮加购模式)
+ // cart_strategy: 加购按钮策略(网盘产品只能直接购买,使用 direct 单按钮模式)
"cart_strategy": {
- "cart_mode": "cart", // 加购模式 cart双按钮(加购+立即购买)/buy单按钮(立即购买)
- "multi_item": true, // 是否支持购物车多商品合并
+ "cart_mode": "direct", // 加购模式 cart双按钮(加购+立即购买)/direct单按钮(立即购买)
+ "multi_item": false, // 是否支持购物车多商品合并(direct 模式不支持)
"button_text": "立即购买" // 自定义按钮文字(空使用默认)
},
- "cart_button": 1, // 加购按钮显示 0隐藏/1显示
+ "cart_button": 0, // 加购按钮显示 0隐藏/1显示(direct 模式为 0)
// ===== 预售配置 =====
"presale": 0, // 是否预售 0否/1是