feat(admin): order shipping/after-sale create and plate list columns

Allow creating shipping records and after-sale tickets from the native
order detail page, and show plate moderator/stats columns on the forum
plate list table. Source only; no SPA build artifacts.
This commit is contained in:
2026-07-15 02:34:31 +08:00
parent e2f7fcad89
commit 8eb5c273ef
4 changed files with 210 additions and 2 deletions
+5
View File
@@ -315,6 +315,11 @@ if (is_admin() && class_exists('AKSRT_Topic_Admin_List')) {
if (is_admin() && class_exists('AKSRT_Topic_Meta_Box')) {
AKSRT_Topic_Meta_Box::get_instance()->init();
}
// 论坛版块列表列
if (is_admin() && class_exists('AKSRT_Plate_Admin_List')) {
AKSRT_Plate_Admin_List::get_instance()->init();
}
+1
View File
@@ -325,6 +325,7 @@ require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-product-variants-meta-bo
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-plate-meta-box.php';
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-topic-admin-list.php';
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-topic-meta-box.php';
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-plate-admin-list.php';
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php';
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-2.php';
require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-3.php';
@@ -35,7 +35,9 @@ class AKSRT_Admin_Pay_Menus {
add_action('admin_post_aksrt_pay_disable_charge_card', [$this, 'handle_disable_charge_card']);
add_action('admin_post_aksrt_pay_batch_charge_cards', [$this, 'handle_batch_charge_cards']);
add_action('admin_post_aksrt_pay_update_shipping', [$this, 'handle_update_shipping']);
add_action('admin_post_aksrt_pay_create_shipping', [$this, 'handle_create_shipping']);
add_action('admin_post_aksrt_pay_update_after_sale', [$this, 'handle_update_after_sale']);
add_action('admin_post_aksrt_pay_create_after_sale', [$this, 'handle_create_after_sale']);
add_action('admin_post_aksrt_pay_create_coupon', [$this, 'handle_create_coupon']);
add_action('admin_post_aksrt_pay_disable_coupon', [$this, 'handle_disable_coupon']);
add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']);
@@ -202,8 +204,12 @@ class AKSRT_Admin_Pay_Menus {
'card_batch_failed' => __('卡密批量生成失败。', 'aksrt'),
'shipping_updated' => __('物流状态已更新。', 'aksrt'),
'shipping_failed' => __('物流状态更新失败。', 'aksrt'),
'shipping_created' => __('发货记录已创建。', 'aksrt'),
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
'after_sale_updated' => __('售后状态已更新。', 'aksrt'),
'after_sale_failed' => __('售后状态更新失败。', 'aksrt'),
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
'coupon_created' => __('优惠码已创建。', 'aksrt'),
'coupon_create_failed'=> __('优惠码创建失败(可能重复)。', 'aksrt'),
'coupon_disabled' => __('优惠码已禁用。', 'aksrt'),
@@ -463,8 +469,18 @@ class AKSRT_Admin_Pay_Menus {
echo '</form>';
echo '<h2 style="margin-top:24px;">' . esc_html__('物流记录', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_shipping" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
wp_nonce_field('aksrt_pay_create_shipping_' . $order_number);
echo '<p><label>' . esc_html__('承运商', 'aksrt') . '<br><input type="text" name="carrier" required placeholder="SF/YTO/..." /></label></p>';
echo '<p><label>' . esc_html__('运单号', 'aksrt') . '<br><input type="text" name="tracking_no" required /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" /></label></p>';
submit_button(__('创建发货', 'aksrt'), 'secondary', '', false);
echo ' <a class="button" href="' . esc_url($this->page_url('aksrt_shipping')) . '">' . esc_html__('物流管理', 'aksrt') . '</a>';
echo '</form>';
if (!$shipping_items) {
echo '<p>' . esc_html__('暂无物流记录。', 'aksrt') . ' <a href="' . esc_url($this->page_url('aksrt_shipping')) . '">' . esc_html__('去物流管理', 'aksrt') . '</a></p>';
echo '<p>' . esc_html__('暂无物流记录。', 'aksrt') . '</p>';
} else {
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>' . esc_html__('承运商', 'aksrt') . '</th><th>' . esc_html__('运单号', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th></tr></thead><tbody>';
foreach ($shipping_items as $item) {
@@ -480,8 +496,29 @@ class AKSRT_Admin_Pay_Menus {
}
echo '<h2 style="margin-top:24px;">' . esc_html__('售后工单', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_after_sale" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $user_id) . '" />';
wp_nonce_field('aksrt_pay_create_after_sale_' . $order_number);
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
foreach ([
'refund' => __('仅退款', 'aksrt'),
'refund_return' => __('退货退款', 'aksrt'),
'exchange' => __('换货', 'aksrt'),
'repair' => __('维修', 'aksrt'),
] as $k => $label) {
printf('<option value="%s">%s</option>', esc_attr($k), esc_html($label));
}
echo '</select></label></p>';
echo '<p><label>' . esc_html__('金额', 'aksrt') . '<br><input type="number" step="0.01" min="0" name="amount" value="' . esc_attr((string) ($order['amount'] ?? 0)) . '" /></label></p>';
echo '<p><label>' . esc_html__('原因', 'aksrt') . '<br><input type="text" name="reason" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" /></label></p>';
submit_button(__('创建售后', 'aksrt'), 'secondary', '', false);
echo ' <a class="button" href="' . esc_url($this->page_url('aksrt_after_sale', ['s' => $order_number])) . '">' . esc_html__('售后管理', 'aksrt') . '</a>';
echo '</form>';
if (!$after_sale) {
echo '<p>' . esc_html__('暂无售后工单。', 'aksrt') . ' <a href="' . esc_url($this->page_url('aksrt_after_sale')) . '">' . esc_html__('去售后管理', 'aksrt') . '</a></p>';
echo '<p>' . esc_html__('暂无售后工单。', 'aksrt') . '</p>';
} else {
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th></tr></thead><tbody>';
foreach ($after_sale as $item) {
@@ -1019,6 +1056,64 @@ class AKSRT_Admin_Pay_Menus {
exit;
}
public function handle_create_shipping(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('aksrt_pay_create_shipping_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('AKSRT_Shipping')) {
$id = AKSRT_Shipping::get_instance()->create([
'order_number' => $order_number,
'carrier' => sanitize_text_field((string) wp_unslash($_POST['carrier'] ?? '')),
'tracking_no' => sanitize_text_field((string) wp_unslash($_POST['tracking_no'] ?? '')),
'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')),
'operator_id' => get_current_user_id(),
]);
$ok = $id > 0;
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $order_number,
'aksrt_pay_msg' => $ok ? 'shipping_created' : 'shipping_create_failed',
]));
exit;
}
public function handle_create_after_sale(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
check_admin_referer('aksrt_pay_create_after_sale_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('AKSRT_After_Sale')) {
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
if ($user_id <= 0 && class_exists('AKSRT_Order_Repository')) {
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
$user_id = (int) ($order['user_id'] ?? 0);
}
$id = AKSRT_After_Sale::get_instance()->create([
'order_number' => $order_number,
'user_id' => $user_id,
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'refund')),
'reason' => sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? '')),
'amount' => (float) ($_POST['amount'] ?? 0),
'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')),
]);
$ok = $id > 0;
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $order_number,
'aksrt_pay_msg' => $ok ? 'after_sale_created' : 'after_sale_create_failed',
]));
exit;
}
/* ------------------------------------------------------------------
* After-sale
* ----------------------------------------------------------------*/
@@ -0,0 +1,107 @@
<?php
/**
* Native list-table columns for aksrt_forum_plate.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Plate_Admin_List {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_filter('manage_edit-aksrt_forum_plate_columns', [$this, 'columns']);
add_action('manage_aksrt_forum_plate_posts_custom_column', [$this, 'render_column'], 10, 2);
add_filter('manage_edit-aksrt_forum_plate_sortable_columns', [$this, 'sortable_columns']);
add_action('pre_get_posts', [$this, 'apply_orderby']);
}
public function columns(array $columns): array {
$new = [];
foreach ($columns as $key => $label) {
$new[$key] = $label;
if ($key === 'title') {
$new['aksrt_plate_mods'] = __('版主数', 'aksrt');
$new['aksrt_plate_posts'] = __('帖子数', 'aksrt');
$new['aksrt_plate_reply'] = __('回复数', 'aksrt');
$new['aksrt_plate_views'] = __('浏览', 'aksrt');
$new['aksrt_plate_follow'] = __('关注', 'aksrt');
$new['aksrt_plate_pending_apps'] = __('待审申请', 'aksrt');
}
}
return $new;
}
public function sortable_columns(array $columns): array {
$columns['aksrt_plate_posts'] = 'aksrt_posts_count';
$columns['aksrt_plate_views'] = 'aksrt_plate_views';
return $columns;
}
public function render_column(string $column, int $post_id): void {
switch ($column) {
case 'aksrt_plate_mods':
$mods = get_post_meta($post_id, '_aksrt_moderators', true);
$count = is_array($mods) ? count(array_filter(array_map('intval', $mods))) : 0;
echo esc_html((string) $count);
break;
case 'aksrt_plate_posts':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_posts_count', true));
break;
case 'aksrt_plate_reply':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_reply_count', true));
break;
case 'aksrt_plate_views':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_plate_views', true));
break;
case 'aksrt_plate_follow':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_follow_count', true));
break;
case 'aksrt_plate_pending_apps':
$apps = get_post_meta($post_id, '_aksrt_moderator_applications', true);
$pending = 0;
if (is_array($apps)) {
foreach ($apps as $app) {
if (is_array($app) && (($app['status'] ?? '') === 'pending')) {
$pending++;
}
}
}
if ($pending > 0) {
echo '<strong style="color:#b32d2e;">' . esc_html((string) $pending) . '</strong>';
} else {
echo '0';
}
break;
}
}
public function apply_orderby(WP_Query $query): void {
if (!is_admin() || !$query->is_main_query()) {
return;
}
if ($query->get('post_type') !== 'aksrt_forum_plate') {
return;
}
$orderby = $query->get('orderby');
if ($orderby === 'aksrt_posts_count') {
$query->set('meta_key', '_aksrt_posts_count');
$query->set('orderby', 'meta_value_num');
} elseif ($orderby === 'aksrt_plate_views') {
$query->set('meta_key', '_aksrt_plate_views');
$query->set('orderby', 'meta_value_num');
}
}
}