feat(forum): plate follow cash coupons + free auto-order
- Meta _aksrt_follow_pay_coupon_s + plate admin toggle (money only) - Plate_Follow_Pay soft/hard coupon apply, free-after-discount, burn via shared helper - Cashier auto-submits free topic/plate orders; topic coupon label no longer "预留"
This commit is contained in:
@@ -448,7 +448,7 @@
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Free VIP topic/plate_follow still need a free-order unlock POST; content free can reload.
|
// Content free: reload. Topic/plate free: auto free-order POST (VIP free / free-after-coupon).
|
||||||
if (preview.is_free && preview.pay_type !== 'topic' && preview.pay_type !== 'plate_follow') {
|
if (preview.is_free && preview.pay_type !== 'topic' && preview.pay_type !== 'plate_follow') {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return;
|
return;
|
||||||
@@ -458,6 +458,25 @@
|
|||||||
const priceType = (preview._price_type || preview.price_type || '');
|
const priceType = (preview._price_type || preview.price_type || '');
|
||||||
const apiKind = preview._api_kind
|
const apiKind = preview._api_kind
|
||||||
|| (preview.pay_type === 'topic' ? 'topic' : (preview.pay_type === 'plate_follow' ? 'plate_follow' : 'content'));
|
|| (preview.pay_type === 'topic' ? 'topic' : (preview.pay_type === 'plate_follow' ? 'plate_follow' : 'content'));
|
||||||
|
|
||||||
|
// Auto-submit free topic/plate orders so user doesn't click through empty cash UI.
|
||||||
|
if (preview.is_free && (apiKind === 'topic' || apiKind === 'plate_follow') && !isPoints) {
|
||||||
|
open(
|
||||||
|
'<h3 class="aksrt-dl-modal__title">' + tt('confirm_buy', '确认购买') + '</h3>' +
|
||||||
|
'<p class="aksrt-dl-modal__msg">' + tt('processing', '处理中…') + '</p>',
|
||||||
|
returnFocusEl
|
||||||
|
);
|
||||||
|
const freeBtn = document.createElement('button');
|
||||||
|
freeBtn.type = 'button';
|
||||||
|
freeBtn.style.display = 'none';
|
||||||
|
freeBtn.setAttribute('data-price-type', priceType || '');
|
||||||
|
document.body.appendChild(freeBtn);
|
||||||
|
const freeCode = String(preview.coupon_code || preview._coupon_code || '');
|
||||||
|
submitPurchase(preview.post_id, 'cash', 'balance', freeBtn, returnFocusEl, freeCode, apiKind);
|
||||||
|
try { freeBtn.remove(); } catch (e0) { /* ignore */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let amountLabel = escapeHtml(preview.amount_label || '');
|
let amountLabel = escapeHtml(preview.amount_label || '');
|
||||||
let baseLabel = escapeHtml(preview.base_label || '');
|
let baseLabel = escapeHtml(preview.base_label || '');
|
||||||
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
|
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ Note: zibll「商品管理」`zibpay_product_page` is **paid article sales**, no
|
|||||||
| 3 users.php submenus | done |
|
| 3 users.php submenus | done |
|
||||||
| 4 SPA strip | done (ops routes redirect to native) |
|
| 4 SPA strip | done (ops routes redirect to native) |
|
||||||
| 5+ behavior parity | done — native ops shell + plate follow-pay + topic pay matrix + multi-gateway topic cashier (`AKSRT_Topic_Pay` /pay/topic) + topic coupon burn (P1) + plate follow VIP1/2 + multi-gateway (`AKSRT_Plate_Follow_Pay` /pay/plate-follow) + cash creator revenue (content/topic/plate) + sales report/download logs + hidecontent + users commerce + order filters/clear-closed + post pay_type filter + bulk content-pay edit + income/rebate user filter |
|
| 5+ behavior parity | done — native ops shell + plate follow-pay + topic pay matrix + multi-gateway topic cashier (`AKSRT_Topic_Pay` /pay/topic) + topic coupon burn (P1) + plate follow VIP1/2 + multi-gateway (`AKSRT_Plate_Follow_Pay` /pay/plate-follow) + cash creator revenue (content/topic/plate) + sales report/download logs + hidecontent + users commerce + order filters/clear-closed + post pay_type filter + bulk content-pay edit + income/rebate user filter |
|
||||||
| 5 remaining | optional polish only (plate coupons, guest topic/plate purchase — out of scope); order note JSON preserve + shared coupon burn helper done |
|
| 5 remaining | optional polish only (guest topic/plate purchase — out of scope); plate follow coupons + free auto-order + note JSON preserve + shared coupon burn done |
|
||||||
|
|
||||||
## Dual-UI rule
|
## Dual-UI rule
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - product_type = plate_follow, product_id = plate_id
|
* - product_type = plate_follow, product_id = plate_id
|
||||||
* - order note.plate_follow_pay = 1
|
* - order note.plate_follow_pay = 1
|
||||||
* - paid → mark_plate_followed (idempotent)
|
* - paid → mark_plate_followed (idempotent)
|
||||||
* - no coupons this phase
|
* - optional coupons when _aksrt_follow_pay_coupon_s=1 (cash only)
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package AKSRT
|
||||||
*/
|
*/
|
||||||
@@ -62,7 +62,8 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
public function rest_preview(WP_REST_Request $request) {
|
public function rest_preview(WP_REST_Request $request) {
|
||||||
$plate_id = (int) $request['id'];
|
$plate_id = (int) $request['id'];
|
||||||
$uid = get_current_user_id();
|
$uid = get_current_user_id();
|
||||||
$preview = $this->preview($plate_id, $uid);
|
$coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? ''));
|
||||||
|
$preview = $this->preview($plate_id, $uid, $coupon_code);
|
||||||
if (is_wp_error($preview)) {
|
if (is_wp_error($preview)) {
|
||||||
return $preview;
|
return $preview;
|
||||||
}
|
}
|
||||||
@@ -89,9 +90,10 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
if ($payment_method === '' || $payment_method === 'cash') {
|
if ($payment_method === '' || $payment_method === 'cash') {
|
||||||
$payment_method = 'balance';
|
$payment_method = 'balance';
|
||||||
}
|
}
|
||||||
|
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
|
||||||
|
|
||||||
return $this->rest_wrap_result(
|
return $this->rest_wrap_result(
|
||||||
$this->create_cash_order($plate_id, $uid, $payment_method)
|
$this->create_cash_order($plate_id, $uid, $payment_method, $coupon_code)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
/**
|
/**
|
||||||
* @return array|WP_Error
|
* @return array|WP_Error
|
||||||
*/
|
*/
|
||||||
public function preview(int $plate_id, int $user_id = 0) {
|
public function preview(int $plate_id, int $user_id = 0, string $coupon_code = '') {
|
||||||
$assert = $this->assert_plate($plate_id);
|
$assert = $this->assert_plate($plate_id);
|
||||||
if (is_wp_error($assert)) {
|
if (is_wp_error($assert)) {
|
||||||
return $assert;
|
return $assert;
|
||||||
@@ -129,6 +131,7 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
/** @var WP_Post $post */
|
/** @var WP_Post $post */
|
||||||
$post = $assert['post'];
|
$post = $assert['post'];
|
||||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||||
|
$coupon_code = sanitize_text_field($coupon_code);
|
||||||
|
|
||||||
if ($uid <= 0) {
|
if ($uid <= 0) {
|
||||||
return [
|
return [
|
||||||
@@ -179,9 +182,9 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
|
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$amount = round((float) ($priced['amount'] ?? 0), 2);
|
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
|
||||||
$base = round((float) ($priced['base'] ?? $amount), 2);
|
$base = round((float) ($priced['base'] ?? $list_amount), 2);
|
||||||
$is_free = !empty($priced['is_free']) || $amount <= 0;
|
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
|
||||||
$has_followed = $this->user_has_followed($uid, $plate_id);
|
$has_followed = $this->user_has_followed($uid, $plate_id);
|
||||||
|
|
||||||
$balance = 0;
|
$balance = 0;
|
||||||
@@ -193,9 +196,39 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
? aksrt_list_cash_payment_methods(true)
|
? aksrt_list_cash_payment_methods(true)
|
||||||
: [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
|
: [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
|
||||||
|
|
||||||
|
$coupon_allowed = $this->is_coupon_allowed($plate_id);
|
||||||
|
$coupon_error = '';
|
||||||
|
$coupon_discount = 0.0;
|
||||||
|
$coupon_meta = null;
|
||||||
|
$applied_code = '';
|
||||||
|
$payable = $list_amount;
|
||||||
|
|
||||||
|
if (!$vip_free && $list_amount > 0) {
|
||||||
|
$discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, $coupon_code, true);
|
||||||
|
if (is_wp_error($discounted)) {
|
||||||
|
$coupon_error = $discounted->get_error_message();
|
||||||
|
$discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, '', true);
|
||||||
|
}
|
||||||
|
if (!is_wp_error($discounted)) {
|
||||||
|
$list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
|
||||||
|
$payable = round((float) ($discounted['payable'] ?? $list_amount), 2);
|
||||||
|
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
|
||||||
|
$coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null;
|
||||||
|
$applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? ''));
|
||||||
|
if ($coupon_error === '' && !empty($discounted['coupon_error'])) {
|
||||||
|
$coupon_error = (string) $discounted['coupon_error'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($vip_free) {
|
||||||
|
$payable = 0.0;
|
||||||
|
$list_amount = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_free = $vip_free || $payable <= 0;
|
||||||
$amount_label = $is_free
|
$amount_label = $is_free
|
||||||
? __('免费', 'aksrt')
|
? __('免费', 'aksrt')
|
||||||
: sprintf('¥%s', number_format($amount, 2, '.', ''));
|
: sprintf('¥%s', number_format($payable, 2, '.', ''));
|
||||||
|
$list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', ''));
|
||||||
$base_label = sprintf('¥%s', number_format($base, 2, '.', ''));
|
$base_label = sprintf('¥%s', number_format($base, 2, '.', ''));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -205,11 +238,11 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'pay_modo' => '0',
|
'pay_modo' => '0',
|
||||||
'pay_type' => 'plate_follow',
|
'pay_type' => 'plate_follow',
|
||||||
'pay_type_label' => __('付费关注', 'aksrt'),
|
'pay_type_label' => __('付费关注', 'aksrt'),
|
||||||
'amount' => $amount,
|
'amount' => $payable,
|
||||||
'base' => $base,
|
'base' => $base,
|
||||||
'list_amount' => $amount,
|
'list_amount' => $list_amount > 0 ? $list_amount : $base,
|
||||||
'list_amount_label' => $amount_label,
|
'list_amount_label' => $list_label,
|
||||||
'payable' => $amount,
|
'payable' => $payable,
|
||||||
'currency' => '¥',
|
'currency' => '¥',
|
||||||
'amount_label' => $amount_label,
|
'amount_label' => $amount_label,
|
||||||
'base_label' => $base_label,
|
'base_label' => $base_label,
|
||||||
@@ -228,13 +261,15 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'login_url' => wp_login_url(get_permalink($post)),
|
'login_url' => wp_login_url(get_permalink($post)),
|
||||||
'vip_url' => home_url('/'),
|
'vip_url' => home_url('/'),
|
||||||
'credits_url' => home_url('/'),
|
'credits_url' => home_url('/'),
|
||||||
'coupon_allowed' => false,
|
'coupon_allowed' => $coupon_allowed,
|
||||||
'coupon_desc' => '',
|
'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '',
|
||||||
'coupon_code' => '',
|
'coupon_code' => $applied_code,
|
||||||
'coupon' => null,
|
'coupon' => $coupon_meta,
|
||||||
'coupon_error' => '',
|
'coupon_error' => $coupon_error,
|
||||||
'coupon_discount' => 0,
|
'coupon_discount' => $coupon_discount,
|
||||||
'coupon_discount_label' => '',
|
'coupon_discount_label' => $coupon_discount > 0
|
||||||
|
? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', '')))
|
||||||
|
: '',
|
||||||
'rebate_discount' => 0,
|
'rebate_discount' => 0,
|
||||||
'rebate_discount_label' => '',
|
'rebate_discount_label' => '',
|
||||||
'points_deduction_allowed' => false,
|
'points_deduction_allowed' => false,
|
||||||
@@ -248,7 +283,7 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
/**
|
/**
|
||||||
* @return array|WP_Error
|
* @return array|WP_Error
|
||||||
*/
|
*/
|
||||||
public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance') {
|
public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') {
|
||||||
if ($user_id <= 0) {
|
if ($user_id <= 0) {
|
||||||
return new WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), ['status' => 401]);
|
return new WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), ['status' => 401]);
|
||||||
}
|
}
|
||||||
@@ -269,18 +304,52 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
|
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$amount = round((float) ($priced['amount'] ?? 0), 2);
|
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
|
||||||
$base = round((float) ($priced['base'] ?? $amount), 2);
|
$base = round((float) ($priced['base'] ?? $list_amount), 2);
|
||||||
$is_free = !empty($priced['is_free']) || $amount <= 0;
|
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
|
||||||
|
$coupon_code = sanitize_text_field($coupon_code);
|
||||||
|
|
||||||
if ($is_free) {
|
// VIP free — no coupon stack.
|
||||||
return $this->create_free_paid_order($plate_id, $user_id, $post, $priced);
|
if ($vip_free) {
|
||||||
|
return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [
|
||||||
|
'list_amount' => 0.0,
|
||||||
|
'payable' => 0.0,
|
||||||
|
'coupon_code' => '',
|
||||||
|
'coupon_discount' => 0.0,
|
||||||
|
'coupon' => [],
|
||||||
|
'message' => __('VIP 免费关注成功', 'aksrt'),
|
||||||
|
'paid_note' => __('VIP 免费关注版块。', 'aksrt'),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($amount <= 0) {
|
if ($list_amount <= 0) {
|
||||||
return new WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]);
|
return new WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $user_id, $coupon_code, false);
|
||||||
|
if (is_wp_error($discounted)) {
|
||||||
|
return $discounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
$list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
|
||||||
|
$amount = round((float) ($discounted['payable'] ?? $list_amount), 2);
|
||||||
|
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
|
||||||
|
$coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : [];
|
||||||
|
$coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code);
|
||||||
|
|
||||||
|
// Free after coupon.
|
||||||
|
if ($amount <= 0) {
|
||||||
|
return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [
|
||||||
|
'list_amount' => $list_amount,
|
||||||
|
'payable' => 0.0,
|
||||||
|
'coupon_code' => $coupon_code_norm,
|
||||||
|
'coupon_discount' => $coupon_discount,
|
||||||
|
'coupon' => $coupon_meta,
|
||||||
|
'message' => __('无需支付,已关注。', 'aksrt'),
|
||||||
|
'paid_note' => __('优惠后无需支付。', 'aksrt'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
|
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
|
||||||
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
|
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
|
||||||
}
|
}
|
||||||
@@ -291,7 +360,17 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pending = $this->find_pending_order($user_id, $plate_id);
|
$pending = $this->find_pending_order($user_id, $plate_id);
|
||||||
$can_resume = $pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001;
|
$can_resume = false;
|
||||||
|
if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
|
||||||
|
$pending_meta = $this->decode_order_note_meta($pending);
|
||||||
|
$pending_coupon = (string) ($pending_meta['coupon_code'] ?? '');
|
||||||
|
if (is_array($pending_meta['coupon'] ?? null)) {
|
||||||
|
$pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon);
|
||||||
|
}
|
||||||
|
if ($pending_coupon === $coupon_code_norm) {
|
||||||
|
$can_resume = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($can_resume) {
|
if ($can_resume) {
|
||||||
$order_number = (string) ($pending['order_number'] ?? '');
|
$order_number = (string) ($pending['order_number'] ?? '');
|
||||||
@@ -303,9 +382,12 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'pay_modo' => '0',
|
'pay_modo' => '0',
|
||||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||||
'base' => $base,
|
'base' => $base,
|
||||||
'list_amount' => $amount,
|
'list_amount' => $list_amount,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
'payable' => $amount,
|
'payable' => $amount,
|
||||||
|
'coupon_code' => $coupon_code_norm,
|
||||||
|
'coupon_discount' => $coupon_discount,
|
||||||
|
'coupon' => $coupon_meta,
|
||||||
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
@@ -433,6 +515,7 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->mark_followed($user_id, $plate_id);
|
$this->mark_followed($user_id, $plate_id);
|
||||||
|
$this->finalize_plate_order_side_effects($order_number, $paid_order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -515,9 +598,28 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Free VIP or free-after-discount paid order.
|
||||||
|
*
|
||||||
|
* @param array{
|
||||||
|
* list_amount?:float,
|
||||||
|
* payable?:float,
|
||||||
|
* coupon_code?:string,
|
||||||
|
* coupon_discount?:float,
|
||||||
|
* coupon?:array,
|
||||||
|
* message?:string,
|
||||||
|
* paid_note?:string
|
||||||
|
* } $opts
|
||||||
* @return array|WP_Error
|
* @return array|WP_Error
|
||||||
*/
|
*/
|
||||||
private function create_free_paid_order(int $plate_id, int $user_id, WP_Post $post, array $priced) {
|
private function create_free_paid_order(int $plate_id, int $user_id, WP_Post $post, array $priced, array $opts = []) {
|
||||||
|
$list_amount = round((float) ($opts['list_amount'] ?? 0), 2);
|
||||||
|
$payable = round((float) ($opts['payable'] ?? 0), 2);
|
||||||
|
$coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? ''));
|
||||||
|
$coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2);
|
||||||
|
$coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : [];
|
||||||
|
$message = (string) ($opts['message'] ?? __('VIP 免费关注成功', 'aksrt'));
|
||||||
|
$paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费关注版块。', 'aksrt'));
|
||||||
|
|
||||||
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
|
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
|
||||||
$this->mark_followed($user_id, $plate_id);
|
$this->mark_followed($user_id, $plate_id);
|
||||||
return [
|
return [
|
||||||
@@ -527,7 +629,7 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'amount' => 0,
|
'amount' => 0,
|
||||||
'pay_modo' => '0',
|
'pay_modo' => '0',
|
||||||
'free' => true,
|
'free' => true,
|
||||||
'message' => __('VIP 免费关注成功', 'aksrt'),
|
'message' => $message,
|
||||||
'reload' => true,
|
'reload' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -539,9 +641,12 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'pay_modo' => '0',
|
'pay_modo' => '0',
|
||||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||||
'base' => (float) ($priced['base'] ?? 0),
|
'base' => (float) ($priced['base'] ?? 0),
|
||||||
'list_amount' => 0,
|
'list_amount' => $list_amount,
|
||||||
'amount' => 0,
|
'amount' => $payable,
|
||||||
'payable' => 0,
|
'payable' => $payable,
|
||||||
|
'coupon_code' => $coupon_code,
|
||||||
|
'coupon_discount' => $coupon_discount,
|
||||||
|
'coupon' => $coupon_meta,
|
||||||
'free' => 1,
|
'free' => 1,
|
||||||
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
@@ -566,17 +671,20 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'order_number' => '',
|
'order_number' => '',
|
||||||
'amount' => 0,
|
'amount' => 0,
|
||||||
'free' => true,
|
'free' => true,
|
||||||
'message' => __('VIP 免费关注成功', 'aksrt'),
|
'message' => $message,
|
||||||
'reload' => true,
|
'reload' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
|
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
|
||||||
'payment_method' => 'free',
|
'payment_method' => 'free',
|
||||||
'payment_transaction_id' => 'free_' . $order_number,
|
'payment_transaction_id' => 'free_' . $order_number,
|
||||||
'note' => __('VIP 免费关注版块。', 'aksrt'),
|
'note' => $paid_note,
|
||||||
]);
|
]);
|
||||||
|
if (!$paid) {
|
||||||
|
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
|
||||||
|
}
|
||||||
$this->mark_followed($user_id, $plate_id);
|
$this->mark_followed($user_id, $plate_id);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -586,12 +694,105 @@ class AKSRT_Plate_Follow_Pay {
|
|||||||
'amount' => 0,
|
'amount' => 0,
|
||||||
'pay_modo' => '0',
|
'pay_modo' => '0',
|
||||||
'free' => true,
|
'free' => true,
|
||||||
'message' => __('VIP 免费关注成功', 'aksrt'),
|
'message' => $message,
|
||||||
'reload' => true,
|
'reload' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function is_coupon_allowed(int $plate_id): bool {
|
||||||
|
return (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIP list price → coupon → payable.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $priced
|
||||||
|
* @return array|WP_Error
|
||||||
|
*/
|
||||||
|
private function apply_plate_cash_discounts(array $priced, int $plate_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) {
|
||||||
|
$list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2);
|
||||||
|
$pay = $list_amount;
|
||||||
|
$coupon_discount = 0.0;
|
||||||
|
$coupon_meta = [];
|
||||||
|
$coupon_error = '';
|
||||||
|
$coupon_code = sanitize_text_field($coupon_code);
|
||||||
|
|
||||||
|
if ($coupon_code !== '') {
|
||||||
|
if (!$this->is_coupon_allowed($plate_id)) {
|
||||||
|
$err = new WP_Error('aksrt_coupon_not_allowed', __('当前版块不支持优惠码。', 'aksrt'), ['status' => 400]);
|
||||||
|
if (!$soft_coupon) {
|
||||||
|
return $err;
|
||||||
|
}
|
||||||
|
$coupon_error = $err->get_error_message();
|
||||||
|
$coupon_code = '';
|
||||||
|
} elseif (!class_exists('AKSRT_Coupon')) {
|
||||||
|
$err = new WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]);
|
||||||
|
if (!$soft_coupon) {
|
||||||
|
return $err;
|
||||||
|
}
|
||||||
|
$coupon_error = $err->get_error_message();
|
||||||
|
$coupon_code = '';
|
||||||
|
} else {
|
||||||
|
$result = AKSRT_Coupon::get_instance()->validate($coupon_code, $pay, [
|
||||||
|
'user_id' => max(0, $user_id),
|
||||||
|
'product_ids' => [$plate_id],
|
||||||
|
'context' => 'plate_follow',
|
||||||
|
]);
|
||||||
|
if (empty($result['valid'])) {
|
||||||
|
$err = new WP_Error(
|
||||||
|
'aksrt_coupon_error',
|
||||||
|
(string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')),
|
||||||
|
['status' => 400]
|
||||||
|
);
|
||||||
|
if (!$soft_coupon) {
|
||||||
|
return $err;
|
||||||
|
}
|
||||||
|
$coupon_error = $err->get_error_message();
|
||||||
|
$coupon_code = '';
|
||||||
|
} else {
|
||||||
|
$coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2);
|
||||||
|
if ($coupon_discount > $pay) {
|
||||||
|
$coupon_discount = $pay;
|
||||||
|
}
|
||||||
|
$pay = round(max(0.0, $pay - $coupon_discount), 2);
|
||||||
|
$c = is_array($result['coupon'] ?? null) ? $result['coupon'] : [];
|
||||||
|
$coupon_meta = [
|
||||||
|
'id' => (int) ($c['id'] ?? 0),
|
||||||
|
'code' => (string) ($c['code'] ?? $coupon_code),
|
||||||
|
'type' => (string) ($c['type'] ?? ''),
|
||||||
|
'value' => (float) ($c['value'] ?? 0),
|
||||||
|
'discount' => $coupon_discount,
|
||||||
|
];
|
||||||
|
$coupon_code = (string) $coupon_meta['code'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'list_amount' => $list_amount,
|
||||||
|
'coupon_discount' => $coupon_discount,
|
||||||
|
'coupon' => $coupon_meta,
|
||||||
|
'coupon_code' => $coupon_code,
|
||||||
|
'coupon_error' => $coupon_error,
|
||||||
|
'payable' => round(max(0.0, $pay), 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function finalize_plate_order_side_effects(string $order_number, array $order): void {
|
||||||
|
$meta = $this->decode_order_note_meta($order);
|
||||||
|
if (empty($meta['plate_follow_pay'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (($meta['pay_modo'] ?? '') === 'points') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$buyer_id = (int) ($order['user_id'] ?? 0);
|
||||||
|
if (function_exists('aksrt_burn_order_coupon')) {
|
||||||
|
aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function find_pending_order(int $user_id, int $plate_id): ?array {
|
private function find_pending_order(int $user_id, int $plate_id): ?array {
|
||||||
if ($user_id <= 0 || $plate_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
|
if ($user_id <= 0 || $plate_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -190,6 +190,13 @@ class AKSRT_Plate_Meta_Box {
|
|||||||
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_plate_follow_pay_vip2_price" id="aksrt_plate_follow_pay_vip2_price" value="<?php echo esc_attr((string) ($follow_pay['vip2_price'] ?? '')); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP2 价;0=VIP2 免费', 'aksrt'); ?>">
|
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_plate_follow_pay_vip2_price" id="aksrt_plate_follow_pay_vip2_price" value="<?php echo esc_attr((string) ($follow_pay['vip2_price'] ?? '')); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP2 价;0=VIP2 免费', 'aksrt'); ?>">
|
||||||
<span class="description"><?php esc_html_e('现金模式用元;积分模式填积分数。未填 VIP1 时仍可读旧「VIP 价格」字段。', 'aksrt'); ?></span>
|
<span class="description"><?php esc_html_e('现金模式用元;积分模式填积分数。未填 VIP1 时仍可读旧「VIP 价格」字段。', 'aksrt'); ?></span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="aksrt_plate_follow_pay_coupon_s" value="1" <?php checked(!empty($follow_pay['coupon_s'])); ?>>
|
||||||
|
<?php esc_html_e('允许优惠码(仅现金关注)', 'aksrt'); ?>
|
||||||
|
</label>
|
||||||
|
<span class="description" style="display:block;"><?php esc_html_e('开启后收银台可输入优惠码;积分关注不吃券。', 'aksrt'); ?></span>
|
||||||
|
</p>
|
||||||
<hr>
|
<hr>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
@@ -398,6 +405,7 @@ class AKSRT_Plate_Meta_Box {
|
|||||||
delete_post_meta($post_id, '_aksrt_follow_pay_vip_price');
|
delete_post_meta($post_id, '_aksrt_follow_pay_vip_price');
|
||||||
delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price');
|
delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price');
|
||||||
delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price');
|
delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price');
|
||||||
|
delete_post_meta($post_id, '_aksrt_follow_pay_coupon_s');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +413,7 @@ class AKSRT_Plate_Meta_Box {
|
|||||||
update_post_meta($post_id, '_aksrt_follow_pay_modo', $modo);
|
update_post_meta($post_id, '_aksrt_follow_pay_modo', $modo);
|
||||||
update_post_meta($post_id, '_aksrt_follow_pay_price', $price);
|
update_post_meta($post_id, '_aksrt_follow_pay_price', $price);
|
||||||
update_post_meta($post_id, '_aksrt_follow_pay_points', $points);
|
update_post_meta($post_id, '_aksrt_follow_pay_points', $points);
|
||||||
|
update_post_meta($post_id, '_aksrt_follow_pay_coupon_s', !empty($_POST['aksrt_plate_follow_pay_coupon_s']) ? 1 : 0);
|
||||||
|
|
||||||
if (isset($_POST['aksrt_plate_follow_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']) !== '') {
|
if (isset($_POST['aksrt_plate_follow_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']) !== '') {
|
||||||
$vip1 = max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']));
|
$vip1 = max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']));
|
||||||
@@ -424,7 +433,7 @@ class AKSRT_Plate_Meta_Box {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{enabled:bool,modo:string,price:float,points:int,vip1_price:string,vip2_price:string}
|
* @return array{enabled:bool,modo:string,price:float,points:int,vip1_price:string,vip2_price:string,coupon_s:bool}
|
||||||
*/
|
*/
|
||||||
private function get_follow_pay_meta(int $plate_id): array {
|
private function get_follow_pay_meta(int $plate_id): array {
|
||||||
$enabled = (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1;
|
$enabled = (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1;
|
||||||
@@ -451,6 +460,7 @@ class AKSRT_Plate_Meta_Box {
|
|||||||
'points' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true),
|
'points' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true),
|
||||||
'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '',
|
'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '',
|
||||||
'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '',
|
'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '',
|
||||||
|
'coupon_s' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,9 +223,9 @@ class AKSRT_Topic_Meta_Box {
|
|||||||
<p class="aksrt-topic-meta-pay-fields">
|
<p class="aksrt-topic-meta-pay-fields">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="aksrt_topic_coupon_s" value="1" <?php checked($coupon_s); ?>>
|
<input type="checkbox" name="aksrt_topic_coupon_s" value="1" <?php checked($coupon_s); ?>>
|
||||||
<?php esc_html_e('允许优惠码(预留)', 'aksrt'); ?>
|
<?php esc_html_e('允许优惠码', 'aksrt'); ?>
|
||||||
</label>
|
</label>
|
||||||
<span class="description" style="display:block;"><?php esc_html_e('标记本帖允许优惠码;收银台多网关接入前仅存 meta。', 'aksrt'); ?></span>
|
<span class="description" style="display:block;"><?php esc_html_e('开启后现金收银台可输入优惠码;积分解锁不吃券。', 'aksrt'); ?></span>
|
||||||
</p>
|
</p>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ function aksrt_register_forum_topic_meta() {
|
|||||||
'_aksrt_follow_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy read fallback → vip1
|
'_aksrt_follow_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy read fallback → vip1
|
||||||
'_aksrt_follow_pay_vip1_price' => ['type' => 'number', 'default' => 0],
|
'_aksrt_follow_pay_vip1_price' => ['type' => 'number', 'default' => 0],
|
||||||
'_aksrt_follow_pay_vip2_price' => ['type' => 'number', 'default' => 0],
|
'_aksrt_follow_pay_vip2_price' => ['type' => 'number', 'default' => 0],
|
||||||
|
'_aksrt_follow_pay_coupon_s' => ['type' => 'integer', 'default' => 0],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($plate_meta as $key => $args) {
|
foreach ($plate_meta as $key => $args) {
|
||||||
|
|||||||
Reference in New Issue
Block a user