From 2ead893923ad7695cf3d4a00881da9d817bdfb1a Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jul 2026 22:12:12 +0800 Subject: [PATCH] chore(pay): close residual zibll edge contracts for content pay Expand coupon scope to content categories, document points_deduction as runtime-off like zibll, reject card_pass on content cashier, and pass shop/content context into coupon validate. --- .../src/schema/sections/users/credits.js | 8 ++--- .../schema/class-aksrt-admin-schema-users.php | 8 ++--- inc/classes/class-aksrt-content-pay.php | 19 +++++++++-- inc/classes/class-aksrt-coupon.php | 34 +++++++++++++------ .../class-aksrt-shop-checkout-service.php | 13 +++++-- inc/helpers/aksrt-pay.php | 32 +++++++++++++++++ 6 files changed, 90 insertions(+), 24 deletions(-) diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index 92f298f2..e8659acb 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -79,12 +79,12 @@ export const creditsSchema = { }, { key: 'deduction', - title: '积分抵扣', + title: '积分抵扣(预留)', icon: 'fas fa-percent', fields: [ - { key: 'points_deduction_s', label: '启用积分抵扣', type: 'switch', default: false }, - { key: 'points_deduction_rate', label: '抵扣比例(%)', type: 'number', default: 100, min: 1, max: 100, dependsOn: { key: 'points_deduction_s', value: true } }, - { key: 'points_deduction_max', label: '最大抵扣比例(%)', type: 'number', default: 50, min: 1, max: 100, dependsOn: { key: 'points_deduction_s', value: true } }, + { key: 'points_deduction_s', label: '启用积分抵扣', type: 'switch', default: false, help: '对齐 zibll:运行时暂未启用(zibpay_is_allow_points_deduction 恒为 false)。字段保留供后续接收银台现金单抵扣。' }, + { key: 'points_deduction_rate', label: '积分换算比例', type: 'number', default: 100, min: 1, max: 10000, dependsOn: { key: 'points_deduction_s', value: true }, help: 'zibll 语义:points_deduction_rate = 多少积分抵 1 元(示例 30 表示 30 积分=1 元)。当前未参与下单计算。' }, + { key: 'points_deduction_max', label: '最大抵扣比例(%)', type: 'number', default: 50, min: 1, max: 100, dependsOn: { key: 'points_deduction_s', value: true }, help: '预留:最多可抵订单金额的百分比。当前未参与下单计算。' }, ], }, { diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php index 3aa2b7bb..f2d4ac62 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php @@ -432,10 +432,10 @@ class AKSRT_Admin_Schema_Users { ['key' => 'points_service_charge', 'label' => '转账手续费(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 50], ['key' => 'points_transfer_min', 'label' => '最低转账金额', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 9999], ]], - ['key' => 'deduction', 'title' => '积分抵扣', 'fields' => [ - ['key' => 'points_deduction_s', 'label' => '启用积分抵扣', 'type' => 'switch', 'default' => false], - ['key' => 'points_deduction_rate', 'label' => '抵扣比例(%)', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 100], - ['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100], + ['key' => 'deduction', 'title' => '积分抵扣(预留)', 'fields' => [ + ['key' => 'points_deduction_s', 'label' => '启用积分抵扣', 'type' => 'switch', 'default' => false, 'help' => '对齐 zibll:运行时暂未启用。字段保留供后续接收银台现金单抵扣。'], + ['key' => 'points_deduction_rate', 'label' => '积分换算比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'help' => '多少积分抵 1 元(zibll points_deduction_rate)。当前未参与下单。'], + ['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100, 'help' => '预留:最多可抵订单金额百分比。当前未参与下单。'], ]], ['key' => 'card_pass', 'title' => '积分卡密兑换', 'fields' => [ ['key' => 'points_pass_exchange_s', 'label' => '启用积分卡密兑换', 'type' => 'switch', 'default' => false], diff --git a/inc/classes/class-aksrt-content-pay.php b/inc/classes/class-aksrt-content-pay.php index 76572455..f5f7c2b8 100644 --- a/inc/classes/class-aksrt-content-pay.php +++ b/inc/classes/class-aksrt-content-pay.php @@ -552,6 +552,13 @@ class AKSRT_Content_Pay { 'payable' => round($payable, 2), 'rebate_price' => round($rebate_price, 2), ], + // zibll: points_deduction runtime hard-off; card_pass not a content payment method. + 'points_deduction_allowed' => function_exists('aksrt_is_allow_points_deduction') + ? (bool) aksrt_is_allow_points_deduction('content') + : false, + 'card_pass_allowed' => function_exists('aksrt_is_content_card_pass_pay_allowed') + ? (bool) aksrt_is_content_card_pass_pay_allowed($post) + : false, 'price_type' => $price_type, ]; } @@ -687,9 +694,16 @@ class AKSRT_Content_Pay { return new WP_Error('aksrt_wrong_modo', __('该内容仅支持积分支付。', 'aksrt'), ['status' => 400]); } - // Card pass cannot stack coupon (zibll). + // Card pass cannot stack coupon (zibll). Content cashier does not sell via card_pass. $payment_method = sanitize_key($payment_method); if ($payment_method === 'card_pass') { + if (!function_exists('aksrt_is_content_card_pass_pay_allowed') || !aksrt_is_content_card_pass_pay_allowed($post)) { + return new WP_Error( + 'aksrt_card_pass_not_allowed', + __('内容购买不支持卡密支付,请在用户中心使用卡密兑换余额/会员/积分。', 'aksrt'), + ['status' => 400] + ); + } $coupon_code = ''; } @@ -1170,8 +1184,9 @@ class AKSRT_Content_Pay { } } else { $result = AKSRT_Coupon::get_instance()->validate($coupon_code, $pay, [ - 'user_id' => max(0, $user_id), + 'user_id' => max(0, $user_id), 'product_ids' => [(int) $post->ID], + 'context' => 'content', ]); if (empty($result['valid'])) { $err = new WP_Error( diff --git a/inc/classes/class-aksrt-coupon.php b/inc/classes/class-aksrt-coupon.php index d118b162..589e2349 100644 --- a/inc/classes/class-aksrt-coupon.php +++ b/inc/classes/class-aksrt-coupon.php @@ -161,30 +161,42 @@ class AKSRT_Coupon { } } - // Product scope check + // Product / content scope check. + // Supports shop product cats and content post cats. if (!empty($coupon['product_scope']) && !empty($args['product_ids'])) { $scope = is_array($coupon['product_scope']) ? $coupon['product_scope'] : json_decode($coupon['product_scope'], true); if (is_array($scope) && !empty($scope)) { - $scope_type = (string) ($scope['type'] ?? ''); - $scope_ids = array_map('intval', (array) ($scope['ids'] ?? [])); + $scope_type = sanitize_key((string) ($scope['type'] ?? '')); + $scope_ids = array_map('intval', (array) ($scope['ids'] ?? [])); $product_ids = array_map('intval', (array) $args['product_ids']); + $taxonomies = array_values(array_filter(array_map('sanitize_key', (array) ($scope['taxonomies'] ?? [])))); + if ($taxonomies === []) { + // content context prefers category; shop admin may pass aksrt_product_cat. + $context = sanitize_key((string) ($args['context'] ?? '')); + $taxonomies = ($context === 'content') + ? ['category'] + : ['aksrt_product_cat', 'category']; + } + if ($scope_type === 'include' && empty(array_intersect($product_ids, $scope_ids))) { - return ['valid' => false, 'message' => '该优惠码不适用于当前商品']; + return ['valid' => false, 'message' => __('该优惠码不适用于当前商品', 'aksrt')]; } if ($scope_type === 'exclude' && !empty(array_intersect($product_ids, $scope_ids))) { - return ['valid' => false, 'message' => '该优惠码不适用于当前商品']; + return ['valid' => false, 'message' => __('该优惠码不适用于当前商品', 'aksrt')]; } - if ($scope_type === 'category') { + if (in_array($scope_type, ['category', 'content_category', 'post_category'], true)) { $matched = false; foreach ($product_ids as $pid) { - $cats = wp_get_post_terms($pid, 'aksrt_product_cat', ['fields' => 'ids']); - if (!is_wp_error($cats) && !empty(array_intersect($cats, $scope_ids))) { - $matched = true; - break; + foreach ($taxonomies as $tax) { + $cats = wp_get_post_terms($pid, $tax, ['fields' => 'ids']); + if (!is_wp_error($cats) && !empty(array_intersect(array_map('intval', (array) $cats), $scope_ids))) { + $matched = true; + break 2; + } } } if (!$matched) { - return ['valid' => false, 'message' => '该优惠码不适用于当前商品分类']; + return ['valid' => false, 'message' => __('该优惠码不适用于当前分类', 'aksrt')]; } } } diff --git a/inc/classes/shop/class-aksrt-shop-checkout-service.php b/inc/classes/shop/class-aksrt-shop-checkout-service.php index a53f2c3a..7f13d6a5 100644 --- a/inc/classes/shop/class-aksrt-shop-checkout-service.php +++ b/inc/classes/shop/class-aksrt-shop-checkout-service.php @@ -73,7 +73,10 @@ class AKSRT_Shop_Checkout_Service { $shipping_profile = $this->resolve_shipping_quote($product_views, $shipping_method); $coupon_code = sanitize_text_field((string) ($payload['coupon_code'] ?? '')); - $coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee']); + $product_ids = array_values(array_unique(array_map(static function ($row) { + return (int) ($row['product_id'] ?? $row['id'] ?? 0); + }, $product_views))); + $coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee'], $product_ids); if (!$coupon_result['success']) { return $coupon_result; } @@ -306,7 +309,7 @@ class AKSRT_Shop_Checkout_Service { return $normalized; } - private function resolve_coupon_discount(string $coupon_code, float $order_amount): array { + private function resolve_coupon_discount(string $coupon_code, float $order_amount, array $product_ids = []): array { if ($coupon_code === '') { return ['success' => true, 'discount' => 0.0, 'coupon' => []]; } @@ -315,7 +318,11 @@ class AKSRT_Shop_Checkout_Service { return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')]; } - $result = AKSRT_Coupon::get_instance()->validate($coupon_code, $order_amount); + $result = AKSRT_Coupon::get_instance()->validate($coupon_code, $order_amount, [ + 'user_id' => get_current_user_id(), + 'product_ids' => array_values(array_filter(array_map('intval', $product_ids))), + 'context' => 'shop', + ]); if (empty($result['valid'])) { return ['success' => false, 'message' => $result['message'] ?? __('优惠券不可用。', 'aksrt')]; } diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index c16ace6e..f4032f3b 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -559,6 +559,38 @@ if (!function_exists('aksrt_get_post_rebate_discount')) { } } +/** + * Whether cash checkout may use points deduction (zibll points_deduction). + * + * zibll: zibpay_is_allow_points_deduction() always returns false ("暂未启用"). + * Keep the same contract: schema/UI may exist, runtime is hard-off unless filter overrides. + */ +if (!function_exists('aksrt_is_allow_points_deduction')) { + function aksrt_is_allow_points_deduction(string $context = 'content'): bool { + // Mirror zibll hard-off first. + $allowed = false; + if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_all')) { + $all = AKSRT_Settings::get_all(); + // Even if switch on, default remains false like zibll until product enables it. + if (!empty($all['points_deduction_s']) && class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'credits_enabled') && AKSRT_Settings::credits_enabled()) { + $allowed = false; // still disabled at runtime + } + } + return (bool) apply_filters('aksrt_is_allow_points_deduction', $allowed, $context); + } +} + +/** + * Content cashier does not accept card_pass payment_method. + * Card-pass for balance/VIP/points is user-center redeem (zibll order types 4/8/9), + * not content pay types (reading/download/gallery/video). + */ +if (!function_exists('aksrt_is_content_card_pass_pay_allowed')) { + function aksrt_is_content_card_pass_pay_allowed($post = null): bool { + return (bool) apply_filters('aksrt_is_content_card_pass_pay_allowed', false, $post); + } +} + // ============================================================ // 状态判断 // ============================================================