Merge pull request 'feat(forum): plate follow cash coupons + free auto-order' (#164) from worktree-forum-pay-plate-coupon into main

Reviewed-on: CYL/wordpress-theme-aksrt#164
This commit is contained in:
2026-07-15 13:25:52 +08:00
6 changed files with 273 additions and 42 deletions
+20 -1
View File
@@ -448,7 +448,7 @@
window.location.reload();
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') {
window.location.reload();
return;
@@ -458,6 +458,25 @@
const priceType = (preview._price_type || preview.price_type || '');
const apiKind = preview._api_kind
|| (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 baseLabel = escapeHtml(preview.base_label || '');
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
+1 -1
View File
@@ -100,7 +100,7 @@ Note: zibll「商品管理」`zibpay_product_page` is **paid article sales**, no
| 3 users.php submenus | done |
| 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 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
+238 -37
View File
@@ -7,7 +7,7 @@
* - product_type = plate_follow, product_id = plate_id
* - order note.plate_follow_pay = 1
* - paid → mark_plate_followed (idempotent)
* - no coupons this phase
* - optional coupons when _aksrt_follow_pay_coupon_s=1 (cash only)
*
* @package AKSRT
*/
@@ -62,7 +62,8 @@ class AKSRT_Plate_Follow_Pay {
public function rest_preview(WP_REST_Request $request) {
$plate_id = (int) $request['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)) {
return $preview;
}
@@ -89,9 +90,10 @@ class AKSRT_Plate_Follow_Pay {
if ($payment_method === '' || $payment_method === 'cash') {
$payment_method = 'balance';
}
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
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
*/
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);
if (is_wp_error($assert)) {
return $assert;
@@ -129,6 +131,7 @@ class AKSRT_Plate_Follow_Pay {
/** @var WP_Post $post */
$post = $assert['post'];
$uid = $user_id > 0 ? $user_id : get_current_user_id();
$coupon_code = sanitize_text_field($coupon_code);
if ($uid <= 0) {
return [
@@ -179,9 +182,9 @@ class AKSRT_Plate_Follow_Pay {
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
}
$amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $amount), 2);
$is_free = !empty($priced['is_free']) || $amount <= 0;
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $list_amount), 2);
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
$has_followed = $this->user_has_followed($uid, $plate_id);
$balance = 0;
@@ -193,9 +196,39 @@ class AKSRT_Plate_Follow_Pay {
? aksrt_list_cash_payment_methods(true)
: [['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
? __('免费', '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, '.', ''));
return [
@@ -205,11 +238,11 @@ class AKSRT_Plate_Follow_Pay {
'pay_modo' => '0',
'pay_type' => 'plate_follow',
'pay_type_label' => __('付费关注', 'aksrt'),
'amount' => $amount,
'amount' => $payable,
'base' => $base,
'list_amount' => $amount,
'list_amount_label' => $amount_label,
'payable' => $amount,
'list_amount' => $list_amount > 0 ? $list_amount : $base,
'list_amount_label' => $list_label,
'payable' => $payable,
'currency' => '¥',
'amount_label' => $amount_label,
'base_label' => $base_label,
@@ -228,13 +261,15 @@ class AKSRT_Plate_Follow_Pay {
'login_url' => wp_login_url(get_permalink($post)),
'vip_url' => home_url('/'),
'credits_url' => home_url('/'),
'coupon_allowed' => false,
'coupon_desc' => '',
'coupon_code' => '',
'coupon' => null,
'coupon_error' => '',
'coupon_discount' => 0,
'coupon_discount_label' => '',
'coupon_allowed' => $coupon_allowed,
'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '',
'coupon_code' => $applied_code,
'coupon' => $coupon_meta,
'coupon_error' => $coupon_error,
'coupon_discount' => $coupon_discount,
'coupon_discount_label' => $coupon_discount > 0
? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', '')))
: '',
'rebate_discount' => 0,
'rebate_discount_label' => '',
'points_deduction_allowed' => false,
@@ -248,7 +283,7 @@ class AKSRT_Plate_Follow_Pay {
/**
* @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) {
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]);
}
$amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $amount), 2);
$is_free = !empty($priced['is_free']) || $amount <= 0;
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $list_amount), 2);
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
$coupon_code = sanitize_text_field($coupon_code);
if ($is_free) {
return $this->create_free_paid_order($plate_id, $user_id, $post, $priced);
// VIP free — no coupon stack.
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]);
}
$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')) {
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);
$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) {
$order_number = (string) ($pending['order_number'] ?? '');
@@ -303,9 +382,12 @@ class AKSRT_Plate_Follow_Pay {
'pay_modo' => '0',
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => $base,
'list_amount' => $amount,
'list_amount' => $list_amount,
'amount' => $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'] ?? ''),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -433,6 +515,7 @@ class AKSRT_Plate_Follow_Pay {
}
$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
*/
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')) {
$this->mark_followed($user_id, $plate_id);
return [
@@ -527,7 +629,7 @@ class AKSRT_Plate_Follow_Pay {
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => __('VIP 免费关注成功', 'aksrt'),
'message' => $message,
'reload' => true,
],
];
@@ -539,9 +641,12 @@ class AKSRT_Plate_Follow_Pay {
'pay_modo' => '0',
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? 0),
'list_amount' => 0,
'amount' => 0,
'payable' => 0,
'list_amount' => $list_amount,
'amount' => $payable,
'payable' => $payable,
'coupon_code' => $coupon_code,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'free' => 1,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -566,17 +671,20 @@ class AKSRT_Plate_Follow_Pay {
'order_number' => '',
'amount' => 0,
'free' => true,
'message' => __('VIP 免费关注成功', 'aksrt'),
'message' => $message,
'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_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);
return [
@@ -586,12 +694,105 @@ class AKSRT_Plate_Follow_Pay {
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => __('VIP 免费关注成功', 'aksrt'),
'message' => $message,
'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 {
if ($user_id <= 0 || $plate_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
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'); ?>">
<span class="description"><?php esc_html_e('现金模式用元;积分模式填积分数。未填 VIP1 时仍可读旧「VIP 价格」字段。', 'aksrt'); ?></span>
</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>
<?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_vip1_price');
delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price');
delete_post_meta($post_id, '_aksrt_follow_pay_coupon_s');
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_price', $price);
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']) !== '') {
$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 {
$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),
'vip1_price' => $vip1_set ? (string) (float) $vip1_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">
<label>
<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>
<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>
<?php
}
+1
View File
@@ -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_vip1_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) {