feat: remove shop feature (Phase 2)

Delete all shop domain classes, controllers, schema builders, templates,
JS/CSS, admin SPA pages, and shop-only payment classes (Shipping,
AfterSale, Discount). Remove shop-only DB table definitions (product_variant,
after_sale, order_shipping, card_key) from migrator and add their DROP
to uninstall.php. Clean up shared files:

- Remove aksrt_product CPT + taxonomies from Setup.php
- Remove shop rewrite rules/query vars/template redirect from SetupPart2.php
- Remove shop widget areas from SetupPart1.php
- Remove shop enqueues from ScriptsPart1b.php
- Remove sepakerator-aksrt-shop from SetupPart2.php
- Remove get_shop_settings facade (delete ForumShop.php, clean DomainGroups.php)
- Remove ORDER_PRODUCT_SHOP constant from DomainModel.php
- Remove Shop::class fallback branches from UserOrders.php, OrderCenterRest.php
- Remove shipping/after-sale branches from OrderRepositoryAdmin.php, PayMenus.php, PayOrders.php
- Remove shop product widgets from widgets-register.php
- Remove cart page from template-registry.php, routing.php, ScriptsPart2.php
- Remove cart UI from sidebar-nav.php, profile-orders.php, sidebar-orders.php
- Clean up order-center.js (remove rebuy/cartUrl/receipt/after-sale handlers)
- Remove aksrt_get_cart_page_url/product_page/product_edit from transactions.php
- Clean up aksrt_product_cat references in Coupon.php, PayCoupons.php

Shared payment/order/credits/content-pay stack fully retained.
This commit is contained in:
2026-08-02 21:25:52 +08:00
parent 2bbd7c01a3
commit 1d2505356e
103 changed files with 46 additions and 21267 deletions
-357
View File
@@ -1,357 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
class PayAfterSale {
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_after_sale';
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_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_after_sale_action', [$this, 'handle_after_sale_action']);
}
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'])) : '';
$redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order';
$nonce_action = $redirect_page === 'after_sale' ? 'aksrt_pay_create_after_sale' : ('aksrt_pay_create_after_sale_' . $order_number);
check_admin_referer($nonce_action);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\AfterSale')) {
$id = \AKSRT\Payment\AfterSale::get_instance()->create([
'order_number' => $order_number,
'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 = !is_wp_error($id) && $id > 0;
}
$msg = $ok ? 'after_sale_created' : 'after_sale_create_failed';
if ($redirect_page === 'after_sale') {
wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $msg, 's' => $order_number]));
exit;
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $order_number,
'aksrt_pay_msg' => $msg,
]));
exit;
}
public function render_after_sale_page(): void {
$this->ensure_cap();
$this->render_header(__('售后管理', 'aksrt'));
if (!class_exists('\AKSRT\Payment\AfterSale')) {
echo '<div class="notice notice-error"><p>' . esc_html__('售后模块未初始化。', 'aksrt') . '</p></div>';
$this->render_footer();
return;
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
if ($search === '' && isset($_GET['order_number'])) {
$search = sanitize_text_field((string) wp_unslash($_GET['order_number']));
}
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$svc = \AKSRT\Payment\AfterSale::get_instance();
$stats = $svc->get_stats();
$status_options = $this->after_sale_status_options(true);
$type_options = $this->after_sale_type_options(true);
echo '<p>' . esc_html(sprintf(
/* translators: 1: total 2: pending */
__('售后工单共 %1$d · 待处理 %2$d', 'aksrt'),
(int) ($stats['total'] ?? 0),
(int) ($stats['pending'] ?? 0)
)) . '</p>';
echo '<h2>' . esc_html__('新建售后', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 20px;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="redirect_page" value="after_sale" />';
wp_nonce_field('aksrt_pay_create_after_sale');
echo '<p><label>' . esc_html__('订单号', 'aksrt') . '<br><input type="text" name="order_number" value="' . esc_attr($search) . '" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
foreach ($this->after_sale_type_options(false) 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="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'), 'primary', '', false);
echo '</form>';
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_after_sale" />';
echo '<select name="status">';
foreach ($status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
echo '<select name="type">';
foreach ($type_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($type, $key, false), esc_html($label));
}
echo '</select> ';
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号', 'aksrt') . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$list_args = [
'status' => $status,
'type' => $type,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
];
if ($search !== '') {
$list_args['order_number'] = $search;
}
$result = $svc->get_list($list_args);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
$row_status_options = $this->after_sale_status_options(false);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无售后工单', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$st = (string) ($row['status'] ?? '');
$order_number = (string) ($row['order_number'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '"><code>' . esc_html($order_number) . '</code></a></td>';
echo '<td>' . esc_html((string) ($row['type_label'] ?? $type_options[$row['type'] ?? ''] ?? $row['type'] ?? '')) . '</td>';
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
echo '<td>' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
if ($st === 'pending') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_after_sale_action" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="as_action" value="approve" />';
wp_nonce_field('aksrt_pay_after_sale_action_' . $id);
submit_button(__('同意', 'aksrt'), 'small', '', false);
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_after_sale_action" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="as_action" value="reject" />';
wp_nonce_field('aksrt_pay_after_sale_action_' . $id);
echo '<button type="submit" class="button button-small" onclick="return confirm(\'' . esc_js(__('确认拒绝该售后?', 'aksrt')) . '\');">' . esc_html__('拒绝', 'aksrt') . '</button>';
echo '</form>';
}
if ($st === 'processing') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_after_sale_action" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="as_action" value="confirm_return" />';
wp_nonce_field('aksrt_pay_after_sale_action_' . $id);
submit_button(__('确认收货', 'aksrt'), 'small', '', false);
echo '</form>';
}
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;flex-wrap:wrap;">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_after_sale" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_update_after_sale_' . $id);
echo '<select name="new_status">';
foreach ($row_status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($label));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form>';
echo '</td></tr>';
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
$this->render_footer();
}
public function handle_update_after_sale(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0;
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_pay_update_after_sale_' . $id);
$ok = false;
if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\AfterSale')) {
$ok = \AKSRT\Payment\AfterSale::get_instance()->update_status($id, $new_status, get_current_user_id(), $note);
}
wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $ok ? 'after_sale_updated' : 'after_sale_failed']));
exit;
}
public function handle_after_sale_action(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0;
$as_action = isset($_POST['as_action']) ? sanitize_key((string) wp_unslash($_POST['as_action'])) : '';
check_admin_referer('aksrt_pay_after_sale_action_' . $id);
$ok = false;
if ($id > 0 && class_exists('\AKSRT\Payment\AfterSale')) {
$svc = \AKSRT\Payment\AfterSale::get_instance();
$operator_id = get_current_user_id();
if ($as_action === 'approve') {
$ok = $svc->merchant_approve($id, $operator_id);
} elseif ($as_action === 'reject') {
$ok = $svc->merchant_reject($id, $operator_id, __('商家拒绝', 'aksrt'));
} elseif ($as_action === 'confirm_return') {
$ok = $svc->merchant_confirm_return($id, $operator_id);
}
}
wp_safe_redirect($this->page_url('aksrt_after_sale', [
'aksrt_pay_msg' => $ok ? 'after_sale_action_ok' : 'after_sale_action_failed',
]));
exit;
}
private function after_sale_status_options(bool $include_all = false): array {
$options = [];
if ($include_all) {
$options[''] = __('全部状态', 'aksrt');
}
if (class_exists('\AKSRT\Payment\AfterSale')) {
foreach (\AKSRT\Payment\AfterSale::get_status_options() as $item) {
$value = (string) ($item['value'] ?? '');
if ($value === '') {
continue;
}
$options[$value] = (string) ($item['label'] ?? $value);
}
}
return $options;
}
/**
* @return array<string, string>
*/
public function after_sale_type_options_public(bool $include_all = false): array {
return $this->after_sale_type_options($include_all);
}
private function after_sale_type_options(bool $include_all = false): array {
$options = [];
if ($include_all) {
$options[''] = __('全部类型', 'aksrt');
}
// Keep operational create options broader than public get_type_options().
$defaults = [
'refund' => __('仅退款', 'aksrt'),
'refund_return' => __('退货退款', 'aksrt'),
'exchange' => __('换货', 'aksrt'),
'repair' => __('维修', 'aksrt'),
];
if (class_exists('\AKSRT\Payment\AfterSale')) {
foreach (\AKSRT\Payment\AfterSale::get_type_options() as $item) {
$value = (string) ($item['value'] ?? '');
if ($value === '') {
continue;
}
$defaults[$value] = (string) ($item['label'] ?? $value);
}
}
return array_merge($options, $defaults);
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
}
private function render_header(string $title): void {
echo '<div class="wrap">';
echo '<h1>' . esc_html($title) . '</h1>';
$this->render_flash();
}
private function render_footer(): void {
echo '</div>';
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$map = [
'after_sale_updated' => __('售后状态已更新。', 'aksrt'),
'after_sale_failed' => __('售后状态更新失败。', 'aksrt'),
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
'after_sale_action_ok' => __('售后操作已完成。', 'aksrt'),
'after_sale_action_failed' => __('售后操作失败。', 'aksrt'),
];
if (!isset($map[$code])) {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
}
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+1 -1
View File
@@ -372,7 +372,7 @@ class PayCoupons {
'ids' => $ids,
];
if ($type === 'category') {
$scope['taxonomies'] = ['aksrt_product_cat', 'category'];
$scope['taxonomies'] = ['category'];
}
return $scope;
}
+1 -45
View File
@@ -74,28 +74,6 @@ class PayMenus {
wp_die(esc_html__('下载记录模块未初始化。', 'aksrt'));
},
],
'shipping' => [
'slug' => 'aksrt_shipping',
'title' => __('发货及物流', 'aksrt'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) {
PayShipping::get_instance()->render_shipping_page();
return;
}
wp_die(esc_html__('物流模块未初始化。', 'aksrt'));
},
],
'after_sale' => [
'slug' => 'aksrt_after_sale',
'title' => __('售后管理', 'aksrt'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) {
PayAfterSale::get_instance()->render_after_sale_page();
return;
}
wp_die(esc_html__('售后模块未初始化。', 'aksrt'));
},
],
'coupons' => [
'slug' => 'aksrt_coupons',
'title' => __('优惠码', 'aksrt'),
@@ -203,23 +181,6 @@ class PayMenus {
echo '</div>';
}
}
if ($page !== 'aksrt_after_sale' && class_exists('\AKSRT\Payment\AfterSale')) {
$as_stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats();
$pending_as = (int) ($as_stats['pending'] ?? 0);
if ($pending_as > 0) {
$url = admin_url('admin.php?page=aksrt_after_sale&status=pending');
echo '<div class="notice notice-info is-dismissible">';
echo '<h3>' . esc_html__('售后工单待处理', 'aksrt') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d: pending after-sale count */
__('您有 %d 个售后工单待处理', 'aksrt'),
$pending_as
)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('去处理', 'aksrt') . '</a></p>';
echo '</div>';
}
}
}
private function ensure_cap(): void {
@@ -255,17 +216,12 @@ class PayMenus {
? \AKSRT\Payment\ChargeCard::get_instance()->get_stats()
: [];
echo '<p class="description">' . esc_html__('运营数据入口(对齐 zibll 商城中心)。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '</p>';
echo '<p class="description">' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '</p>';
echo '<div class="aksrt-pay-stats" style="display:flex;flex-wrap:wrap;gap:16px;margin:20px 0;">';
$pending_ship = 0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$pending_ship = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1])['total'] ?? 0);
}
$this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders'));
$this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending']));
$this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid']));
$this->stat_card(__('待发货', 'aksrt'), (string) $pending_ship, $this->page_url('aksrt_shipping', ['view' => 'pending']));
$this->stat_card(__('营收', 'aksrt'), number_format((float) ($order_stats['revenue'] ?? 0), 2), $this->page_url('aksrt_orders'));
$this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending']));
$this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active']));
-112
View File
@@ -288,13 +288,6 @@ class PayOrders {
$admin_note = (string) $order['note'];
}
$shipping_items = class_exists('\AKSRT\Payment\Shipping')
? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number)
: [];
$after_sale = class_exists('\AKSRT\Payment\AfterSale')
? (\AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20])['items'] ?? [])
: [];
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">&larr; ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
@@ -344,87 +337,6 @@ class PayOrders {
echo '</select> ';
submit_button(__('保存状态', 'aksrt'), 'primary', '', false);
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) . '" />';
echo '<input type="hidden" name="redirect_page" value="order" />';
wp_nonce_field('aksrt_pay_create_shipping_' . $order_number);
echo '<p><label>' . esc_html__('承运商', 'aksrt') . '<br>';
$this->render_carrier_field('');
echo '</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', ['s' => $order_number])) . '">' . esc_html__('物流管理', 'aksrt') . '</a>';
echo '</form>';
if (!$shipping_items) {
echo '<p>' . esc_html__('暂无物流记录。', 'aksrt') . '</p>';
} else {
$status_options = $this->shipping_status_options(false);
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><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
foreach ($shipping_items as $item) {
$sid = (int) ($item['id'] ?? 0);
$st = (string) ($item['status'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $sid) . '</td>';
echo '<td>' . esc_html((string) ($item['carrier'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($item['tracking_no'] ?? '')) . '</td>';
echo '<td>' . esc_html($status_options[$st] ?? $st) . '</td>';
echo '<td>' . esc_html((string) ($item['created_at'] ?? '')) . '</td>';
echo '<td><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_shipping" />';
echo '<input type="hidden" name="shipping_id" value="' . esc_attr((string) $sid) . '" />';
echo '<input type="hidden" name="redirect_order" value="' . esc_attr($order_number) . '" />';
wp_nonce_field('aksrt_pay_update_shipping_' . $sid);
echo '<select name="new_status">';
foreach ($status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($label));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
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) . '" />';
echo '<input type="hidden" name="redirect_page" value="order" />';
wp_nonce_field('aksrt_pay_create_after_sale_' . $order_number);
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
foreach ($this->after_sale_type_options(false) 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') . '</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 ($after_sale as $item) {
$tid = (int) ($item['id'] ?? 0);
echo '<tr>';
echo '<td>' . esc_html((string) $tid) . '</td>';
echo '<td>' . esc_html((string) ($item['type_label'] ?? $item['type'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($item['status_label'] ?? $item['status'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($item['created_at'] ?? '')) . '</td>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_after_sale', ['s' => $order_number])) . '">' . esc_html__('在售后页处理', 'aksrt') . '</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
}
public function handle_update_order_status(): void {
@@ -510,30 +422,6 @@ class PayOrders {
exit;
}
private function shipping_status_options(bool $include_all = false): array {
if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) {
return PayShipping::get_instance()->shipping_status_options_public($include_all);
}
return $include_all ? ['' => __('全部状态', 'aksrt')] : [];
}
private function after_sale_type_options(bool $include_all = false): array {
if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) {
return PayAfterSale::get_instance()->after_sale_type_options_public($include_all);
}
return $include_all ? ['' => __('全部类型', 'aksrt')] : [
'refund' => __('仅退款', 'aksrt'),
];
}
private function render_carrier_field(string $selected = ''): void {
if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) {
PayShipping::get_instance()->render_carrier_field_public($selected);
return;
}
echo '<input type="text" name="carrier" value="' . esc_attr($selected) . '" required />';
}
private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void {
$actions = [
'completed' => __('完成', 'aksrt'),
-507
View File
@@ -1,507 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
class PayShipping {
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_shipping';
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_action('admin_post_aksrt_pay_update_shipping', [$this, 'handle_update_shipping']);
add_action('admin_post_aksrt_pay_query_express', [$this, 'handle_query_express']);
add_action('admin_post_aksrt_pay_create_shipping', [$this, 'handle_create_shipping']);
}
public function render_shipping_page(): void {
$this->ensure_cap();
$this->render_header(__('发货及物流', 'aksrt'));
if (!class_exists('\AKSRT\Payment\Shipping')) {
echo '<div class="notice notice-error"><p>' . esc_html__('物流模块未初始化。', 'aksrt') . '</p></div>';
$this->render_footer();
return;
}
$view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'records';
if (!in_array($view, ['records', 'pending'], true)) {
$view = 'records';
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$svc = \AKSRT\Payment\Shipping::get_instance();
$stats = $svc->get_stats();
$status_options = $this->shipping_status_options(true);
$pending_total = 0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$pending_total = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1, 'search' => ''])['total'] ?? 0);
}
echo '<p>' . esc_html(sprintf(
/* translators: 1: shipping records 2: pending shipment orders */
__('物流记录 %1$d 条 · 待发货订单 %2$d', 'aksrt'),
(int) ($stats['total'] ?? 0),
$pending_total
)) . '</p>';
echo '<h2 class="nav-tab-wrapper" style="margin:12px 0 16px;">';
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
esc_url($this->page_url('aksrt_shipping', ['view' => 'pending'])),
$view === 'pending' ? ' nav-tab-active' : '',
esc_html(sprintf(__('待发货 (%d)', 'aksrt'), $pending_total))
);
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
esc_url($this->page_url('aksrt_shipping', ['view' => 'records'])),
$view === 'records' ? ' nav-tab-active' : '',
esc_html__('物流记录', 'aksrt')
);
echo '</h2>';
if ($view === 'pending') {
$this->render_pending_shipment_board($search, $page_num, $per_page);
$this->render_footer();
return;
}
echo '<h2>' . esc_html__('新建发货', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 20px;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="redirect_page" value="shipping" />';
wp_nonce_field('aksrt_pay_create_shipping');
echo '<p><label>' . esc_html__('订单号', 'aksrt') . '<br><input type="text" name="order_number" value="' . esc_attr($search) . '" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('承运商', 'aksrt') . '<br>';
$this->render_carrier_field('');
echo '</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'), 'primary', '', false);
echo '</form>';
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_shipping" />';
echo '<input type="hidden" name="view" value="records" />';
echo '<select name="status">';
foreach ($status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号 / 运单号', 'aksrt') . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$args = [
'status' => $status,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
];
if ($search !== '') {
if (preg_match('/^[A-Za-z0-9_-]+$/', $search)) {
$args['order_number'] = $search;
} else {
$args['tracking_no'] = $search;
}
}
$result = $svc->get_list($args);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
$row_status_options = $this->shipping_status_options(false);
$show_express_id = isset($_GET['express_id']) ? (int) $_GET['express_id'] : 0;
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('承运商', 'aksrt') . '</th><th>' . esc_html__('运单号', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无物流记录', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
$st = (string) ($row['status'] ?? '');
$order_number = (string) ($row['order_number'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '"><code>' . esc_html($order_number) . '</code></a></td>';
echo '<td>' . esc_html((string) ($row['carrier'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($row['tracking_no'] ?? '')) . '</td>';
echo '<td>' . esc_html($row_status_options[$st] ?? $st) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_shipping" />';
echo '<input type="hidden" name="shipping_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_update_shipping_' . $id);
echo '<select name="new_status">';
foreach ($row_status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($label));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_query_express" />';
echo '<input type="hidden" name="shipping_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="force" value="1" />';
wp_nonce_field('aksrt_pay_query_express_' . $id);
submit_button(__('查物流', 'aksrt'), 'small', '', false);
echo '</form>';
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_shipping', ['view' => 'records', 'express_id' => $id, 's' => $order_number])) . '">' . esc_html__('轨迹', 'aksrt') . '</a>';
echo '</td></tr>';
if ($show_express_id === $id) {
echo '<tr class="aksrt-express-row"><td colspan="7">';
$this->render_express_timeline_panel($id);
echo '</td></tr>';
}
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
$this->render_footer();
}
private function render_pending_shipment_board(string $search, int $page_num, int $per_page): void {
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
return;
}
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_shipping" />';
echo '<input type="hidden" name="view" value="pending" />';
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号 / 类型', 'aksrt') . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$result = \AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders([
'search' => $search,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
]);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ($result['max_pages'] ?? 1));
$labels = class_exists('\AKSRT\Core\DomainModel')
? \AKSRT\Core\DomainModel::order_status_labels()
: [];
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
echo '<th>' . esc_html__('类型', 'aksrt') . '</th>';
echo '<th>' . esc_html__('金额', 'aksrt') . '</th>';
echo '<th>' . esc_html__('状态', 'aksrt') . '</th>';
echo '<th>' . esc_html__('支付时间', 'aksrt') . '</th>';
echo '<th>' . esc_html__('立即发货', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无待发货订单', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$order_number = (string) ($row['order_number'] ?? '');
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$status_key = (string) ($row['status'] ?? '');
echo '<tr>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '"><code>' . esc_html($order_number) . '</code></a></td>';
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
echo '<td>' . esc_html((string) ($row['product_type'] ?? '')) . '</td>';
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '</td>';
echo '<td>' . esc_html($labels[$status_key] ?? $status_key) . '</td>';
echo '<td>' . esc_html((string) ($row['paid_at'] ?? $row['created_at'] ?? '')) . '</td>';
echo '<td>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_shipping" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
echo '<input type="hidden" name="redirect_page" value="pending" />';
wp_nonce_field('aksrt_pay_create_shipping_' . $order_number);
$this->render_carrier_field('');
echo '<input type="text" name="tracking_no" required placeholder="' . esc_attr__('运单号', 'aksrt') . '" />';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('发货', 'aksrt'), 'primary small', '', false);
echo '</form>';
echo '</td>';
echo '</tr>';
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
}
public function handle_update_shipping(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0;
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_pay_update_shipping_' . $id);
$ok = false;
if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\Shipping')) {
$ok = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id());
}
$redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : '';
if ($redirect_order !== '') {
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $redirect_order,
'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed',
]));
exit;
}
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'records',
'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed',
]));
exit;
}
public function handle_query_express(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0;
$force = !empty($_POST['force']);
check_admin_referer('aksrt_pay_query_express_' . $id);
$ok = false;
$order_number = '';
if ($id > 0 && class_exists('\AKSRT\Payment\Shipping')) {
$svc = \AKSRT\Payment\Shipping::get_instance();
$record = $svc->get($id);
$order_number = is_array($record) ? (string) ($record['order_number'] ?? '') : '';
$result = $svc->query_express($id, $force);
$ok = !empty($result['success']);
}
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'records',
'express_id' => $id,
's' => $order_number,
'aksrt_pay_msg' => $ok ? 'express_ok' : 'express_failed',
]));
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'])) : '';
$redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order';
$nonce_action = $redirect_page === 'shipping' ? 'aksrt_pay_create_shipping' : ('aksrt_pay_create_shipping_' . $order_number);
check_admin_referer($nonce_action);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\Shipping')) {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order) {
$this->redirect_shipping_create_result(false, $order_number, $redirect_page);
}
}
$id = \AKSRT\Payment\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;
}
$this->redirect_shipping_create_result($ok, $order_number, $redirect_page);
}
private function redirect_shipping_create_result(bool $ok, string $order_number, string $redirect_page): void {
$msg = $ok ? 'shipping_created' : 'shipping_create_failed';
if ($redirect_page === 'shipping') {
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'records',
'aksrt_pay_msg' => $msg,
's' => $order_number,
]));
exit;
}
if ($redirect_page === 'pending') {
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'pending',
'aksrt_pay_msg' => $msg,
]));
exit;
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $order_number,
'aksrt_pay_msg' => $msg,
]));
exit;
}
public function shipping_status_options_public(bool $include_all = false): array {
return $this->shipping_status_options($include_all);
}
private function shipping_status_options(bool $include_all = false): array {
$options = [];
if ($include_all) {
$options[''] = __('全部状态', 'aksrt');
}
if (class_exists('\AKSRT\Payment\Shipping')) {
foreach (\AKSRT\Payment\Shipping::get_status_options() as $item) {
$value = (string) ($item['value'] ?? '');
if ($value === '') {
continue;
}
$options[$value] = (string) ($item['label'] ?? $value);
}
}
if (count($options) === (int) $include_all) {
$options = array_merge($options, [
'shipped' => __('已发货', 'aksrt'),
'in_transit' => __('运输中', 'aksrt'),
'delivered' => __('已签收', 'aksrt'),
'returned' => __('已退回', 'aksrt'),
]);
}
return $options;
}
public function render_carrier_field_public(string $selected = ''): void {
$this->render_carrier_field($selected);
}
private function render_carrier_field(string $selected = ''): void {
$options = [];
if (class_exists('\AKSRT\Payment\Shipping')) {
foreach (\AKSRT\Payment\Shipping::get_carrier_options() as $item) {
$value = (string) ($item['value'] ?? '');
$label = (string) ($item['label'] ?? $value);
if ($value !== '') {
$options[$value] = $label;
}
}
}
if ($options) {
echo '<select name="carrier" required>';
foreach ($options as $value => $label) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($value),
selected($selected, $value, false),
esc_html($label)
);
}
echo '</select>';
return;
}
echo '<input type="text" name="carrier" value="' . esc_attr($selected) . '" required placeholder="SF/YTO/..." />';
}
private function render_express_timeline_panel(int $shipping_id): void {
if ($shipping_id <= 0 || !class_exists('\AKSRT\Payment\Shipping')) {
echo '<p>' . esc_html__('无法加载物流轨迹。', 'aksrt') . '</p>';
return;
}
$svc = \AKSRT\Payment\Shipping::get_instance();
// Prefer cached data; do not force remote query on pure view.
$timeline = $svc->build_express_timeline($shipping_id);
$record = $svc->get($shipping_id);
echo '<div style="padding:10px 12px;background:#f6f7f7;border:1px solid #dcdcde;">';
echo '<p style="margin:0 0 8px;"><strong>' . esc_html__('物流轨迹', 'aksrt') . '</strong> · ';
echo esc_html((string) ($record['carrier'] ?? '')) . ' / ' . esc_html((string) ($record['tracking_no'] ?? ''));
echo '</p>';
if (!$timeline) {
echo '<p style="margin:0;">' . esc_html__('暂无轨迹,可点击「查物流」拉取快递 API。', 'aksrt') . '</p>';
echo '</div>';
return;
}
echo '<ol style="margin:0;padding-left:18px;">';
// newest first for operators
$timeline = array_reverse($timeline);
foreach ($timeline as $item) {
$time = (string) ($item['time'] ?? '');
$label = (string) ($item['label'] ?? '');
echo '<li style="margin:0 0 6px;"><code>' . esc_html($time) . '</code> ' . esc_html($label) . '</li>';
}
echo '</ol></div>';
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
}
private function render_header(string $title): void {
echo '<div class="wrap">';
echo '<h1>' . esc_html($title) . '</h1>';
$this->render_flash();
}
private function render_footer(): void {
echo '</div>';
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$map = [
'shipping_updated' => __('物流状态已更新。', 'aksrt'),
'shipping_failed' => __('物流状态更新失败。', 'aksrt'),
'shipping_created' => __('发货记录已创建。', 'aksrt'),
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
'express_ok' => __('物流轨迹已更新。', 'aksrt'),
'express_failed' => __('物流轨迹查询失败。', 'aksrt'),
];
if (!isset($map[$code])) {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
}
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShop {
public static function build(): array {
return [
'key' => 'shop',
'title' => '商城&商品',
'version' => '1.0.0',
'sections' => [
'basic' => SchemaShopBasic::build(),
'list' => SchemaShopList::build(),
'contact' => SchemaShopContact::build(),
'single' => SchemaShopSingle::build(),
'product-defaults' => SchemaShopProductDefaults::build(),
'shipping-config' => SchemaShopShippingConfig::build(),
'timing' => SchemaShopTiming::build(),
],
];
}
}
@@ -1,37 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopBasic {
public static function build(): array {
return [
'key' => 'basic',
'title' => '商城基础',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'general', 'title' => '商城开关', 'fields' => [
['key' => 'shop_s', 'label' => '启用商城', 'type' => 'switch', 'default' => true],
['key' => 'shop_home_url', 'label' => '商城首页链接', 'type' => 'input', 'default' => ''],
]],
['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
['key' => 'shop_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
['key' => 'shop_breadcrumbs_home', 'label' => '显示首页链接', 'type' => 'switch', 'default' => true],
['key' => 'shop_breadcrumbs_shop_home', 'label' => '显示商城首页', 'type' => 'switch', 'default' => false],
['key' => 'shop_breadcrumbs_shop_home_name', 'label' => '商城首页名称', 'type' => 'input', 'default' => '商城首页'],
]],
['key' => 'rewrite', 'title' => 'URL 别名', 'fields' => [
['key' => 'shop_product_rewrite_slug', 'label' => '商品 URL 别名', 'type' => 'input', 'default' => 'product'],
['key' => 'shop_cart_rewrite_slug', 'label' => '购物车 URL 别名', 'type' => 'input', 'default' => 'cart'],
['key' => 'shop_rewrite_suffix_html_s', 'label' => '.html 伪静态后缀', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
@@ -1,30 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopContact {
public static function build(): array {
return [
'key' => 'contact',
'title' => '客服配置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'button', 'title' => '客服按钮', 'fields' => [
['key' => 'shop_contact_msg_s', 'label' => '显示客服按钮', 'type' => 'switch', 'default' => true],
['key' => 'shop_contact_name', 'label' => '按钮文案', 'type' => 'input', 'default' => '联系客服'],
['key' => 'shop_contact_note', 'label' => '客服备注', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'channels', 'title' => '联系方式', 'fields' => [
['key' => 'shop_contact_more', 'label' => '多客服联系方式', 'type' => 'textarea', 'default' => '', 'help' => '每行一个:名称|图标|链接|二维码图片 URL'],
]],
],
];
}
}
-51
View File
@@ -1,51 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopList {
public static function build(): array {
return [
'key' => 'list',
'title' => '商品列表',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'pagination', 'title' => '列表分页', 'fields' => [
['key' => 'shop_list_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [
['label' => '最新发布', 'value' => 'date'],
['label' => '销量最高', 'value' => 'sales'],
['label' => '价格最低', 'value' => 'price_asc'],
['label' => '价格最高', 'value' => 'price_desc'],
['label' => '评价最多', 'value' => 'comments'],
]],
['key' => 'shop_list_count', 'label' => '每页数量', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 100],
['key' => 'shop_list_paging', 'label' => '翻页模式', 'type' => 'radio', 'default' => 'number', 'options' => [
['label' => '数字翻页', 'value' => 'number'],
['label' => 'AJAX 加载', 'value' => 'ajax'],
]],
['key' => 'shop_list_ias_s', 'label' => '滚动自动加载', 'type' => 'switch', 'default' => false],
['key' => 'shop_list_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
]],
['key' => 'style', 'title' => '列表样式', 'fields' => [
['key' => 'shop_list_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'default', 'options' => [
['label' => '默认', 'value' => 'default'],
['label' => '竖向大卡片', 'value' => 'card-lg'],
['label' => '横向小卡片', 'value' => 'card-sm'],
]],
['key' => 'shop_sales_show', 'label' => '销量显示', 'type' => 'select', 'default' => 'show', 'options' => [
['label' => '显示', 'value' => 'show'],
['label' => '不显示', 'value' => 'hide'],
['label' => '超量显示', 'value' => 'threshold'],
]],
['key' => 'shop_sales_show_min', 'label' => '起显阈值', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 99999],
]],
],
];
}
}
@@ -1,46 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopProductDefaults {
public static function build(): array {
return [
'key' => 'product-defaults',
'title' => '商品默认参数',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'policy', 'title' => '购买策略', 'fields' => [
['key' => 'shop_limit_buy', 'label' => '默认限购', 'type' => 'number', 'default' => -1, 'min' => -1, 'max' => 9999, 'help' => '-1 不限,0 禁购,正数为限购数量'],
['key' => 'shop_guest_buy', 'label' => '免登录购买', 'type' => 'switch', 'default' => false],
['key' => 'shop_email_fill', 'label' => '邮箱必留', 'type' => 'switch', 'default' => false],
['key' => 'shop_rebate', 'label' => '允许返佣', 'type' => 'switch', 'default' => true],
]],
['key' => 'shipping', 'title' => '运费默认', 'fields' => [
['key' => 'shop_shipping_fee', 'label' => '默认运费类型', 'type' => 'select', 'default' => 'free', 'options' => [
['label' => '包邮', 'value' => 'free'],
['label' => '固定运费', 'value' => 'fixed'],
['label' => '按金额计算', 'value' => 'amount'],
]],
]],
['key' => 'afterSale', 'title' => '售后与服务', 'fields' => [
['key' => 'shop_after_sale', 'label' => '默认售后政策', 'type' => 'select', 'default' => 'refund_return', 'options' => [
['label' => '仅退款', 'value' => 'refund'],
['label' => '退货退款', 'value' => 'refund_return'],
['label' => '不支持售后', 'value' => 'none'],
]],
['key' => 'shop_service', 'label' => '服务保障', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'params', 'title' => '默认参数模板', 'fields' => [
['key' => 'shop_product_params_default', 'label' => '商品参数模板', 'type' => 'textarea', 'default' => ''],
]],
],
];
}
}
@@ -1,41 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopShippingConfig {
public static function build(): array {
return [
'key' => 'shipping-config',
'title' => '物流配置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'address', 'title' => '收货地址', 'fields' => [
['key' => 'shop_manual_address', 'label' => '手动填写收货地址', 'type' => 'switch', 'default' => false],
]],
['key' => 'express', 'title' => '快递查询', 'fields' => [
['key' => 'shop_express_api_sdk', 'label' => '快递查询接口', 'type' => 'select', 'default' => 'kuaidi100', 'options' => [
['label' => '快递100', 'value' => 'kuaidi100'],
['label' => '阿里云', 'value' => 'aliyun'],
['label' => '快递鸟', 'value' => 'kdniao'],
]],
['key' => 'shop_express_query_interval', 'label' => '查询刷新间隔(分钟)', 'type' => 'number', 'default' => 60, 'min' => 30, 'max' => 240],
]],
['key' => 'credentials', 'title' => '接口凭据', 'fields' => [
['key' => 'shop_express_kdniao_opt', 'label' => '快递鸟凭据', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_express_kuaidi100_opt', 'label' => '快递100凭据', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_express_aliyun_opt', 'label' => '阿里云凭据', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'companies', 'title' => '物流公司', 'fields' => [
['key' => 'shop_shipping_company', 'label' => '物流公司列表', 'type' => 'textarea', 'default' => ''],
]],
],
];
}
}
@@ -1,39 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopSingle {
public static function build(): array {
return [
'key' => 'single',
'title' => '商品详情',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'layout', 'title' => '详情页布局', 'fields' => [
['key' => 'shop_content_layout', 'label' => '内容布局', 'type' => 'select', 'default' => 'adapt', 'options' => [
['label' => '适应宽度', 'value' => 'adapt'],
['label' => '全屏', 'value' => 'full'],
['label' => '适应宽度+侧栏', 'value' => 'sidebar'],
]],
['key' => 'shop_content_show_bg', 'label' => '详情背景盒子', 'type' => 'switch', 'default' => false],
['key' => 'shop_single_tab', 'label' => '自定义 TAB 栏目', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_content_after', 'label' => '详情页底部内容', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'related', 'title' => '相关推荐', 'fields' => [
['key' => 'shop_single_related_s', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true],
['key' => 'shop_single_related_opt', 'label' => '推荐配置', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'image', 'title' => '商品图片', 'fields' => [
['key' => 'shop_main_image_default', 'label' => '默认商品主图', 'type' => 'media', 'default' => ''],
]],
],
];
}
}
@@ -1,38 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopTiming {
public static function build(): array {
return [
'key' => 'timing',
'title' => '时效与售后原因',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'receipt', 'title' => '确认收货', 'fields' => [
['key' => 'shop_order_receipt_max_day', 'label' => '自动确认收货(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 60],
]],
['key' => 'comment', 'title' => '评价时效', 'fields' => [
['key' => 'shop_comment_max_day', 'label' => '评价时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 60],
['key' => 'shop_comment_placeholder', 'label' => '评价占位文案', 'type' => 'input', 'default' => ''],
['key' => 'shop_comment_img_num', 'label' => '评价图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 9],
]],
['key' => 'afterSale', 'title' => '售后退货', 'fields' => [
['key' => 'shop_after_sale_return_express_max_day', 'label' => '退货快递时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 30],
['key' => 'shop_after_sale_reason_refund', 'label' => '仅退款原因', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_after_sale_reason_return', 'label' => '退货退款原因', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'author', 'title' => '商家信息', 'fields' => [
['key' => 'shop_author_show', 'label' => '显示商家信息', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
-453
View File
@@ -1,453 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class ShopController extends ControllerBase {
use ShopControllerProductAdminColumns;
use ShopControllerFrontendFlowOperations;
use ShopControllerProductOperations;
use ShopControllerOrderOperations;
use ShopControllerFulfillmentOperations;
use ShopControllerCouponGatewayOperations;
use ShopControllerVariantExportOperations;
private static $instance = null;
private $admin;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function set_admin(Admin $admin): void {
$this->admin = $admin;
}
public function register_routes(): void {
register_rest_route(self::API_NAMESPACE, '/shop/frontend/checkout/preview', [
'methods' => 'POST',
'callback' => [$this, 'preview_checkout'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_orders'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/pay', [
'methods' => 'POST',
'callback' => [$this, 'pay_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/cancel', [
'methods' => 'POST',
'callback' => [$this, 'cancel_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/after-sale', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_after_sale'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_after_sales'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P<id>\d+)/return-shipping', [
'methods' => 'POST',
'callback' => [$this, 'submit_frontend_return_shipping'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P<id>\d+)/cancel', [
'methods' => 'POST',
'callback' => [$this, 'cancel_frontend_after_sale'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/coupons', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_coupons'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// ── Address book CRUD (frontend) ──
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_addresses'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)/default', [
'methods' => 'POST',
'callback' => [$this, 'set_default_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_product'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_frontend_product'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// Cart endpoints (§6.4)
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart', [
'methods' => 'GET',
'callback' => [$this, 'get_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/add', [
'methods' => 'POST',
'callback' => [$this, 'handle_add_to_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/reduce', [
'methods' => 'POST',
'callback' => [$this, 'handle_reduce_from_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove', [
'methods' => 'POST',
'callback' => [$this, 'handle_remove_from_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove-multi', [
'methods' => 'POST',
'callback' => [$this, 'handle_remove_multi_from_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/empty', [
'methods' => 'POST',
'callback' => [$this, 'handle_empty_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/count', [
'methods' => 'GET',
'callback' => [$this, 'get_cart_count'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// Shop overview / dashboard
register_rest_route(self::API_NAMESPACE, '/shop/overview', [
'methods' => 'GET',
'callback' => [$this, 'get_overview'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product taxonomy options used by the product editor.
register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [
'methods' => 'GET',
'callback' => [$this, 'get_product_taxonomy'],
'permission_callback' => [$this, 'check_permission'],
]);
// Create product category or tag term for the product editor.
register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [
'methods' => 'POST',
'callback' => [$this, 'create_product_taxonomy_term'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product list
register_rest_route(self::API_NAMESPACE, '/shop/products', [
'methods' => 'GET',
'callback' => [$this, 'get_products'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product create
register_rest_route(self::API_NAMESPACE, '/shop/products', [
'methods' => 'POST',
'callback' => [$this, 'create_product'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product update
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_product'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product single read
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_product'],
'permission_callback' => [$this, 'check_permission'],
]);
// Order list
register_rest_route(self::API_NAMESPACE, '/shop/orders', [
'methods' => 'GET',
'callback' => [$this, 'get_orders'],
'permission_callback' => [$this, 'check_permission'],
]);
// Order single read
register_rest_route(self::API_NAMESPACE, '/shop/orders/(?P<order_number>[A-Za-z0-9_-]+)', [
'methods' => 'GET',
'callback' => [$this, 'get_order'],
'permission_callback' => [$this, 'check_permission'],
]);
// Order status update — canonical field is order_number
register_rest_route(self::API_NAMESPACE, '/shop/orders/update-status', [
'methods' => 'POST',
'callback' => [$this, 'update_order_status'],
'permission_callback' => [$this, 'check_permission'],
]);
// Gateway status
register_rest_route(self::API_NAMESPACE, '/shop/gateways', [
'methods' => 'GET',
'callback' => [$this, 'get_gateways'],
'permission_callback' => [$this, 'check_permission'],
]);
// Coupon list (admin)
register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
'methods' => 'GET',
'callback' => [$this, 'get_coupons'],
'permission_callback' => [$this, 'check_permission'],
]);
// Coupon create (admin)
register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
'methods' => 'POST',
'callback' => [$this, 'create_coupon'],
'permission_callback' => [$this, 'check_permission'],
]);
// Coupon disable (admin)
register_rest_route(self::API_NAMESPACE, '/shop/coupons/(?P<id>\d+)/disable', [
'methods' => 'POST',
'callback' => [$this, 'disable_coupon'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── After Sale ──
// After-sale list (admin)
register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
'methods' => 'GET',
'callback' => [$this, 'get_after_sale_list'],
'permission_callback' => [$this, 'check_permission'],
]);
// After-sale create (admin)
register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
'methods' => 'POST',
'callback' => [$this, 'create_after_sale'],
'permission_callback' => [$this, 'check_permission'],
]);
// After-sale status update (admin)
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P<id>\d+)/update-status', [
'methods' => 'POST',
'callback' => [$this, 'update_after_sale_status'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_after_sale_detail'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── Shipping ──
// Shipping list (admin)
register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
'methods' => 'GET',
'callback' => [$this, 'get_shipping_list'],
'permission_callback' => [$this, 'check_permission'],
]);
// Shipping create (admin)
register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
'methods' => 'POST',
'callback' => [$this, 'create_shipping'],
'permission_callback' => [$this, 'check_permission'],
]);
// Shipping status update (admin)
register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P<id>\d+)/update-status', [
'methods' => 'POST',
'callback' => [$this, 'update_shipping_status'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_shipping_detail'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/shipping/batch-update-status', [
'methods' => 'POST',
'callback' => [$this, 'batch_update_shipping_status'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── R68: Order export + confirm receipt + after-sale reasons ──
register_rest_route(self::API_NAMESPACE, '/shop/orders/export', [
'methods' => 'GET',
'callback' => [$this, 'export_orders'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── R69: Frontend after-sale reasons (§6.8) ──
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/reasons', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_after_sale_reasons'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/confirm-receipt', [
'methods' => 'POST',
'callback' => [$this, 'confirm_receipt'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/rebuy', [
'methods' => 'POST',
'callback' => [$this, 'rebuy_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/archive', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_product_archive'],
'permission_callback' => '__return_true',
]);
// Product list widget AJAX (zibll ajax_widget_ui product lists).
register_rest_route(self::API_NAMESPACE, '/shop/frontend/widgets/products', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_widget_products'],
'permission_callback' => '__return_true',
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_revenue'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue/withdraw', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_withdraw'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/review', [
'methods' => 'POST',
'callback' => [$this, 'submit_frontend_product_review'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/review-modal', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_product_review_modal'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)/favorite', [
'methods' => 'POST',
'callback' => [$this, 'toggle_frontend_product_favorite'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/reasons', [
'methods' => 'GET',
'callback' => [$this, 'get_after_sale_reasons'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── Product Variants / SKU ──
// List variants for a product
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)/variants', [
'methods' => 'GET',
'callback' => [$this, 'get_product_variants'],
'permission_callback' => [$this, 'check_permission'],
]);
// Save variants for a product (replace all)
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)/variants', [
'methods' => 'POST',
'callback' => [$this, 'save_product_variants'],
'permission_callback' => [$this, 'check_permission'],
]);
// Delete a variant
register_rest_route(self::API_NAMESPACE, '/shop/variants/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_variant'],
'permission_callback' => [$this, 'check_permission'],
]);
}
public function check_logged_in_permission(): bool {
return is_user_logged_in();
}
}
@@ -1,97 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerCouponGatewayOperations {
/**
* GET /shop/coupons — 优惠券列表
*/
public function get_coupons(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$search = sanitize_text_field((string) $request->get_param('search'));
$result = \AKSRT\Payment\Coupon::get_instance()->get_list([
'status' => $status,
'search' => $search,
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
]);
return $this->success(['data' => $result]);
}
/**
* POST /shop/coupons — 创建优惠券
*/
public function create_coupon(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->error('优惠券系统未启用', 500);
}
$params = $this->get_json_params($request);
$code = sanitize_text_field((string) ($params['code'] ?? ''));
if ($code === '') {
return $this->error('优惠码不能为空');
}
$id = \AKSRT\Payment\Coupon::get_instance()->create($params);
if ($id > 0) {
return $this->success([
'message' => __('优惠券已创建', 'aksrt'),
'data' => ['id' => $id],
], 201);
}
return $this->error(__('创建失败(可能优惠码已存在)', 'aksrt'));
}
/**
* POST /shop/coupons/{id}/disable — 禁用优惠券
*/
public function disable_coupon(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->error('优惠券系统未启用', 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0) {
return $this->error('参数无效');
}
$success = \AKSRT\Payment\Coupon::get_instance()->disable($id);
if ($success) {
return $this->success(['message' => __('优惠券已禁用', 'aksrt')]);
}
return $this->error(__('操作失败', 'aksrt'));
}
/**
* GET /shop/gateways — 支付网关状态列表
*/
public function get_gateways(): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Gateway')) {
return $this->success(['data' => ['gateways' => []]]);
}
return $this->success([
'data' => [
'gateways' => \AKSRT\Payment\Gateway::get_instance()->get_status_summary(),
],
]);
}
}
@@ -1,717 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerFrontendFlowOperations {
public function check_logged_in_permission(): bool {
return is_user_logged_in();
}
public function preview_checkout(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$result = \AKSRT\Shop\Shop::get_instance()->preview_checkout(get_current_user_id(), $this->get_json_params($request));
if (empty($result['success'])) {
return $this->error($result['message'] ?? '结算预览失败');
}
return $this->success(['data' => $result['data']]);
}
public function create_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$result = \AKSRT\Shop\Shop::get_instance()->create_checkout_order(get_current_user_id(), $this->get_json_params($request));
if (empty($result['success'])) {
return $this->error($result['message'] ?? '订单创建失败');
}
return $this->success([
'message' => __('订单已创建', 'aksrt'),
'data' => $result['data'],
], 201);
}
public function get_frontend_orders(\WP_REST_Request $request): \WP_REST_Response {
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$product_type = sanitize_key((string) $request->get_param('product_type'));
$args = [
'page' => $page,
'per_page' => $per_page,
'status' => $status,
'product_type' => $product_type,
];
if (class_exists('\AKSRT\Payment\UserOrders')) {
$view = \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $args);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$view = \AKSRT\Shop\Shop::get_instance()->get_order_center_view(get_current_user_id(), $args);
} else {
$view = ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1];
}
return $this->success(['data' => $view]);
}
public function get_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$order = null;
if (class_exists('\AKSRT\Payment\UserOrders')) {
$order = \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
}
if (!$order) {
return $this->error('订单不存在', 404);
}
return $this->success(['data' => $order]);
}
public function pay_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$payment_method = sanitize_key((string) ($params['payment_method'] ?? ''));
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (class_exists('\AKSRT\Payment\UserOrders')) {
$result = \AKSRT\Payment\UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method ?: 'balance');
} else {
return $this->error('订单服务不可用', 500);
}
if (empty($result['success'])) {
return $this->error($result['message'] ?? '支付失败');
}
return $this->success(['data' => $result['data']]);
}
public function cancel_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (class_exists('\AKSRT\Payment\UserOrders')) {
$result = \AKSRT\Payment\UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number);
} else {
return $this->error('订单服务不可用', 500);
}
if (empty($result['success'])) {
return $this->error($result['message'] ?? '取消订单失败');
}
return $this->success(['data' => $result['data']]);
}
public function create_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$params = $this->get_json_params($request);
$params['order_number'] = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\Shop::get_instance()->create_after_sale_request(get_current_user_id(), $params);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '提交售后失败');
}
return $this->success([
'message' => __('售后申请已提交', 'aksrt'),
'data' => $result['data'],
], 201);
}
public function get_frontend_after_sales(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$data = \AKSRT\Shop\Shop::get_instance()->get_user_after_sales(get_current_user_id());
return $this->success(['data' => $data]);
}
public function submit_frontend_return_shipping(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$result = \AKSRT\Shop\Shop::get_instance()->submit_after_sale_return(get_current_user_id(), $id, $params);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '提交退货物流失败');
}
return $this->success([
'message' => $result['message'] ?? __('退货物流已提交', 'aksrt'),
'data' => $result['data'] ?? [],
]);
}
public function cancel_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$reason = sanitize_text_field((string) ($params['reason'] ?? ''));
$result = \AKSRT\Shop\Shop::get_instance()->cancel_after_sale(get_current_user_id(), $id, $reason);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '取消售后失败');
}
return $this->success(['message' => $result['message'] ?? __('已取消', 'aksrt')]);
}
/**
* Active public coupons for wallet UI (not personal inventory — site-wide active codes).
*/
public function get_frontend_coupons(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->error('优惠券不可用', 500);
}
$list = \AKSRT\Payment\Coupon::get_instance()->get_list([
'status' => 'active',
'limit' => 50,
'offset' => 0,
]);
$user_id = get_current_user_id();
$items = [];
foreach ((array) ($list['items'] ?? []) as $c) {
$items[] = [
'id' => (int) ($c['id'] ?? 0),
'code' => (string) ($c['code'] ?? ''),
'type' => (string) ($c['type'] ?? ''),
'value' => (float) ($c['value'] ?? 0),
'min_amount' => (float) ($c['min_amount'] ?? 0),
'max_discount' => (float) ($c['max_discount'] ?? 0),
'expires_at' => (string) ($c['expires_at'] ?? ''),
'user_used' => $user_id > 0
? (int) \AKSRT\Payment\Coupon::get_instance()->get_user_usage_count((int) ($c['id'] ?? 0), $user_id)
: 0,
'per_user_limit' => (int) ($c['per_user_limit'] ?? 0),
];
}
return $this->success(['data' => ['items' => $items]]);
}
public function get_cart(\WP_REST_Request $request): \WP_REST_Response {
$user_id = get_current_user_id();
$shop = \AKSRT\Shop\Shop::get_instance();
$cart = $shop->get_cart($user_id);
$items = [];
foreach ($cart as $pid => $opts_map) {
$product_id = (int) $pid;
$product = $shop->build_product_card_view($product_id);
if (empty($product) || !is_array($opts_map)) {
continue;
}
foreach ($opts_map as $opt_str => $qty) {
$options_string = $opt_str === '_default' ? '' : (string) $opt_str;
$unit_price = method_exists($shop, 'resolve_option_price')
? (float) $shop->resolve_option_price($product_id, $options_string !== '' ? $options_string : '_default')
: (float) ($product['price'] ?? 0);
$options_label = method_exists($shop, 'resolve_option_labels')
? (string) $shop->resolve_option_labels($product_id, $options_string !== '' ? $options_string : '_default')
: '';
$option_image = method_exists($shop, 'get_option_image')
? (string) $shop->get_option_image($product_id, $options_string !== '' ? $options_string : '_default')
: '';
$stock = method_exists($shop, 'resolve_option_stock')
? (int) $shop->resolve_option_stock($product_id, $options_string !== '' ? $options_string : '_default')
: (int) ($product['stock'] ?? 0);
$items[] = [
'product_id' => $product_id,
'options_string' => $options_string,
'options_label' => $options_label,
'quantity' => (int) $qty,
'unit_price' => $unit_price,
'line_total' => round($unit_price * (int) $qty, 2),
'stock' => $stock,
'image' => $option_image !== '' ? $option_image : (string) ($product['image'] ?? ''),
'title' => (string) ($product['title'] ?? ''),
'product' => $product,
];
}
}
return $this->success([
'data' => [
'items' => $items,
'count' => $shop->get_cart_count($user_id),
],
]);
}
public function get_cart_count(\WP_REST_Request $request): \WP_REST_Response {
$user_id = get_current_user_id();
return $this->success(['data' => ['count' => \AKSRT\Shop\Shop::get_instance()->get_cart_count($user_id)]]);
}
public function handle_add_to_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$product_id = (int) ($params['product_id'] ?? 0);
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
$count = max(1, (int) ($params['count'] ?? 1));
$result = \AKSRT\Shop\Shop::get_instance()->add_to_cart(get_current_user_id(), $product_id, $options_string, $count);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_reduce_from_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$product_id = (int) ($params['product_id'] ?? 0);
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
$count = max(1, (int) ($params['count'] ?? 1));
$result = \AKSRT\Shop\Shop::get_instance()->reduce_from_cart(get_current_user_id(), $product_id, $options_string, $count);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_remove_from_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$product_id = (int) ($params['product_id'] ?? 0);
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
$result = \AKSRT\Shop\Shop::get_instance()->remove_from_cart(get_current_user_id(), $product_id, $options_string);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_remove_multi_from_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$items = is_array($params['items'] ?? null) ? $params['items'] : [];
$result = \AKSRT\Shop\Shop::get_instance()->remove_multi_from_cart(get_current_user_id(), $items);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_empty_cart(\WP_REST_Request $request): \WP_REST_Response {
$result = \AKSRT\Shop\Shop::get_instance()->empty_cart(get_current_user_id());
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function toggle_frontend_product_favorite(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$product_id = (int) $request->get_param('id');
$result = \AKSRT\Shop\Shop::get_instance()->toggle_favorite(get_current_user_id(), $product_id);
if (empty($result['success'])) {
return $this->error(__('收藏操作失败', 'aksrt'));
}
return $this->success([
'message' => !empty($result['is_favorite']) ? __('已收藏', 'aksrt') : __('已取消收藏', 'aksrt'),
'data' => $result,
]);
}
public function rebuy_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\Shop::get_instance()->re_purchase(get_current_user_id(), $order_number);
if (empty($result['success'])) {
return $this->error($result['message'] ?? __('再次购买失败', 'aksrt'));
}
return $this->success([
'message' => __('商品已加入购物车', 'aksrt'),
'data' => $result,
]);
}
public function get_frontend_addresses(): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->success(['data' => ['addresses' => []]]);
}
$addresses = \AKSRT\Shop\Shop::get_instance()->get_address_book(get_current_user_id());
return $this->success(['data' => ['addresses' => $addresses]]);
}
public function create_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$params = $this->get_json_params($request);
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->add_address($user_id, $params);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '添加地址失败');
}
return $this->success([
'message' => __('地址已添加', 'aksrt'),
'data' => ['address' => $result['address']],
], 201);
}
public function update_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$address_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->update_address($user_id, $address_id, $params);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '更新地址失败');
}
return $this->success([
'message' => __('地址已更新', 'aksrt'),
'data' => ['address' => $result['address']],
]);
}
public function delete_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$address_id = (int) $request->get_param('id');
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->delete_address($user_id, $address_id);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '删除地址失败');
}
return $this->success(['message' => __('地址已删除', 'aksrt')]);
}
public function set_default_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$address_id = (int) $request->get_param('id');
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->set_default_address($user_id, $address_id);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '设置默认地址失败');
}
return $this->success(['message' => __('已设为默认地址', 'aksrt')]);
}
public function get_frontend_product_archive(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$taxonomy = sanitize_key((string) ($request->get_param('taxonomy') ?? ''));
$term_id = (int) ($request->get_param('term_id') ?? 0);
$page = max(1, (int) ($request->get_param('page') ?? 1));
$orderby = sanitize_key((string) ($request->get_param('orderby') ?? ''));
$order = sanitize_key((string) ($request->get_param('order') ?? ''));
$view = \AKSRT\Shop\Shop::get_instance()->get_product_archive_view([
'taxonomy' => $taxonomy,
'term_id' => $term_id,
'paged' => $page,
'orderby' => $orderby,
'order' => $order,
]);
$sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : [];
$html = '';
if (!empty($view['products']) && is_array($view['products'])) {
ob_start();
foreach ($view['products'] as $product) {
if (function_exists('get_template_part')) {
get_template_part('templates/parts/shop/product', 'card', [
'product' => $product,
'sales_cfg' => $sales_cfg,
]);
}
}
$html = (string) ob_get_clean();
}
$empty_html = '';
if ($html === '' && function_exists('aksrt_get_ajax_null')) {
$empty_html = aksrt_get_ajax_null(__('内容空空如也', 'aksrt'));
} elseif ($html === '') {
$empty_html = '<div class="aksrt-product-archive__empty ajax-item"><p>' . esc_html__('内容空空如也', 'aksrt') . '</p></div>';
}
// Header HTML for cat filter replace (when filter present).
$header_html = '';
if (!empty($view['filter']) && is_array($view['filter'])) {
ob_start();
$this->render_product_cat_filter_html($view);
$header_html = (string) ob_get_clean();
}
$paginate_html = '';
$page_n = (int) ($view['page'] ?? 1);
$max_pages = (int) ($view['max_pages'] ?? 1);
$has_more = $page_n < $max_pages;
if ($has_more && (string) ($view['paging'] ?? '') === 'ajax') {
$next_url = add_query_arg([
'taxonomy' => $taxonomy,
'term_id' => $term_id,
'page' => $page_n + 1,
'orderby' => (string) ($view['orderby'] ?? 'date'),
'order' => (string) ($view['order'] ?? ''),
], rest_url('aksrt/v1/shop/frontend/products/archive'));
if (function_exists('aksrt_get_ajax_next_paginate')) {
$paginate_html = aksrt_get_ajax_next_paginate(
(int) ($view['total'] ?? 0),
$page_n,
max(1, (int) ($view['per_page'] ?? 12)),
$next_url
);
}
}
return $this->success([
'data' => array_merge($view, [
'html' => $html !== '' ? $html : $empty_html,
'empty_html' => $empty_html,
'header_html' => $header_html,
'paginate_html' => $paginate_html,
'has_more' => $has_more,
'title' => !empty($view['term']['name'])
? (string) $view['term']['name'] . ' - ' . wp_strip_all_tags(get_bloginfo('name'))
: '',
]),
]);
}
/**
* GET /shop/frontend/widgets/products — product list widget AJAX pager.
*/
public function get_frontend_widget_products(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Widgets\WidgetProductList')) {
return $this->error('商品组件不可用', 500);
}
$instance = [
'per_page' => max(1, min(50, (int) ($request->get_param('per_page') ?? 8))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')),
'cat_ids' => sanitize_text_field((string) ($request->get_param('cat_ids') ?? '')),
'tag_ids' => sanitize_text_field((string) ($request->get_param('tag_ids') ?? '')),
'discount_ids' => sanitize_text_field((string) ($request->get_param('discount_ids') ?? '')),
'exclude_cat' => !empty($request->get_param('exclude_cat')) ? 1 : 0,
'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')),
'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')),
];
$page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1));
$payload = \AKSRT\Widgets\WidgetProductList::query_payload($instance, $page);
return $this->success([
'html' => (string) ($payload['html'] ?? ''),
'paginate_html' => (string) ($payload['paginate_html'] ?? ''),
'has_more' => !empty($payload['has_more']),
'page' => (int) ($payload['page'] ?? $page),
'max_pages' => (int) ($payload['max_pages'] ?? 1),
'total' => (int) ($payload['total'] ?? 0),
]);
}
/**
* Render cat filter bar HTML for AJAX header replace.
*/
private function render_product_cat_filter_html(array $view): void {
$filter = is_array($view['filter'] ?? null) ? $view['filter'] : [];
$orderby = (string) ($view['orderby'] ?? ($filter['orderby'] ?? 'date'));
$order = strtoupper((string) ($view['order'] ?? ($filter['order'] ?? 'DESC')));
$term = is_array($view['term'] ?? null) ? $view['term'] : [];
$current_id = (int) ($filter['current_id'] ?? ($term['id'] ?? 0));
include get_template_directory() . '/templates/parts/shop/product-cat-filter.php';
}
public function get_frontend_revenue(\WP_REST_Request $request): \WP_REST_Response {
if (!function_exists('aksrt_get_revenue_page_view')) {
return $this->error('收益服务不可用', 500);
}
$view = aksrt_get_revenue_page_view(get_current_user_id());
$withdrawals = [];
if (class_exists('\AKSRT\Payment\Withdrawal')) {
$rows = \AKSRT\Payment\Withdrawal::get_instance()->get_user_withdrawals(get_current_user_id(), ['limit' => 20]);
foreach ($rows as $row) {
$withdrawals[] = [
'id' => (int) ($row['id'] ?? 0),
'amount' => (float) ($row['amount'] ?? 0),
'status' => (string) ($row['status'] ?? ''),
'method' => (string) ($row['method'] ?? ''),
'created_at' => (string) ($row['created_at'] ?? ''),
'note' => (string) ($row['note'] ?? ''),
];
}
}
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : [];
return $this->success([
'data' => array_merge($view, [
'withdrawals' => $withdrawals,
'settings' => [
'min_amount' => (float) ($settings['min_amount'] ?? 50),
'service_charge_rate' => (float) ($settings['service_charge_rate'] ?? 0),
'text' => (string) ($settings['text'] ?? ''),
],
]),
]);
}
public function create_frontend_withdraw(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Withdrawal') || !class_exists('\AKSRT\Credits\Credits')) {
return $this->error('提现服务不可用', 500);
}
$params = $this->get_json_params($request);
$amount = (float) ($params['amount'] ?? 0);
$method = sanitize_key((string) ($params['method'] ?? 'alipay'));
$account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? ''));
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : [];
$min = (float) ($settings['min_amount'] ?? 50);
if ($amount < $min) {
return $this->error(sprintf(__('最低提现金额为 %s', 'aksrt'), $min));
}
if ($account === '') {
return $this->error(__('请填写收款账户信息', 'aksrt'));
}
$user_id = get_current_user_id();
$balance = (float) \AKSRT\Credits\Credits::balance($user_id);
if ($amount > $balance) {
return $this->error(__('可提现余额不足', 'aksrt'));
}
$cost = (int) round($amount);
if ($cost <= 0) {
return $this->error(__('提现金额无效', 'aksrt'));
}
$deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'withdraw', ['type' => 'withdraw'], __('用户提现申请', 'aksrt'));
if (is_wp_error($deducted)) {
return $this->error($deducted->get_error_message());
}
$id = \AKSRT\Payment\Withdrawal::get_instance()->create($user_id, $amount, $method, $account);
if ($id <= 0) {
\AKSRT\Credits\Credits::add($user_id, $cost, 'withdraw_refund', ['type' => 'withdraw'], __('提现失败退回', 'aksrt'));
return $this->error(__('提现申请创建失败', 'aksrt'), 500);
}
return $this->success([
'message' => __('提现申请已提交', 'aksrt'),
'data' => [
'id' => $id,
'amount' => $amount,
'balance' => (float) \AKSRT\Credits\Credits::balance($user_id),
],
], 201);
}
public function submit_frontend_product_review(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\ProductReview')) {
return $this->error('评价服务不可用', 500);
}
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$product_id = (int) ($params['product_id'] ?? 0);
$result = \AKSRT\Shop\ProductReview::get_instance()->submit_review(
get_current_user_id(),
$order_number,
$product_id,
$params
);
if (empty($result['success'])) {
return $this->error($result['message'] ?? __('评价失败', 'aksrt'));
}
return $this->success([
'message' => __('评价成功', 'aksrt'),
'data' => $result,
], 201);
}
/**
* GET review modal HTML for an order (zibll order_comment_modal parity).
*/
public function get_frontend_product_review_modal(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\ProductReview')) {
return $this->error(__('评价服务不可用', 'aksrt'), 500);
}
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\ProductReview::get_instance()->get_review_modal_html(
get_current_user_id(),
$order_number
);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('无法打开评价', 'aksrt')));
}
return $this->success([
'html' => (string) ($result['html'] ?? ''),
'product_id' => (int) ($result['product_id'] ?? 0),
'need_shipping' => !empty($result['need_shipping']),
'img_max' => (int) ($result['img_max'] ?? 0),
'remain_seconds' => (int) ($result['remain_seconds'] ?? 0),
]);
}
public function create_frontend_product(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$params['author_id'] = get_current_user_id();
$frontend_request = new \WP_REST_Request('POST', '/');
$frontend_request->set_body(wp_json_encode($params));
return $this->create_product($frontend_request);
}
public function update_frontend_product(\WP_REST_Request $request): \WP_REST_Response {
$id = (int) $request->get_param('id');
$post = get_post($id);
if (!$post || $post->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
if ((int) $post->post_author !== get_current_user_id() && !current_user_can('manage_options')) {
return $this->error('没有权限编辑该商品', 403);
}
return $this->update_product($request);
}
}
@@ -1,353 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerFulfillmentOperations {
/**
* GET /shop/after-sale — 售后工单列表
*/
public function get_after_sale_list(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$stage = sanitize_key((string) $request->get_param('stage'));
$order = sanitize_text_field((string) $request->get_param('order_number'));
$type = sanitize_key((string) $request->get_param('type'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$result = \AKSRT\Payment\AfterSale::get_instance()->get_list([
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
'status' => $status,
'stage' => $stage,
'order_number' => $order,
'type' => $type,
'date_from' => $date_from,
'date_to' => $date_to,
'include_order_context' => true,
]);
$stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats();
return $this->success(['data' => array_merge($result, [
'stats' => $stats,
'filters' => [
'status' => $status,
'stage' => $stage,
'order_number' => $order,
'type' => $type,
'date_from' => $date_from,
'date_to' => $date_to,
],
'options' => [
'status' => \AKSRT\Payment\AfterSale::get_status_options(),
'stage' => \AKSRT\Payment\AfterSale::get_stage_options(),
'type' => \AKSRT\Payment\AfterSale::get_type_options(),
],
])]);
}
/**
* POST /shop/after-sale — 创建售后工单
*/
public function create_after_sale(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->error('售后系统未启用', 500);
}
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
if ($order_number === '' || $reason === '') {
return $this->error('订单号和原因不能为空');
}
$order = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number)
: null;
if (!$order) {
return $this->error(__('订单不存在', 'aksrt'), 404);
}
unset($params['user_id'], $params['applicant_user_id']);
$id = \AKSRT\Payment\AfterSale::get_instance()->create($params);
if (is_wp_error($id)) {
return $this->error($id->get_error_message(), 422);
}
if ($id > 0) {
return $this->success([
'message' => __('售后工单已创建', 'aksrt'),
'data' => ['id' => $id],
], 201);
}
return $this->error(__('创建失败', 'aksrt'));
}
/**
* POST /shop/after-sale/{id}/update-status — 更新售后工单状态
*/
public function update_after_sale_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->error('售后系统未启用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$new_status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['operator_note'] ?? ''));
if ($id <= 0 || $new_status === '') {
return $this->error('参数无效');
}
$success = \AKSRT\Payment\AfterSale::get_instance()->update_status(
$id,
$new_status,
get_current_user_id(),
$note
);
if ($success) {
return $this->success(['message' => __('状态已更新', 'aksrt')]);
}
return $this->error(__('更新失败', 'aksrt'));
}
/**
* GET /shop/shipping — 发货记录列表
*/
public function get_shipping_list(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$order = sanitize_text_field((string) $request->get_param('order_number'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$tracking_no = sanitize_text_field((string) $request->get_param('tracking_no'));
$result = \AKSRT\Payment\Shipping::get_instance()->get_list([
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
'status' => $status,
'order_number' => $order,
'date_from' => $date_from,
'date_to' => $date_to,
'tracking_no' => $tracking_no,
'include_order_context' => true,
]);
$stats = \AKSRT\Payment\Shipping::get_instance()->get_stats();
return $this->success(['data' => array_merge($result, [
'stats' => $stats,
'filters' => [
'status' => $status,
'order_number' => $order,
'date_from' => $date_from,
'date_to' => $date_to,
'tracking_no' => $tracking_no,
],
'options' => [
'status' => \AKSRT\Payment\Shipping::get_status_options(),
'carrier' => \AKSRT\Payment\Shipping::get_carrier_options(),
],
])]);
}
/**
* POST /shop/shipping — 创建发货记录
*/
public function create_shipping(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
$carrier = sanitize_text_field((string) ($params['carrier'] ?? ''));
$tracking_no = sanitize_text_field((string) ($params['tracking_no'] ?? ''));
if ($order_number === '' || $carrier === '' || $tracking_no === '') {
return $this->error('订单号、快递公司和物流单号不能为空');
}
$order = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number)
: null;
if (!$order) {
return $this->error(__('订单不存在', 'aksrt'), 404);
}
$params['operator_id'] = get_current_user_id();
$id = \AKSRT\Payment\Shipping::get_instance()->create($params);
if ($id > 0) {
return $this->success([
'message' => __('发货记录已创建', 'aksrt'),
'data' => ['id' => $id],
], 201);
}
return $this->error(__('创建失败', 'aksrt'));
}
/**
* POST /shop/shipping/{id}/update-status — 更新发货状态
*/
public function update_shipping_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$new_status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
if ($id <= 0 || $new_status === '') {
return $this->error('参数无效');
}
$success = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id());
if ($success) {
return $this->success(['message' => __('状态已更新', 'aksrt')]);
}
return $this->error(__('更新失败', 'aksrt'));
}
public function get_after_sale_detail(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->error('售后系统未启用', 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0) {
return $this->error('参数无效');
}
$record = \AKSRT\Payment\AfterSale::get_instance()->get_with_order_context($id);
if (!$record) {
return $this->error(__('售后工单不存在', 'aksrt'), 404);
}
return $this->success([
'data' => [
'record' => $record,
'timeline' => $this->build_after_sale_timeline($record),
],
]);
}
public function get_shipping_detail(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0) {
return $this->error('参数无效');
}
$record = \AKSRT\Payment\Shipping::get_instance()->get_with_order_context($id);
if (!$record) {
return $this->error(__('发货记录不存在', 'aksrt'), 404);
}
return $this->success([
'data' => [
'record' => $record,
'timeline' => $this->build_shipping_timeline($record),
],
]);
}
public function batch_update_shipping_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$params = $this->get_json_params($request);
$ids = is_array($params['ids'] ?? null) ? $params['ids'] : [];
$status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
if (empty($ids) || $status === '') {
return $this->error('参数无效');
}
$result = \AKSRT\Payment\Shipping::get_instance()->batch_update_status($ids, $status, $note, get_current_user_id());
return $this->success([
'message' => __('批量状态更新已执行', 'aksrt'),
'data' => $result,
]);
}
private function build_shipping_timeline(array $record): array {
$timeline = [];
foreach ([
'created_at' => __('创建发货记录', 'aksrt'),
'updated_at' => __('更新物流状态', 'aksrt'),
'delivered_at' => __('确认签收', 'aksrt'),
] as $field => $label) {
if (!empty($record[$field])) {
$timeline[] = [
'time' => (string) $record[$field],
'label' => $label,
];
}
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
});
return $timeline;
}
private function build_after_sale_timeline(array $record): array {
$timeline = [];
foreach ([
'created_at' => __('提交售后申请', 'aksrt'),
'updated_at' => __('更新售后状态', 'aksrt'),
'resolved_at' => __('售后处理结束', 'aksrt'),
] as $field => $label) {
if (!empty($record[$field])) {
$timeline[] = [
'time' => (string) $record[$field],
'label' => $label,
];
}
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
});
return $timeline;
}
}
@@ -1,136 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerOrderOperations {
/**
* GET /shop/orders — 订单列表,支持 search、status、page、per_page
*/
public function get_orders(\WP_REST_Request $request): \WP_REST_Response {
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
$search = sanitize_text_field((string) $request->get_param('search'));
$status = sanitize_key((string) $request->get_param('status'));
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$args = [
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
];
if ($status !== '' && $status !== 'all') {
$args['status'] = $status;
}
$orders = $repo->get_orders_admin($args, $search);
$total = $repo->get_orders_count($args, $search);
return $this->success([
'data' => [
'items' => $orders,
'total' => $total,
'page' => $page,
'total_pages' => max(1, (int) ceil($total / $per_page)),
],
]);
}
return $this->error('订单仓库不可用', 500);
}
/**
* GET /shop/orders/{order_number} — 单个订单详情(含上下文)
*/
public function get_order(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
return $this->error('订单服务不可用', 500);
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
if (!$order) {
return $this->error('订单不存在', 404);
}
$context = $repo->get_admin_order_context($order_number);
$timeline = [];
if (class_exists('\AKSRT\Shop\Shop')) {
$shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : [];
$after_sale_result = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20]) : ['items' => []];
$after_sale_items = $after_sale_result['items'] ?? [];
$timeline = [];
$date_fields = [
'created_at' => '订单创建',
'paid_at' => '完成支付',
];
foreach ($date_fields as $field => $label) {
if (!empty($order[$field])) {
$timeline[] = ['time' => $order[$field], 'title' => $label, 'type' => 'order'];
}
}
foreach ($shipping_records as $record) {
$timeline[] = [
'time' => $record['created_at'] ?? '',
'title' => '物流更新:' . ($record['status_label'] ?? $record['status'] ?? ''),
'detail' => ($record['carrier_label'] ?? '') . ' / ' . ($record['tracking_no'] ?? ''),
'type' => 'shipping',
];
}
foreach ($after_sale_items as $record) {
$timeline[] = [
'time' => $record['created_at'] ?? '',
'title' => '售后:' . ($record['type_label'] ?? '') . ' — ' . ($record['status_label'] ?? ''),
'detail' => $record['reason'] ?? '',
'type' => 'after_sale',
];
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($b['time'] ?? ''), (string) ($a['time'] ?? ''));
});
}
return $this->success([
'data' => $order,
'context' => $context,
'timeline' => $timeline,
]);
}
/**
* POST /shop/orders/update-status — 更新订单状态
* Canonical field: order_number (前端必须发送 order_number)
*/
public function update_order_status(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
$new_status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
if ($order_number === '' || $new_status === '') {
return $this->error('参数无效');
}
$success = false;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$success = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status, [
'note' => $note,
'force' => true,
]);
}
if ($success) {
return $this->success(['message' => __('订单状态已更新', 'aksrt')]);
}
return $this->error(__('更新失败', 'aksrt'));
}
}
@@ -1,187 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerProductAdminColumns {
/**
* Register custom columns for the aksrt_product admin list table.
*/
public function init_product_admin_columns(): void {
add_filter('manage_edit-aksrt_product_columns', [$this, 'product_columns']);
add_action('manage_aksrt_product_posts_custom_column', [$this, 'render_product_column'], 10, 2);
add_filter('manage_edit-aksrt_product_sortable_columns', [$this, 'sortable_product_columns']);
add_action('quick_edit_custom_box', [$this, 'render_product_quick_edit'], 10, 2);
add_action('bulk_edit_custom_box', [$this, 'render_product_quick_edit'], 10, 2);
add_action('save_post_aksrt_product', [$this, 'save_product_quick_edit'], 10, 2);
add_action('admin_footer-edit.php', [$this, 'product_quick_edit_script']);
}
public function product_columns(array $columns): array {
$new = [];
foreach ($columns as $key => $label) {
$new[$key] = $label;
if ($key === 'title') {
$new['product_price'] = __('价格', 'aksrt');
$new['product_stock'] = __('库存', 'aksrt');
$new['product_sales'] = __('销量', 'aksrt');
$new['product_stats'] = __('综合数据', 'aksrt');
$new['product_author_name'] = __('商家', 'aksrt');
$new['product_discount'] = __('优惠活动', 'aksrt');
}
}
return $new;
}
public function render_product_column(string $column, int $post_id): void {
switch ($column) {
case 'product_price':
$price = get_post_meta($post_id, '_aksrt_price', true);
echo esc_html(number_format_i18n((float) $price, 2));
break;
case 'product_stock':
$stock = get_post_meta($post_id, '_aksrt_stock', true);
$stock_type = get_post_meta($post_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts') {
echo esc_html__('按选项', 'aksrt');
} elseif ((int) $stock === -1) {
echo esc_html__('无限', 'aksrt');
} else {
echo esc_html((string) (int) $stock);
}
break;
case 'product_sales':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_sales', true));
break;
case 'product_stats':
$views = (int) get_post_meta($post_id, '_aksrt_views', true);
$fav = (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
printf(
esc_html__('阅读 %1$d / 收藏 %2$d', 'aksrt'),
$views,
$fav
);
break;
case 'product_author_name':
$post = get_post($post_id);
if ($post) {
$author = get_userdata($post->post_author);
echo esc_html($author ? $author->display_name : '—');
}
break;
case 'product_discount':
$terms = wp_get_object_terms($post_id, 'shop_discount', ['fields' => 'all']);
if (!is_wp_error($terms) && !empty($terms)) {
$term_ids = wp_list_pluck($terms, 'term_id');
$names = wp_list_pluck($terms, 'name');
echo '<span class="aksrt-col-data" data-discount-id="' . esc_attr((string) ($term_ids[0] ?? 0)) . '"></span>';
echo esc_html(implode(', ', $names));
} else {
echo '<span class="aksrt-col-data" data-discount-id="0"></span>—';
}
break;
}
}
public function sortable_product_columns(array $columns): array {
$columns['product_sales'] = 'product_sales';
return $columns;
}
public function render_product_quick_edit(string $column_name, string $post_type): void {
if ($column_name !== 'product_discount' || $post_type !== 'aksrt_product') {
return;
}
wp_nonce_field('aksrt_product_quick_edit', 'aksrt_product_qe_nonce');
$discounts = get_terms([
'taxonomy' => 'shop_discount',
'hide_empty' => false,
]);
?>
<fieldset class="inline-edit-col-right" id="aksrt-product-quick-edit">
<div class="inline-edit-col">
<h4><?php esc_html_e('商品优惠活动', 'aksrt'); ?></h4>
<input type="hidden" name="aksrt_product_qe_flag" value="1">
<label class="alignleft">
<span class="title"><?php esc_html_e('优惠活动', 'aksrt'); ?></span>
<select name="aksrt_qe_discount" class="aksrt-qe-discount">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value="0"><?php esc_html_e('无优惠', 'aksrt'); ?></option>
<?php if (!is_wp_error($discounts)) : ?>
<?php foreach ($discounts as $term) : ?>
<option value="<?php echo esc_attr($term->term_id); ?>"><?php echo esc_html($term->name); ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</label>
</div>
</fieldset>
<?php
}
public function save_product_quick_edit(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_qe_nonce']) || !wp_verify_nonce($_POST['aksrt_product_qe_nonce'], 'aksrt_product_quick_edit')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (!isset($_POST['aksrt_product_qe_flag'])) {
return;
}
$discount_id = isset($_POST['aksrt_qe_discount']) ? sanitize_text_field(wp_unslash($_POST['aksrt_qe_discount'])) : '-1';
if ($discount_id === '-1') {
return;
}
if ($discount_id === '0') {
wp_set_object_terms($post_id, [], 'shop_discount');
} else {
$term_id = (int) $discount_id;
if ($term_id > 0 && term_exists($term_id, 'shop_discount')) {
wp_set_object_terms($post_id, [$term_id], 'shop_discount');
}
}
}
public function product_quick_edit_script(): void {
$screen = get_current_screen();
if (!$screen || $screen->post_type !== 'aksrt_product') {
return;
}
?>
<script>
(function(){
var wpInlineEdit = window.inlineEditPost ? window.inlineEditPost.edit : null;
if (!wpInlineEdit) return;
window.inlineEditPost.edit = function(id) {
wpInlineEdit.apply(this, arguments);
var postId = 0;
if (typeof id === 'object') {
postId = parseInt(this.getId(id));
}
if (!postId) return;
var row = document.getElementById('post-' + postId);
if (!row) return;
var dataEl = row.querySelector('.column-product_discount .aksrt-col-data');
if (!dataEl) return;
var termId = dataEl.getAttribute('data-discount-id') || '0';
var select = document.querySelector('.aksrt-qe-discount');
if (select) {
select.value = termId;
}
};
})();
</script>
<?php
}
}
@@ -1,499 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerProductOperations {
public function get_overview(): \WP_REST_Response {
$product_count = 0;
$post_type_obj = get_post_type_object('aksrt_product');
if ($post_type_obj) {
$counts = wp_count_posts('aksrt_product');
$product_count = (int) ($counts->publish ?? 0);
}
$order_stats = ['total' => 0, 'pending' => 0, 'paid' => 0, 'revenue' => 0];
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order_stats = \AKSRT\Payment\OrderRepository::get_instance()->get_stats();
}
$revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0];
if (class_exists('\AKSRT\Payment\Revenue')) {
$revenue_stats = \AKSRT\Payment\Revenue::get_instance()->get_stats();
}
$gateway_summary = [];
if (class_exists('\AKSRT\Payment\Gateway')) {
$gateway_summary = \AKSRT\Payment\Gateway::get_instance()->get_status_summary();
}
return $this->success([
'data' => [
'enabled' => true,
'product_count' => $product_count,
'order_count' => $order_stats['total'],
'order_pending' => $order_stats['pending'] ?? 0,
'revenue_total' => $order_stats['revenue'] ?? 0,
'revenue_pending' => $revenue_stats['pending'] ?? 0,
'gateways' => $gateway_summary,
],
]);
}
public function get_product_taxonomy(): \WP_REST_Response {
return $this->success([
'data' => [
'categories' => $this->format_product_terms('aksrt_product_cat'),
'tags' => $this->format_product_terms('aksrt_product_tag'),
],
]);
}
/**
* POST /shop/products/taxonomy — create category or tag term.
*
* Body: { taxonomy: 'category'|'tag'|'aksrt_product_cat'|'aksrt_product_tag', name: string, parent?: int }
*/
public function create_product_taxonomy_term(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$raw_tax = sanitize_key((string) ($params['taxonomy'] ?? 'category'));
$name = sanitize_text_field((string) ($params['name'] ?? ''));
$parent = max(0, (int) ($params['parent'] ?? 0));
$taxonomy_map = [
'category' => 'aksrt_product_cat',
'tag' => 'aksrt_product_tag',
'aksrt_product_cat' => 'aksrt_product_cat',
'aksrt_product_tag' => 'aksrt_product_tag',
];
$taxonomy = $taxonomy_map[$raw_tax] ?? '';
if ($taxonomy === '' || !taxonomy_exists($taxonomy)) {
return $this->error(__('无效的分类法', 'aksrt'));
}
if ($name === '') {
return $this->error(__('名称不能为空', 'aksrt'));
}
$existing = term_exists($name, $taxonomy, $taxonomy === 'aksrt_product_cat' ? $parent : 0);
if (is_array($existing) && !empty($existing['term_id'])) {
$term = get_term((int) $existing['term_id'], $taxonomy);
if ($term && !is_wp_error($term)) {
return $this->success([
'message' => __('术语已存在', 'aksrt'),
'data' => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'parent' => (int) $term->parent,
'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag',
'existing' => true,
],
]);
}
}
$args = [];
if ($taxonomy === 'aksrt_product_cat' && $parent > 0) {
$args['parent'] = $parent;
}
$result = wp_insert_term($name, $taxonomy, $args);
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
$term_id = (int) ($result['term_id'] ?? 0);
$term = get_term($term_id, $taxonomy);
if (!$term || is_wp_error($term)) {
return $this->error(__('创建失败', 'aksrt'));
}
return $this->success([
'message' => __('术语已创建', 'aksrt'),
'data' => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'parent' => (int) $term->parent,
'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag',
'existing' => false,
],
], 201);
}
/**
* @return array<int, array{id:int,name:string,slug:string,parent:int,count:int}>
*/
private function format_product_terms(string $taxonomy): array {
if (!taxonomy_exists($taxonomy)) {
return [];
}
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
]);
if (is_wp_error($terms) || !is_array($terms)) {
return [];
}
return array_map(static function (\WP_Term $term): array {
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'parent' => (int) $term->parent,
'count' => (int) $term->count,
];
}, $terms);
}
/**
* GET /shop/products — 商品列表,支持 search、status、page、per_page
*/
public function get_products(\WP_REST_Request $request): \WP_REST_Response {
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
$search = sanitize_text_field((string) $request->get_param('search'));
$status = sanitize_key((string) $request->get_param('status'));
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
if ($status !== '') {
$query_args['post_status'] = $status;
} else {
$query_args['post_status'] = ['publish', 'draft', 'pending'];
}
if ($search !== '') {
$query_args['s'] = $search;
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$id = get_the_ID();
$products[] = [
'id' => $id,
'title' => get_the_title(),
'price' => (float) get_post_meta($id, '_aksrt_price', true),
'image' => get_the_post_thumbnail_url($id, 'medium') ?: '',
'stock' => (int) get_post_meta($id, '_aksrt_stock', true),
'sales' => (int) get_post_meta($id, '_aksrt_sales', true),
'status' => get_post_status(),
'created' => get_the_date('Y-m-d H:i'),
];
}
wp_reset_postdata();
return $this->success([
'data' => [
'items' => $products,
'total' => (int) $query->found_posts,
'total_pages' => (int) $query->max_num_pages,
'page' => $page,
],
]);
}
/**
* GET /shop/products/{id} — 单个商品详情
*/
public function get_product(\WP_REST_Request $request): \WP_REST_Response {
$id = (int) $request->get_param('id');
$post = get_post($id);
if (!$post || $post->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
return $this->success([
'data' => [
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'price' => (float) get_post_meta($post->ID, '_aksrt_price', true),
'image' => (string) (get_post_meta($post->ID, '_aksrt_product_image', true) ?: get_the_post_thumbnail_url($post->ID, 'medium') ?: ''),
'stock' => (int) get_post_meta($post->ID, '_aksrt_stock', true),
'sales' => (int) get_post_meta($post->ID, '_aksrt_sales', true),
'status' => $post->post_status,
'created' => get_the_date('Y-m-d H:i', $post),
'modified' => get_the_modified_date('Y-m-d H:i', $post),
'categories' => wp_get_post_terms($post->ID, 'aksrt_product_cat', ['fields' => 'names']) ?: [],
'tags' => wp_get_post_terms($post->ID, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
'gallery' => get_post_meta($post->ID, '_aksrt_product_gallery', true) ?: [],
'shipping_mode' => (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true),
'shipping_fee' => (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true),
'after_sale_policy' => (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($post->ID, '_aksrt_support_refund', true) !== '0',
],
]);
}
/**
* POST /shop/products — 创建商品
*/
public function create_product(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$title = sanitize_text_field((string) ($params['title'] ?? ''));
$content = wp_kses_post((string) ($params['content'] ?? ''));
$excerpt = sanitize_textarea_field((string) ($params['excerpt'] ?? ''));
$price = (float) ($params['price'] ?? 0);
$stock = (int) ($params['stock'] ?? 0);
$status = sanitize_key((string) ($params['status'] ?? 'publish'));
$image = esc_url_raw((string) ($params['image'] ?? ''));
$gallery = is_array($params['gallery'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['gallery']))) : [];
$shipping_mode = sanitize_key((string) ($params['shipping_mode'] ?? 'express'));
$shipping_fee = max(0, (float) ($params['shipping_fee'] ?? 0));
$after_sale_policy = sanitize_textarea_field((string) ($params['after_sale_policy'] ?? ''));
$support_refund = !isset($params['support_refund']) || !empty($params['support_refund']);
$author_id = max(0, (int) ($params['author_id'] ?? 0));
if ($title === '') {
return $this->error('商品名称不能为空');
}
$valid_statuses = ['publish', 'draft', 'pending'];
if (!in_array($status, $valid_statuses, true)) {
$status = 'draft';
}
$post_id = wp_insert_post([
'post_type' => 'aksrt_product',
'post_title' => $title,
'post_content' => $content,
'post_excerpt' => $excerpt,
'post_status' => $status,
'post_author' => $author_id > 0 ? $author_id : get_current_user_id(),
'meta_input' => [
'_aksrt_price' => max(0, $price),
'_aksrt_stock' => max(0, $stock),
'_aksrt_sales' => 0,
'_aksrt_score' => 0,
'_aksrt_views' => 0,
'_aksrt_favorite_count' => 0,
'_aksrt_product_gallery' => $gallery,
'_aksrt_shipping_mode' => $shipping_mode,
'_aksrt_shipping_fee' => $shipping_fee,
'_aksrt_after_sale_policy' => $after_sale_policy,
'_aksrt_support_refund' => $support_refund ? '1' : '0',
'_aksrt_product_video' => is_array($params['product_video'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['product_video']))) : [],
'_aksrt_product_params' => sanitize_textarea_field((string) ($params['product_params'] ?? '')),
'_aksrt_pay_modo' => sanitize_key((string) ($params['pay_modo'] ?? '0')),
'_aksrt_start_price' => max(0, (float) ($params['start_price'] ?? 0)),
'_aksrt_product_rebate' => sanitize_text_field((string) ($params['product_rebate'] ?? '')),
'_aksrt_limit_buy' => max(-1, min(9999, (int) ($params['limit_buy'] ?? -1))),
'_aksrt_limit_buy_note' => sanitize_text_field((string) ($params['limit_buy_note'] ?? '')),
'_aksrt_stock_type' => sanitize_key((string) ($params['stock_type'] ?? 'all')),
'_aksrt_user_required' => sanitize_textarea_field((string) ($params['user_required'] ?? '')),
'_aksrt_shipping_type' => sanitize_key((string) ($params['shipping_type'] ?? $shipping_mode)),
'_aksrt_shipping_delivery_desc' => sanitize_text_field((string) ($params['shipping_delivery_desc'] ?? '')),
'_aksrt_shipping_fee_type' => sanitize_key((string) ($params['shipping_fee_type'] ?? 'free')),
'_aksrt_shipping_fee_free_amount'=> max(0, (float) ($params['shipping_fee_free_amount'] ?? 0)),
'_aksrt_after_sale_matrix' => sanitize_textarea_field((string) ($params['after_sale_matrix'] ?? '')),
'_aksrt_service' => sanitize_textarea_field((string) ($params['service'] ?? '')),
'_aksrt_single_tab' => sanitize_textarea_field((string) ($params['single_tab'] ?? '')),
'_aksrt_content_layout' => sanitize_key((string) ($params['content_layout'] ?? '')),
'_aksrt_content_show_bg' => sanitize_text_field((string) ($params['content_show_bg'] ?? '')),
'_aksrt_delivery_note' => sanitize_text_field((string) ($params['delivery_note'] ?? '')),
'_aksrt_marketing_headline' => sanitize_text_field((string) ($params['marketing_headline'] ?? '')),
'_aksrt_shipping_tip' => sanitize_text_field((string) ($params['shipping_tip'] ?? '')),
'_aksrt_service_tip' => sanitize_text_field((string) ($params['service_tip'] ?? '')),
],
], true);
if (is_wp_error($post_id)) {
return $this->error($post_id->get_error_message());
}
if (!empty($params['categories']) && is_array($params['categories'])) {
wp_set_object_terms($post_id, $params['categories'], 'aksrt_product_cat');
}
if (!empty($params['tags']) && is_array($params['tags'])) {
wp_set_object_terms($post_id, $params['tags'], 'aksrt_product_tag');
}
if ($image !== '') {
update_post_meta($post_id, '_aksrt_product_image', $image);
}
if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) {
\AKSRT\Shop\Shop::get_instance()->save_product_options((int) $post_id, $params['options']);
}
return $this->success([
'message' => __('商品已创建', 'aksrt'),
'data' => ['id' => $post_id],
], 201);
}
/**
* POST /shop/products/{id} — 更新商品(含上下架)
*/
public function update_product(\WP_REST_Request $request): \WP_REST_Response {
$id = (int) $request->get_param('id');
$post = get_post($id);
if (!$post || $post->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
$params = $this->get_json_params($request);
$update = ['ID' => $id];
if (isset($params['title'])) {
$update['post_title'] = sanitize_text_field($params['title']);
}
if (isset($params['content'])) {
$update['post_content'] = wp_kses_post($params['content']);
}
if (isset($params['excerpt'])) {
$update['post_excerpt'] = sanitize_textarea_field($params['excerpt']);
}
if (isset($params['status'])) {
$new_status = sanitize_key($params['status']);
$valid_statuses = ['publish', 'draft', 'pending', 'trash'];
if (in_array($new_status, $valid_statuses, true)) {
$update['post_status'] = $new_status;
}
}
$meta = [];
if (isset($params['price'])) {
$meta['_aksrt_price'] = max(0, (float) $params['price']);
}
if (isset($params['stock'])) {
$meta['_aksrt_stock'] = max(0, (int) $params['stock']);
}
if (isset($params['gallery']) && is_array($params['gallery'])) {
$meta['_aksrt_product_gallery'] = array_values(array_filter(array_map('esc_url_raw', $params['gallery'])));
}
if (isset($params['shipping_mode'])) {
$meta['_aksrt_shipping_mode'] = sanitize_key((string) $params['shipping_mode']);
}
if (isset($params['shipping_fee'])) {
$meta['_aksrt_shipping_fee'] = max(0, (float) $params['shipping_fee']);
}
if (isset($params['after_sale_policy'])) {
$meta['_aksrt_after_sale_policy'] = sanitize_textarea_field((string) $params['after_sale_policy']);
}
if (isset($params['support_refund'])) {
$meta['_aksrt_support_refund'] = !empty($params['support_refund']) ? '1' : '0';
}
if (isset($params['product_video']) && is_array($params['product_video'])) {
$meta['_aksrt_product_video'] = array_values(array_filter(array_map('esc_url_raw', $params['product_video'])));
}
if (isset($params['product_params'])) {
$meta['_aksrt_product_params'] = sanitize_textarea_field((string) $params['product_params']);
}
if (isset($params['pay_modo'])) {
$meta['_aksrt_pay_modo'] = sanitize_key((string) $params['pay_modo']);
}
if (isset($params['start_price'])) {
$meta['_aksrt_start_price'] = max(0, (float) $params['start_price']);
}
if (isset($params['product_rebate'])) {
$meta['_aksrt_product_rebate'] = sanitize_text_field((string) $params['product_rebate']);
}
if (isset($params['limit_buy'])) {
$meta['_aksrt_limit_buy'] = max(-1, min(9999, (int) $params['limit_buy']));
}
if (isset($params['limit_buy_note'])) {
$meta['_aksrt_limit_buy_note'] = sanitize_text_field((string) $params['limit_buy_note']);
}
if (isset($params['stock_type'])) {
$meta['_aksrt_stock_type'] = sanitize_key((string) $params['stock_type']);
}
if (isset($params['stock_opts']) && is_array($params['stock_opts'])) {
$meta['_aksrt_stock_opts'] = wp_json_encode($params['stock_opts']);
}
if (isset($params['user_required'])) {
$meta['_aksrt_user_required'] = sanitize_textarea_field((string) $params['user_required']);
}
if (isset($params['shipping_type'])) {
$meta['_aksrt_shipping_type'] = sanitize_key((string) $params['shipping_type']);
}
if (isset($params['shipping_delivery_desc'])) {
$meta['_aksrt_shipping_delivery_desc'] = sanitize_text_field((string) $params['shipping_delivery_desc']);
}
if (isset($params['auto_delivery']) && is_array($params['auto_delivery'])) {
$meta['_aksrt_auto_delivery'] = wp_json_encode($params['auto_delivery']);
}
if (isset($params['shipping_fee_type'])) {
$meta['_aksrt_shipping_fee_type'] = sanitize_key((string) $params['shipping_fee_type']);
}
if (isset($params['shipping_fee_free_amount'])) {
$meta['_aksrt_shipping_fee_free_amount'] = max(0, (float) $params['shipping_fee_free_amount']);
}
if (isset($params['after_sale_matrix'])) {
$meta['_aksrt_after_sale_matrix'] = sanitize_textarea_field((string) $params['after_sale_matrix']);
}
if (isset($params['service'])) {
$meta['_aksrt_service'] = sanitize_textarea_field((string) $params['service']);
}
if (isset($params['single_tab'])) {
$meta['_aksrt_single_tab'] = sanitize_textarea_field((string) $params['single_tab']);
}
if (isset($params['content_layout'])) {
$meta['_aksrt_content_layout'] = sanitize_key((string) $params['content_layout']);
}
if (isset($params['content_show_bg'])) {
$meta['_aksrt_content_show_bg'] = sanitize_text_field((string) $params['content_show_bg']);
}
if (isset($params['delivery_note'])) {
$meta['_aksrt_delivery_note'] = sanitize_text_field((string) $params['delivery_note']);
}
if (isset($params['marketing_headline'])) {
$meta['_aksrt_marketing_headline'] = sanitize_text_field((string) $params['marketing_headline']);
}
if (isset($params['shipping_tip'])) {
$meta['_aksrt_shipping_tip'] = sanitize_text_field((string) $params['shipping_tip']);
}
if (isset($params['service_tip'])) {
$meta['_aksrt_service_tip'] = sanitize_text_field((string) $params['service_tip']);
}
if (!empty($meta)) {
$update['meta_input'] = $meta;
}
$result = wp_update_post($update, true);
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
if (isset($params['categories']) && is_array($params['categories'])) {
wp_set_object_terms($id, $params['categories'], 'aksrt_product_cat');
}
if (isset($params['tags']) && is_array($params['tags'])) {
wp_set_object_terms($id, $params['tags'], 'aksrt_product_tag');
}
if (isset($params['image'])) {
update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image']));
}
if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) {
\AKSRT\Shop\Shop::get_instance()->save_product_options($id, $params['options']);
}
return $this->success([
'message' => __('商品已更新', 'aksrt'),
]);
}
}
@@ -1,113 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerVariantExportOperations {
public function get_product_variants(\WP_REST_Request $request): \WP_REST_Response {
$product_id = (int) $request->get_param('id');
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->success(['data' => ['variants' => []]]);
}
$variants = \AKSRT\Shop\Shop::get_instance()->get_variants($product_id);
return $this->success(['data' => ['variants' => $variants]]);
}
public function save_product_variants(\WP_REST_Request $request): \WP_REST_Response {
$product_id = (int) $request->get_param('id');
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
$params = method_exists($this, 'get_json_params')
? $this->get_json_params($request)
: ($request->get_json_params() ?? []);
$variants = $params['variants'] ?? [];
if (!is_array($variants)) {
return $this->error('变体数据格式错误');
}
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城系统未启用', 501);
}
$saved = \AKSRT\Shop\Shop::get_instance()->save_variants($product_id, $variants);
return $this->success(['message' => '变体已保存', 'data' => ['variants' => $saved]]);
}
public function delete_variant(\WP_REST_Request $request): \WP_REST_Response {
$variant_id = (int) $request->get_param('id');
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城系统未启用', 501);
}
$result = \AKSRT\Shop\Shop::get_instance()->delete_variant($variant_id);
if (!$result) {
return $this->error('删除失败', 500);
}
return $this->success(['message' => '变体已删除']);
}
public function export_orders(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$args = [
'status' => sanitize_key((string) $request->get_param('status')),
'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
'date_to' => sanitize_text_field((string) $request->get_param('date_to')),
'limit' => max(1, min(5000, (int) ($request->get_param('limit') ?: 1000))),
];
$result = \AKSRT\Shop\Shop::get_instance()->export_orders_csv($args);
return new \WP_REST_Response($result['csv'], 200, [
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="orders-' . date('Ymd-His') . '.csv"',
'X-Order-Count' => $result['count'],
]);
}
public function confirm_receipt(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\Shop::get_instance()->confirm_receipt(get_current_user_id(), $order_number);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '确认收货失败');
}
return $this->success(['data' => $result['data']]);
}
public function get_after_sale_reasons(\WP_REST_Request $request): \WP_REST_Response {
if (class_exists('\AKSRT\Payment\AfterSale')) {
return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]);
}
return $this->success(['data' => ['refund' => [], 'return' => []]]);
}
public function get_frontend_after_sale_reasons(\WP_REST_Request $request): \WP_REST_Response {
if (class_exists('\AKSRT\Payment\AfterSale')) {
return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]);
}
return $this->success(['data' => ['refund' => [], 'return' => []]]);
}
}
-95
View File
@@ -261,24 +261,6 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_restriction);
// ---------- 13. 商品变体/SKU ----------
$sql_variant = "CREATE TABLE {$prefix}aksrt_product_variant (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
product_id BIGINT UNSIGNED NOT NULL,
sku VARCHAR(64) NOT NULL DEFAULT '',
attributes TEXT DEFAULT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
image VARCHAR(512) NOT NULL DEFAULT '',
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_product (product_id),
KEY idx_sku (sku),
KEY idx_sort (sort_order)
) {$charset_collate};";
dbDelta($sql_variant);
// ── follow ──
$sql_follow = "CREATE TABLE {$prefix}aksrt_follow (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -329,64 +311,6 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_coupon_usage);
// ── after_sale ──
$sql_after_sale = "CREATE TABLE {$prefix}aksrt_after_sale (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL DEFAULT '',
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
type VARCHAR(20) NOT NULL DEFAULT 'refund',
reason TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
note TEXT DEFAULT NULL,
operator_id BIGINT UNSIGNED DEFAULT NULL,
operator_note TEXT DEFAULT NULL,
refund_request_id VARCHAR(64) DEFAULT NULL,
provider VARCHAR(32) NOT NULL DEFAULT '',
provider_refund_id VARCHAR(128) NOT NULL DEFAULT '',
provider_status VARCHAR(24) NOT NULL DEFAULT 'not_started',
refund_effect_status VARCHAR(24) NOT NULL DEFAULT 'not_started',
refund_error TEXT DEFAULT NULL,
refund_attempts INT UNSIGNED NOT NULL DEFAULT 0,
refund_requested_at DATETIME DEFAULT NULL,
provider_updated_at DATETIME DEFAULT NULL,
refund_succeeded_at DATETIME DEFAULT NULL,
refund_effected_at DATETIME DEFAULT NULL,
order_status_before VARCHAR(20) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
updated_at DATETIME DEFAULT NULL,
resolved_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_refund_request (refund_request_id),
KEY idx_order (order_number),
KEY idx_user (user_id),
KEY idx_status (status),
KEY idx_type (type),
KEY idx_provider_status (provider_status),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_after_sale);
// ── order_shipping ──
$sql_shipping = "CREATE TABLE {$prefix}aksrt_order_shipping (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL DEFAULT '',
carrier VARCHAR(60) NOT NULL DEFAULT '',
tracking_no VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'shipped',
note TEXT DEFAULT NULL,
operator_id BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
updated_at DATETIME DEFAULT NULL,
delivered_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_order (order_number),
KEY idx_status (status),
KEY idx_tracking (tracking_no),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_shipping);
// ── charge_card (zibll-style typed card_pass) ──
$sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -461,25 +385,6 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_order_items);
// ── card_key 卡密表 (§6.2 自动发货) ──
$sql_card_key = "CREATE TABLE {$prefix}aksrt_card_key (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
options_key VARCHAR(255) NOT NULL DEFAULT '',
card_code TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'available',
order_number VARCHAR(32) NOT NULL DEFAULT '',
used_by BIGINT UNSIGNED DEFAULT NULL,
used_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (id),
KEY idx_product (product_id),
KEY idx_options (options_key(50)),
KEY idx_status (status),
KEY idx_order (order_number)
) {$charset_collate};";
dbDelta($sql_card_key);
self::migrate_identity_tables($prefix, $charset_collate);
}
}
+4 -8
View File
@@ -31,16 +31,12 @@ trait DBMigratorPart3 {
'report' => 'aksrt_report',
'follow' => 'aksrt_follow',
'coupon' => 'aksrt_coupon',
'after_sale' => 'aksrt_after_sale',
'order_shipping' => 'aksrt_order_shipping',
'charge_card' => 'aksrt_charge_card',
'rebate_log' => 'aksrt_rebate_log',
'coupon_usage' => 'aksrt_coupon_usage',
'card_key' => 'aksrt_card_key',
'user_restriction' => 'aksrt_user_restriction',
'product_variant' => 'aksrt_product_variant',
'identity_application' => 'aksrt_identity_application',
'identity_event' => 'aksrt_identity_event',
'coupon_usage' => 'aksrt_coupon_usage',
'user_restriction' => 'aksrt_user_restriction',
'identity_application' => 'aksrt_identity_application',
'identity_event' => 'aksrt_identity_event',
];
if (!isset($allowed[$name])) {
return '';
-2
View File
@@ -30,7 +30,6 @@ class DomainModel {
public const ORDER_PRODUCT_RECHARGE = 'recharge';
/** Cash purchase of points (zibll order_type=9). */
public const ORDER_PRODUCT_POINTS_BUY = 'points_buy';
public const ORDER_PRODUCT_SHOP = 'shop_product';
/**
* Human labels for aksrt_order.product_type (front-end order center).
@@ -39,7 +38,6 @@ class DomainModel {
*/
public static function order_product_type_labels(): array {
return [
self::ORDER_PRODUCT_SHOP => __('商城商品', 'aksrt'),
self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'),
self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'),
self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'),
+2 -91
View File
@@ -635,13 +635,11 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
}
}
// 交易中心共享壳层(购物车 / 订单 / 收益)
// Virtual rewrite routes (aksrt_orders / aksrt_cart / aksrt_revenue) also count.
// 交易中心共享壳层(订单 / 收益)
// Virtual rewrite routes (aksrt_orders / aksrt_revenue) also count.
$is_commerce_center = is_page_template('templates/pages/page-orders.php')
|| is_page_template('templates/pages/page-cart.php')
|| is_page_template('templates/pages/page-revenue.php')
|| (bool) get_query_var('aksrt_orders')
|| (bool) get_query_var('aksrt_cart')
|| (bool) get_query_var('aksrt_revenue');
if ($is_commerce_center) {
$commerce_css = get_template_directory() . '/assets/css/routes/commerce.css';
@@ -735,93 +733,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
}
}
// Product archive pages (category / tag)
if (is_tax('aksrt_product_cat') || is_tax('aksrt_product_tag')) {
wp_enqueue_style(
'aksrt-product-archive',
$base_uri . '/assets/css/frontend/product-archive.css',
['aksrt-tokens'],
$version
);
}
// Product detail page
if (is_page_template('templates/pages/page-product.php')) {
wp_enqueue_style(
'aksrt-route-product',
$base_uri . '/assets/css/routes/product.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/product.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css')
: $version
);
wp_enqueue_script(
'aksrt-product-purchase',
$base_uri . '/assets/js/product-purchase.js',
['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/product-purchase.js')
? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js')
: $version,
true
);
}
// Cart page
if (is_page_template('templates/pages/page-cart.php') || (bool) get_query_var('aksrt_cart')) {
wp_enqueue_style(
'aksrt-route-cart',
$base_uri . '/assets/css/routes/cart.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/cart.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/cart.css')
: $version
);
wp_enqueue_script(
'aksrt-cart-checkout',
$base_uri . '/assets/js/cart-checkout.js',
['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/cart-checkout.js')
? (string) filemtime(get_template_directory() . '/assets/js/cart-checkout.js')
: $version,
true
);
}
// Product edit page
if (is_page_template('templates/pages/page-product-edit.php')) {
wp_enqueue_style(
'aksrt-route-product',
$base_uri . '/assets/css/routes/product.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/product.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css')
: $version
);
wp_enqueue_script(
'aksrt-product-purchase',
$base_uri . '/assets/js/product-purchase.js',
['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/product-purchase.js')
? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js')
: $version,
true
);
}
// 商品编辑页共用 page-edit.js
if (is_page_template('templates/pages/page-product-edit.php')) {
$page_edit_js = get_template_directory() . '/assets/js/page-edit.js';
if (file_exists($page_edit_js)) {
wp_enqueue_script(
'aksrt-page-edit',
$base_uri . '/assets/js/page-edit.js',
['aksrt-script'],
(string) filemtime($page_edit_js),
true
);
}
}
// Unified share-data builder — single payload assembler for every share
// surface (dropdown, copy, WeChat, poster). Loaded on any singular view.
if (is_singular()) {
-1
View File
@@ -264,7 +264,6 @@ trait ScriptsPart2 {
*/
private function resolve_frontend_routes(): array {
return [
'cart' => $this->resolve_template_page_url('templates/pages/page-cart.php', '/cart/'),
'orders' => $this->resolve_template_page_url('templates/pages/page-orders.php', '/orders/'),
'resources' => home_url('/resources/'),
];
-105
View File
@@ -31,20 +31,6 @@ class Setup {
add_action('init', [$this, 'register_nav_menus'], 0);
add_action('init', [$this, 'register_post_types'], 1);
add_action('init', [$this, 'register_meta_fields'], 5);
add_action('init', [$this, 'register_shop_rewrite_rules'], 20);
add_filter('query_vars', [$this, 'register_shop_query_vars']);
add_action('template_redirect', [$this, 'shop_template_redirect']);
// Shop fulfillment hook (§6.5 R68)
if (class_exists('\AKSRT\Shop\Shop')) {
add_action('aksrt_order_paid', [\AKSRT\Shop\Shop::get_instance(), 'handle_order_paid'], 10, 2);
}
// Auto-confirm receipt cron (§6.5 R68)
if (!wp_next_scheduled('aksrt_auto_confirm_receipt')) {
wp_schedule_event(time(), 'hourly', 'aksrt_auto_confirm_receipt');
}
if (class_exists('\AKSRT\Shop\Shop')) {
add_action('aksrt_auto_confirm_receipt', [\AKSRT\Shop\Shop::get_instance(), 'run_auto_confirm_cron']);
}
// 文章浏览量统计(单次访问)
add_action('wp', 'aksrt_set_post_views');
// 隐藏默认WordPress管理菜单
@@ -110,97 +96,6 @@ class Setup {
* 通过 _aksrt_is_resource meta 标记是否为资源。
*/
public function register_post_types(): void {
// Shop rewrite settings (safe fallback if settings not yet loaded)
$shop_rewrite = [];
if (class_exists('\AKSRT\Settings\Settings')) {
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$shop_rewrite = $shop['rewrite'] ?? [];
}
$product_slug = sanitize_key((string) ($shop_rewrite['product_slug'] ?? 'product'));
$suffix_html = !empty($shop_rewrite['suffix_html']);
// Shop admin visibility: align with zibll native sidebar when shop is enabled.
$shop_enabled = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$shop_settings = \AKSRT\Settings\Settings::get_shop_settings();
$shop_enabled = !empty($shop_settings['enabled']);
}
// Always register UI in admin so settings/tools remain reachable; hide menu when disabled.
$shop_show_ui = true;
$shop_show_menu = is_admin() ? $shop_enabled : false;
// 商品 CPT — native WP sidebar (zibll: shop_product @ pos 35)
register_post_type('aksrt_product', [
'labels' => [
'name' => __('商城', 'aksrt'),
'singular_name' => __('商品', 'aksrt'),
'add_new' => __('新增商品', 'aksrt'),
'add_new_item' => __('新增商品', 'aksrt'),
'edit_item' => __('编辑商品', 'aksrt'),
'new_item' => __('新商品', 'aksrt'),
'view_item' => __('查看商品', 'aksrt'),
'search_items' => __('搜索商品', 'aksrt'),
'not_found' => __('未找到商品', 'aksrt'),
'not_found_in_trash' => __('回收站中无商品', 'aksrt'),
'all_items' => __('所有商品', 'aksrt'),
'menu_name' => __('商城', 'aksrt'),
],
'public' => true,
'publicly_queryable' => true,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 35,
'menu_icon' => 'dashicons-store',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'author', 'revisions'],
'rewrite' => ['slug' => $product_slug, 'with_front' => false],
'capability_type' => 'post',
'map_meta_cap' => true,
]);
// 商品分类 taxonomy(独立于文章 category)
register_taxonomy('aksrt_product_cat', 'aksrt_product', [
'labels' => [
'name' => __('商品分类', 'aksrt'),
'singular_name' => __('商品分类', 'aksrt'),
'search_items' => __('搜索商品分类', 'aksrt'),
'all_items' => __('所有商品分类', 'aksrt'),
'parent_item' => __('父级分类', 'aksrt'),
'edit_item' => __('编辑分类', 'aksrt'),
'add_new_item' => __('新增分类', 'aksrt'),
'menu_name' => __('商品分类', 'aksrt'),
],
'public' => true,
'hierarchical' => true,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'product-cat', 'with_front' => false],
]);
// 商品标签 taxonomy(独立于文章 post_tag)
register_taxonomy('aksrt_product_tag', 'aksrt_product', [
'labels' => [
'name' => __('商品标签', 'aksrt'),
'singular_name' => __('商品标签', 'aksrt'),
'search_items' => __('搜索商品标签', 'aksrt'),
'all_items' => __('所有商品标签', 'aksrt'),
'edit_item' => __('编辑标签', 'aksrt'),
'add_new_item' => __('新增标签', 'aksrt'),
'menu_name' => __('特色标签', 'aksrt'),
],
'public' => true,
'hierarchical' => false,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'product-tag', 'with_front' => false],
]);
// 项目展示
register_post_type('project', [
'labels' => [
-24
View File
@@ -382,30 +382,6 @@ trait SetupPart1 {
'after_title' => $title_after,
]);
// ── Shop widget areas (§6.9) ──
$shop_widgets = [
'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'],
'shop_home_sidebar' => ['商城首页 · 侧栏', '商城首页侧栏区域。'],
'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'],
'shop_product_sidebar' => ['商品详情页 · 侧栏', '商品详情页侧栏区域(PC 端)。'],
'shop_product_bottom_fluid' => ['商品详情页 · 底部通栏', '商品详情页底部全宽区域。'],
'shop_cart_top_fluid' => ['购物车页 · 顶部通栏', '购物车页顶部全宽区域。'],
'shop_cart_bottom_fluid' => ['购物车页 · 底部通栏', '购物车页底部全宽区域。'],
'shop_order_top_fluid' => ['订单页 · 顶部通栏', '订单中心顶部全宽。'],
'shop_order_bottom_fluid' => ['订单页 · 底部通栏', '订单中心底部全宽。'],
];
foreach ($shop_widgets as $sid => $meta) {
register_sidebar([
'id' => $sid,
'name' => $meta[0],
'description' => $meta[1],
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
}
// ── Per-page dynamic sidebars (zibll zib_page_register_sidebar parity) ──
// Pages that opt in via meta `widgets_register=1` + `widgets_register_container`
// (a serialized list of area keys) get page_{key}_{ID} sidebars registered, and
-92
View File
@@ -23,97 +23,5 @@ trait SetupPart2 {
// 移除仪表盘中的不必要组件
remove_meta_box('dashboard_primary', 'dashboard', 'normal');
remove_meta_box('dashboard_quick_press', 'dashboard', 'side');
// Separators around shop (35) and forum (40) — match zibll admin_menu_separator.
global $menu;
if (!is_array($menu)) {
return;
}
if (!isset($menu[34])) {
$menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator'];
}
}
/**
* Register shop rewrite rules for:
* - /shop/{ID}.html — product detail (ID-based, .html suffix)
* - /shop/{ID}.html/comment-{page} — product comment pagination
* - /{cart_slug}/ — cart page (fixed alias)
*
* Runs at init priority 20 (after CPT registration at priority 1).
*/
public function register_shop_rewrite_rules(): void {
if (!class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
if (empty($shop['enabled'])) {
return;
}
$rw = $shop['rewrite'] ?? [];
$product_slug = sanitize_key((string) ($rw['product_slug'] ?? 'product'));
$cart_slug = sanitize_key((string) ($rw['cart_slug'] ?? 'cart'));
$suffix_html = !empty($rw['suffix_html']);
// Product ID-based .html routes: /shop/{ID}.html
if ($suffix_html) {
add_rewrite_rule(
'^shop/(\d+)\.html/comment-page-([0-9]{1,})/?$',
'index.php?post_type=aksrt_product&p=$matches[1]&cpage=$matches[2]',
'top'
);
add_rewrite_rule(
'^shop/(\d+)\.html/?$',
'index.php?post_type=aksrt_product&p=$matches[1]',
'top'
);
// Slug-based .html: /{product_slug}/{slug}.html
add_rewrite_rule(
'^' . preg_quote($product_slug) . '/([^/]+)\.html/comment-page-([0-9]{1,})/?$',
'index.php?post_type=aksrt_product&aksrt_product=$matches[1]&cpage=$matches[3]',
'top'
);
add_rewrite_rule(
'^' . preg_quote($product_slug) . '/([^/]+)\.html/?$',
'index.php?post_type=aksrt_product&aksrt_product=$matches[1]',
'top'
);
}
// Cart page: /{cart_slug}/
add_rewrite_rule(
'^' . preg_quote($cart_slug) . '/?$',
'index.php?pagename=' . $cart_slug,
'top'
);
}
/**
* Register shop-specific query vars.
*/
public function register_shop_query_vars(array $vars): array {
$vars[] = 'product_id';
$vars[] = 'shop_action';
return $vars;
}
/**
* Template redirect for shop pages.
*
* - Single aksrt_product: serve page-product.php template
* - Cart page: serve page-cart.php template
*/
public function shop_template_redirect(): void {
// Single product: if the main query resolves to aksrt_product, use our template
if (is_singular('aksrt_product')) {
$tpl = AKSRT_DIR . '/templates/pages/page-product.php';
if (file_exists($tpl)) {
// Set product_id query var for template consumption
set_query_var('product_id', get_queried_object_id());
include $tpl;
exit;
}
}
}
}
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -159,7 +159,7 @@ class Coupon {
}
// Product / content scope check.
// Supports shop product cats and content post cats.
// Supports product 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)) {
@@ -168,11 +168,7 @@ class Coupon {
$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'];
$taxonomies = ['aksrt_product_cat', 'category'];
}
if ($scope_type === 'include' && empty(array_intersect($product_ids, $scope_ids))) {
-167
View File
@@ -1,167 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
class Discount {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get all active discount campaigns for a product.
*
* @param int $product_id
* @return array<int, array> Sorted by priority desc.
*/
public function get_active_discounts(int $product_id): array {
$terms = wp_get_object_terms($product_id, 'shop_discount', ['fields' => 'all']);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
$now = current_time('timestamp');
$active = [];
foreach ($terms as $term) {
$meta = \AKSRT\Shop\TermMeta::get_discount_meta($term->term_id);
// Time limit check
if (!empty($meta['start']) && strtotime($meta['start']) > $now) {
continue;
}
if (!empty($meta['end']) && strtotime($meta['end']) < $now) {
continue;
}
$meta['term_id'] = $term->term_id;
$meta['name'] = $term->name;
$active[] = $meta;
}
// Sort by priority desc
usort($active, function ($a, $b) {
return ((int) $b['priority']) - ((int) $a['priority']);
});
return $active;
}
/**
* Calculate discount for a single line item.
*
* @param float $unit_price Unit price
* @param int $quantity Quantity
* @param int $product_id Product ID
* @param int $user_id User ID (for user_limit checks)
* @return array{
* original: float,
* discount_amount: float,
* final_price: float,
* gifts: array,
* applied: array<int, array>
* }
*/
public function calculate(float $unit_price, int $quantity, int $product_id, int $user_id = 0): array {
$original = $unit_price * $quantity;
$discount_amount = 0.0;
$gifts = [];
$applied = [];
$discounts = $this->get_active_discounts($product_id);
foreach ($discounts as $d) {
// User limit check
if (!empty($d['user_limit']) && $user_id > 0) {
if (!$this->check_user_limit($d['user_limit'], $user_id)) {
continue;
}
}
// Price limit check
if ((float) $d['price_limit'] > 0 && $original < (float) $d['price_limit']) {
continue;
}
$type = $d['type'] ?? 'reduction';
$value = (float) ($d['value'] ?? 0);
switch ($type) {
case 'reduction':
// 立减
$discount_amount += min($value * $quantity, $original - $discount_amount);
break;
case 'discount':
// 打折 (value = 折扣系数 e.g. 8.5 = 85折)
if ($value > 0 && $value < 10) {
$rate = $value / 10.0;
$save = ($original - $discount_amount) * (1 - $rate);
$discount_amount += $save;
}
break;
case 'gift':
// 买赠 — collect gift config, no price reduction
if (!empty($d['gift_config'])) {
$gift_data = json_decode($d['gift_config'], true);
if (is_array($gift_data)) {
$gifts[] = $gift_data;
}
}
break;
}
$applied[] = [
'term_id' => $d['term_id'],
'name' => $d['name'] ?? '',
'type' => $type,
'value' => $value,
];
}
$discount_amount = min($discount_amount, $original);
return [
'original' => $original,
'discount_amount' => round($discount_amount, 2),
'final_price' => round($original - $discount_amount, 2),
'gifts' => $gifts,
'applied' => $applied,
];
}
/**
* Check if a user meets the user_limit requirement.
*
* @param string $limit 'vip' / 'vip_2' / 'auth'
* @param int $user_id
* @return bool
*/
private function check_user_limit(string $limit, int $user_id): bool {
if (empty($limit)) {
return true;
}
switch ($limit) {
case 'vip':
return function_exists('aksrt_is_vip') ? aksrt_is_vip($user_id) : false;
case 'vip_2':
return function_exists('aksrt_is_vip2') ? aksrt_is_vip2($user_id) : false;
case 'auth':
return function_exists('aksrt_is_verified') ? aksrt_is_verified($user_id) : false;
default:
return true;
}
}
}
+10 -21
View File
@@ -81,17 +81,10 @@ class OrderCenterRest {
}
$payment_method = sanitize_key((string) ($params['payment_method'] ?? ''));
if (class_exists(UserOrders::class)) {
$result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->pay_order(
get_current_user_id(),
$order_number,
$payment_method
);
} else {
if (!class_exists(UserOrders::class)) {
return $this->error(__('订单服务不可用', 'aksrt'), 500);
}
}
$result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400);
@@ -103,13 +96,10 @@ class OrderCenterRest {
public function cancel(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (class_exists(UserOrders::class)) {
$result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number);
} else {
if (!class_exists(UserOrders::class)) {
return $this->error(__('订单服务不可用', 'aksrt'), 500);
}
}
$result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('取消订单失败', 'aksrt')), 400);
@@ -122,11 +112,10 @@ class OrderCenterRest {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$order = null;
if (class_exists(UserOrders::class)) {
$order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
}
if (!class_exists(UserOrders::class)) {
return $this->error(__('订单服务不可用', 'aksrt'), 500);
}
$order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
if (!$order) {
return $this->error(__('订单不存在', 'aksrt'), 404);
+5 -74
View File
@@ -63,18 +63,9 @@ trait OrderRepositoryAdmin {
}
$resolved_order_numbers = array_keys($orders);
$shipping_map = class_exists('AKSRT\\Payment\\Shipping')
? Shipping::get_instance()->get_summary_map($resolved_order_numbers)
: [];
$after_sale_map = class_exists('AKSRT\\Payment\\AfterSale')
? AfterSale::get_instance()->get_summary_map($resolved_order_numbers)
: [];
foreach ($orders as $order_number => $order) {
$contexts[$order_number] = $this->build_admin_order_context($order, [
'shipping_summary' => $shipping_map[$order_number] ?? [],
'after_sale_summary' => $after_sale_map[$order_number] ?? [],
]);
$contexts[$order_number] = $this->build_admin_order_context($order, []);
}
return $contexts;
@@ -130,64 +121,6 @@ trait OrderRepositoryAdmin {
/**
* 管理后台订单列表 — 支持 search(order_number / product_type 模糊匹配)
*/
/**
* Paid orders that still need shipping (no shipping row yet).
*
* @return array{items: array<int,array<string,mixed>>, total: int, page: int, max_pages: int}
*/
public function get_pending_shipment_orders(array $args = []): array {
global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? ''));
$order_table = $this->table();
// Must match Shipping::table() key.
$shipping_table = class_exists('\AKSRT\Core\DBMigrator')
? \AKSRT\Core\DBMigrator::table('order_shipping')
: $wpdb->prefix . 'aksrt_order_shipping';
$where = ["o.status = %s"];
$params = ['paid'];
if ($search !== '') {
$where[] = '(o.order_number LIKE %s OR o.product_type LIKE %s)';
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
}
// NOT EXISTS avoids ONLY_FULL_GROUP_BY issues from LEFT JOIN + GROUP BY.
$where[] = "NOT EXISTS (
SELECT 1 FROM {$shipping_table} s
WHERE s.order_number = o.order_number
)";
$where_sql = 'WHERE ' . implode(' AND ', $where);
$count_sql = "SELECT COUNT(*) FROM {$order_table} o {$where_sql}";
$total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
$sql = "SELECT o.*
FROM {$order_table} o
{$where_sql}
ORDER BY COALESCE(o.paid_at, o.created_at) DESC, o.id DESC
LIMIT %d OFFSET %d";
$query_params = $params;
$query_params[] = $limit;
$query_params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
$items = array_map([$this, 'normalize_row'], (array) $rows);
return [
'items' => $items,
'total' => $total,
'page' => (int) floor($offset / $limit) + 1,
'max_pages' => (int) ceil(max(1, $total) / $limit),
];
}
public function get_orders_admin(array $args = [], string $search = ''): array {
global $wpdb;
@@ -499,8 +432,6 @@ trait OrderRepositoryAdmin {
$discount = (float) ($meta['discount'] ?? 0);
$amount = (float) ($order['amount'] ?? 0);
$subtotal = $this->calculate_order_subtotal($items, max(0, $amount + $discount - $shipping_fee));
$shipping_summary = is_array($related['shipping_summary'] ?? null) ? $related['shipping_summary'] : [];
$after_sale_summary = is_array($related['after_sale_summary'] ?? null) ? $related['after_sale_summary'] : [];
$address['full_text'] = $this->build_address_text($address);
@@ -534,12 +465,12 @@ trait OrderRepositoryAdmin {
'fee' => $shipping_fee,
],
'shipping' => [
'count' => (int) ($shipping_summary['count'] ?? 0),
'latest' => is_array($shipping_summary['latest'] ?? null) ? $shipping_summary['latest'] : [],
'count' => 0,
'latest' => [],
],
'after_sale' => [
'count' => (int) ($after_sale_summary['count'] ?? 0),
'latest' => is_array($after_sale_summary['latest'] ?? null) ? $after_sale_summary['latest'] : [],
'count' => 0,
'latest' => [],
],
'coupon' => $coupon,
'pricing' => [
-371
View File
@@ -1,371 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
class Shipping {
use ShippingPart1;
use ShippingPart2;
private static $instance = null;
const STATUS_SHIPPED = 'shipped';
const STATUS_IN_TRANSIT = 'in_transit';
const STATUS_DELIVERED = 'delivered';
const STATUS_RETURNED = 'returned';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function table(): string {
return \AKSRT\Core\DBMigrator::table('order_shipping');
}
/**
* 创建发货记录
*/
public function create(array $data): int {
global $wpdb;
$order_number = sanitize_text_field((string) ($data['order_number'] ?? ''));
$carrier = sanitize_text_field((string) ($data['carrier'] ?? ''));
$tracking_no = sanitize_text_field((string) ($data['tracking_no'] ?? ''));
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
$operator_id = (int) ($data['operator_id'] ?? 0);
if ($order_number === '' || $carrier === '' || $tracking_no === '') {
return 0;
}
$result = $wpdb->insert($this->table(), [
'order_number' => $order_number,
'carrier' => $carrier,
'tracking_no' => $tracking_no,
'status' => self::STATUS_SHIPPED,
'note' => $note,
'operator_id' => $operator_id ?: null,
'created_at' => current_time('mysql'),
]);
$insert_id = $result ? (int) $wpdb->insert_id : 0;
if ($insert_id > 0) {
$this->sync_order_status($order_number, self::STATUS_SHIPPED, sprintf(
__('发货记录已创建:%1$s %2$s', 'aksrt'),
$this->get_carrier_label($carrier),
$tracking_no
));
// R68: Email notification on shipping creation (§6.6)
$this->send_shipping_notification($order_number, $carrier, $tracking_no, $note);
}
return $insert_id;
}
/**
* 获取发货记录列表
*/
public function get_list(array $args = []): array {
global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$include_order_context = !empty($args['include_order_context']);
$status = sanitize_key((string) ($args['status'] ?? ''));
$order_number = sanitize_text_field((string) ($args['order_number'] ?? ''));
$date_from = sanitize_text_field((string) ($args['date_from'] ?? ''));
$date_to = sanitize_text_field((string) ($args['date_to'] ?? ''));
$tracking_no = sanitize_text_field((string) ($args['tracking_no'] ?? ''));
$where = [];
$params = [];
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
if ($order_number !== '') {
$where[] = 'order_number = %s';
$params[] = $order_number;
}
if ($tracking_no !== '') {
$where[] = 'tracking_no LIKE %s';
$params[] = '%' . $wpdb->esc_like($tracking_no) . '%';
}
if ($date_from !== '') {
$where[] = 'created_at >= %s';
$params[] = $date_from . ' 00:00:00';
}
if ($date_to !== '') {
$where[] = 'created_at <= %s';
$params[] = $date_to . ' 23:59:59';
}
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql);
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
$items = array_map([$this, 'decorate_record'], (array) $items);
if ($include_order_context && !empty($items) && class_exists('AKSRT\\Payment\\OrderRepository')) {
$contexts = OrderRepository::get_instance()->get_admin_order_context_map(array_column($items, 'order_number'));
foreach ($items as &$item) {
$item['order_context'] = $contexts[(string) ($item['order_number'] ?? '')] ?? [];
}
unset($item);
}
return [
'items' => $items ?: [],
'total' => $total,
'page' => floor($offset / $limit) + 1,
'max_pages' => ceil($total / $limit),
];
}
/**
* 获取单个发货记录
*/
public function get(int $id): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE id = %d",
$id
), ARRAY_A);
return $row ? $this->decorate_record($row) : null;
}
/**
* 获取订单的发货记录
*/
public function get_by_order(string $order_number): array {
global $wpdb;
$items = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE order_number = %s ORDER BY created_at DESC",
$order_number
), ARRAY_A);
return array_map([$this, 'decorate_record'], (array) $items);
}
/**
* 获取单个发货记录并附带订单上下文。
*/
public function get_with_order_context(int $id): ?array {
$record = $this->get($id);
if (!$record) {
return null;
}
$record['order_context'] = class_exists('AKSRT\\Payment\\OrderRepository')
? OrderRepository::get_instance()->get_admin_order_context((string) ($record['order_number'] ?? ''))
: [];
return $record;
}
/**
* @param array<int, string> $order_numbers
* @return array<string, array<string, mixed>>
*/
public function get_summary_map(array $order_numbers): array {
global $wpdb;
$order_numbers = array_values(array_unique(array_filter(array_map(
static function ($order_number): string {
return sanitize_text_field((string) $order_number);
},
$order_numbers
))));
if (empty($order_numbers)) {
return [];
}
$map = [];
foreach ($order_numbers as $order_number) {
$map[$order_number] = [
'count' => 0,
'latest' => [],
];
}
$placeholders = implode(', ', array_fill(0, count($order_numbers), '%s'));
$sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders}) ORDER BY created_at DESC, id DESC";
$rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A);
foreach ((array) $rows as $row) {
$record = $this->decorate_record($row);
$order_number = (string) ($record['order_number'] ?? '');
if (!isset($map[$order_number])) {
continue;
}
$map[$order_number]['count']++;
if (empty($map[$order_number]['latest'])) {
$map[$order_number]['latest'] = $this->build_summary_item($record);
}
}
return $map;
}
public function batch_update_status(array $ids, string $new_status, string $note = '', int $operator_id = 0): array {
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
$result = [
'updated_ids' => [],
'failed' => [],
];
foreach ($ids as $id) {
$record = $this->get($id);
if (!$record) {
$result['failed'][] = [
'id' => $id,
'reason' => __('发货记录不存在', 'aksrt'),
];
continue;
}
if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) {
$result['failed'][] = [
'id' => $id,
'reason' => __('当前状态不允许批量更新到目标状态', 'aksrt'),
];
continue;
}
if ($this->update_status($id, $new_status, $note, $operator_id)) {
$result['updated_ids'][] = $id;
} else {
$result['failed'][] = [
'id' => $id,
'reason' => __('状态更新失败', 'aksrt'),
];
}
}
return $result;
}
/**
* 更新发货状态
*/
public function update_status(int $id, string $new_status, string $note = '', int $operator_id = 0): bool {
global $wpdb;
$record = $this->get($id);
if (!$record) {
return false;
}
$valid = [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED];
if (!in_array($new_status, $valid, true)) return false;
if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) {
return false;
}
$update = [
'status' => $new_status,
'updated_at' => current_time('mysql'),
];
if ($note !== '') {
$update['note'] = sanitize_textarea_field($note);
}
if ($operator_id > 0) {
$update['operator_id'] = $operator_id;
}
if ($new_status === self::STATUS_DELIVERED) {
$update['delivered_at'] = current_time('mysql');
}
$updated = (bool) $wpdb->update($this->table(), $update, ['id' => $id]);
if ($updated) {
$this->sync_order_status(
(string) ($record['order_number'] ?? ''),
$new_status,
$note !== '' ? $note : sprintf(__('物流状态更新为:%s', 'aksrt'), $this->get_status_label($new_status))
);
}
return $updated;
}
/**
* 获取统计
*/
public function get_stats(): array {
global $wpdb;
$table = $this->table();
$counts = $wpdb->get_results(
"SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status",
ARRAY_A
);
$counts = array_column($counts, 'cnt', 'status');
$stats = [
'total' => 0,
'shipped' => 0,
'in_transit' => 0,
'delivered' => 0,
'returned' => 0,
];
foreach ($counts as $status => $cnt) {
if (isset($stats[$status])) {
$stats[$status] = (int) $cnt;
}
$stats['total'] += (int) $cnt;
}
return $stats;
}
/**
* 获取常用快递公司列表
*/
public static function get_carrier_options(): array {
return [
['value' => 'sf', 'label' => '顺丰速运'],
['value' => 'yd', 'label' => '韵达快递'],
['value' => 'zt', 'label' => '中通快递'],
['value' => 'yt', 'label' => '圆通速递'],
['value' => 'sto', 'label' => '申通快递'],
['value' => 'jd', 'label' => '京东物流'],
['value' => 'ems', 'label' => 'EMS'],
['value' => 'db', 'label' => '德邦快递'],
['value' => 'yunda', 'label' => '韵达快递'],
['value' => 'other', 'label' => '其他'],
];
}
public static function get_status_options(): array {
return [
['value' => self::STATUS_SHIPPED, 'label' => __('已发货', 'aksrt')],
['value' => self::STATUS_IN_TRANSIT, 'label' => __('运输中', 'aksrt')],
['value' => self::STATUS_DELIVERED, 'label' => __('已签收', 'aksrt')],
['value' => self::STATUS_RETURNED, 'label' => __('已退回', 'aksrt')],
];
}
/**
* Send email notification to order customer when shipping record is created.
*/
}
-371
View File
@@ -1,371 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
trait ShippingPart1 {
private function send_shipping_notification(string $order_number, string $carrier, string $tracking_no, string $note = ''): void {
if (!class_exists('AKSRT\\Payment\\OrderRepository')) {
return;
}
$order = OrderRepository::get_instance()->get($order_number);
if (!$order) {
return;
}
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) {
return;
}
$user = get_userdata($user_id);
if (!$user || empty($user->user_email)) {
return;
}
$carrier_label = $this->get_carrier_label($carrier);
$site_name = get_bloginfo('name');
$subject = sprintf(__('[%s] 您的订单已发货', 'aksrt'), $site_name);
$body = sprintf(
__('您好 %1$s,\n\n您的订单 %2$s 已发货。\n\n物流公司:%3$s\n运单号:%4$s\n%5$s\n\n如有疑问请联系客服。\n\n%s', 'aksrt'),
$user->display_name,
$order_number,
$carrier_label,
$tracking_no,
$note !== '' ? sprintf(__('备注:%s', 'aksrt'), $note) : '',
$site_name
);
wp_mail($user->user_email, $subject, $body);
}
private function decorate_record(array $record): array {
$record['id'] = (int) ($record['id'] ?? 0);
$record['operator_id'] = isset($record['operator_id']) ? (int) $record['operator_id'] : 0;
$record['status_label'] = $this->get_status_label((string) ($record['status'] ?? ''));
$record['carrier_label'] = $this->get_carrier_label((string) ($record['carrier'] ?? ''));
$record['operator'] = $this->get_user_summary((int) ($record['operator_id'] ?? 0));
return $record;
}
private function get_carrier_label(string $carrier): string {
foreach (self::get_carrier_options() as $item) {
if (($item['value'] ?? '') === $carrier) {
return (string) ($item['label'] ?? $carrier);
}
}
return $carrier;
}
private function get_status_label(string $status): string {
foreach (self::get_status_options() as $item) {
if (($item['value'] ?? '') === $status) {
return (string) ($item['label'] ?? $status);
}
}
return $status;
}
private function get_user_summary(int $user_id): array {
if ($user_id <= 0) {
return [];
}
$user = get_userdata($user_id);
if (!$user) {
return [];
}
return [
'id' => $user_id,
'display_name' => (string) $user->display_name,
'user_login' => (string) $user->user_login,
];
}
private function build_summary_item(array $record): array {
return [
'id' => (int) ($record['id'] ?? 0),
'status' => (string) ($record['status'] ?? ''),
'status_label' => (string) ($record['status_label'] ?? ''),
'carrier' => (string) ($record['carrier'] ?? ''),
'carrier_label' => (string) ($record['carrier_label'] ?? ''),
'tracking_no' => (string) ($record['tracking_no'] ?? ''),
'created_at' => (string) ($record['created_at'] ?? ''),
'updated_at' => (string) ($record['updated_at'] ?? ''),
'delivered_at' => (string) ($record['delivered_at'] ?? ''),
];
}
private function can_transition_status(string $current_status, string $new_status): bool {
$map = [
self::STATUS_SHIPPED => [self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED],
self::STATUS_IN_TRANSIT => [self::STATUS_DELIVERED, self::STATUS_RETURNED],
self::STATUS_DELIVERED => [],
self::STATUS_RETURNED => [],
];
return in_array($new_status, $map[$current_status] ?? [], true);
}
private function sync_order_status(string $order_number, string $shipping_status, string $note = ''): void {
if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository') || !class_exists('AKSRT\\Payment\\Order')) {
return;
}
if (!in_array($shipping_status, [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED], true)) {
return;
}
OrderRepository::get_instance()->update_status($order_number, Order::STATUS_DELIVERED, [
'note' => $note,
]);
}
/* ============================================================
* Express Query API (§6.6 快递查询)
*
* Supports kuaidi100, aliyun, kdniao.
* Traces cached in order_shipping.express_data JSON.
* Refresh interval from shop settings.
* ============================================================ */
public function query_express(int $shipping_id, bool $force = false): array {
global $wpdb;
$record = $this->get($shipping_id);
if (!$record) {
return ['success' => false, 'message' => __('发货记录不存在', 'aksrt')];
}
$tracking_no = (string) ($record['tracking_no'] ?? '');
$carrier = (string) ($record['carrier'] ?? '');
if ($tracking_no === '' || $carrier === '') {
return ['success' => false, 'message' => __('缺少运单号或物流公司', 'aksrt')];
}
$cached = $this->get_express_cache($shipping_id);
if (!$force && !empty($cached) && !$this->should_refresh_cache($cached)) {
return ['success' => true, 'data' => $cached];
}
if (!empty($cached['signed'])) {
return ['success' => true, 'data' => $cached];
}
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$api_sdk = sanitize_key((string) ($shop_settings['express']['api_sdk'] ?? 'kuaidi100'));
$result = $this->call_express_api($api_sdk, $carrier, $tracking_no, $shop_settings);
if ($result['success']) {
$express_data = [
'query_time' => current_time('mysql'),
'traces' => $result['traces'] ?? [],
'state' => $result['state'] ?? 'unknown',
'signed' => !empty($result['signed']),
'api' => $api_sdk,
];
$wpdb->update(
$this->table(),
['note' => wp_json_encode($express_data, JSON_UNESCAPED_UNICODE)],
['id' => $shipping_id],
['%s'],
['%d']
);
if (!empty($result['signed']) && ($record['status'] ?? '') !== self::STATUS_DELIVERED) {
$this->update_status($shipping_id, self::STATUS_DELIVERED, __('快递已签收(API检测)', 'aksrt'));
}
return ['success' => true, 'data' => $express_data];
}
return $result;
}
private function get_express_cache(int $shipping_id): array {
global $wpdb;
$note = $wpdb->get_var($wpdb->prepare(
"SELECT note FROM {$this->table()} WHERE id = %d",
$shipping_id
));
if (!$note) return [];
$data = json_decode($note, true);
return is_array($data) && isset($data['traces']) ? $data : [];
}
private function should_refresh_cache(array $cached): bool {
if (empty($cached['query_time'])) return true;
$shop = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$interval = max(30, min(240, (int) ($shop['express']['query_interval'] ?? 60)));
$last = strtotime($cached['query_time']);
return (time() - $last) > ($interval * 60);
}
private function call_express_api(string $sdk, string $carrier, string $tracking_no, array $settings): array {
$carrier_code = $this->map_carrier_code($sdk, $carrier);
if ($sdk === 'kuaidi100') {
return $this->query_kuaidi100($carrier_code, $tracking_no, $settings);
}
if ($sdk === 'kdniao') {
return $this->query_kdniao($carrier_code, $tracking_no, $settings);
}
if ($sdk === 'aliyun') {
return $this->query_aliyun($carrier_code, $tracking_no, $settings);
}
return ['success' => false, 'message' => __('未配置快递查询接口', 'aksrt')];
}
private function query_kuaidi100(string $com, string $num, array $settings): array {
$key = sanitize_text_field((string) ($settings['express']['kuaidi100_key'] ?? ''));
$customer = sanitize_text_field((string) ($settings['express']['kuaidi100_customer'] ?? ''));
if ($key === '' || $customer === '') {
return ['success' => false, 'message' => __('快递100 API 未配置', 'aksrt')];
}
$param = wp_json_encode(['com' => $com, 'num' => $num]);
$sign = strtoupper(md5($param . $key . $customer));
$response = wp_remote_post('https://poll.kuaidi100.com/poll/query.do', [
'body' => ['customer' => $customer, 'sign' => $sign, 'param' => $param],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body) || (int) ($body['status'] ?? 0) !== 200) {
return ['success' => false, 'message' => (string) ($body['message'] ?? __('查询失败', 'aksrt'))];
}
$state = (string) ($body['state'] ?? '0');
$signed = $state === '3';
$traces = [];
foreach ((array) ($body['data'] ?? []) as $item) {
$traces[] = [
'time' => (string) ($item['ftime'] ?? $item['time'] ?? ''),
'context' => (string) ($item['context'] ?? ''),
];
}
return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed];
}
private function query_kdniao(string $code, string $num, array $settings): array {
$app_id = sanitize_text_field((string) ($settings['express']['kdniao_app_id'] ?? ''));
$app_key = sanitize_text_field((string) ($settings['express']['kdniao_app_key'] ?? ''));
if ($app_id === '' || $app_key === '') {
return ['success' => false, 'message' => __('快递鸟 API 未配置', 'aksrt')];
}
$request_data = wp_json_encode(['ShipperCode' => $code, 'LogisticCode' => $num]);
$data_sign = urlencode(base64_encode(md5($request_data . $app_key)));
$response = wp_remote_post('https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx', [
'body' => [
'RequestData' => urlencode($request_data),
'EBusinessID' => $app_id,
'RequestType' => '1002',
'DataSign' => $data_sign,
'DataType' => '2',
],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body) || !($body['Success'] ?? false)) {
return ['success' => false, 'message' => (string) ($body['Reason'] ?? __('查询失败', 'aksrt'))];
}
$state = (string) ($body['State'] ?? '0');
$signed = $state === '3';
$traces = [];
foreach ((array) ($body['Traces'] ?? []) as $item) {
$traces[] = [
'time' => (string) ($item['AcceptTime'] ?? ''),
'context' => (string) ($item['AcceptStation'] ?? ''),
];
}
return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed];
}
private function query_aliyun(string $code, string $num, array $settings): array {
$app_code = sanitize_text_field((string) ($settings['express']['aliyun_app_code'] ?? ''));
if ($app_code === '') {
return ['success' => false, 'message' => __('阿里云快递 API 未配置', 'aksrt')];
}
$url = add_query_arg(['no' => $num, 'type' => $code], 'https://wuliu.market.alicloudapi.com/kdi');
$response = wp_remote_get($url, [
'headers' => ['Authorization' => 'APPCODE ' . $app_code],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body) || (int) ($body['status'] ?? -1) !== 0) {
return ['success' => false, 'message' => (string) ($body['msg'] ?? __('查询失败', 'aksrt'))];
}
$result_data = $body['result'] ?? [];
$state = (string) ($result_data['deliverystatus'] ?? '0');
$signed = $state === '3';
$traces = [];
foreach ((array) ($result_data['list'] ?? []) as $item) {
$traces[] = [
'time' => (string) ($item['time'] ?? ''),
'context' => (string) ($item['status'] ?? ''),
];
}
return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed];
}
private function map_carrier_code(string $sdk, string $carrier): string {
$kuaidi100_map = [
'sf' => 'shunfeng', 'yd' => 'yunda', 'zt' => 'zhongtong', 'yt' => 'yuantong',
'sto' => 'shentong', 'jd' => 'jd', 'ems' => 'ems', 'db' => 'debangkuaidi',
'yunda' => 'yunda',
];
$kdniao_map = [
'sf' => 'SF', 'yd' => 'YD', 'zt' => 'ZTO', 'yt' => 'YTO',
'sto' => 'STO', 'jd' => 'JD', 'ems' => 'EMS', 'db' => 'DBL',
'yunda' => 'YD',
];
if ($sdk === 'kuaidi100') {
return $kuaidi100_map[$carrier] ?? $carrier;
}
if ($sdk === 'kdniao') {
return $kdniao_map[$carrier] ?? strtoupper($carrier);
}
return $carrier;
}
}
-152
View File
@@ -1,152 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
trait ShippingPart2 {
public function build_express_timeline(int $shipping_id): array {
$record = $this->get($shipping_id);
if (!$record) return [];
$cached = $this->get_express_cache($shipping_id);
$timeline = [];
$timeline[] = [
'time' => (string) ($record['created_at'] ?? ''),
'label' => sprintf(__('商家已发货:%s %s', 'aksrt'), $record['carrier_label'] ?? '', $record['tracking_no'] ?? ''),
'type' => 'shipped',
];
if (!empty($cached['traces'])) {
foreach ($cached['traces'] as $trace) {
$timeline[] = [
'time' => (string) ($trace['time'] ?? ''),
'label' => (string) ($trace['context'] ?? ''),
'type' => 'trace',
];
}
}
if (!empty($cached['signed'])) {
$timeline[] = [
'time' => (string) ($record['delivered_at'] ?? ($cached['query_time'] ?? '')),
'label' => __('快递已签收', 'aksrt'),
'type' => 'signed',
];
}
usort($timeline, static fn($a, $b) => strcmp($a['time'] ?? '', $b['time'] ?? ''));
return $timeline;
}
/* ============================================================
* Shipping Fee Resolution (§6.6 运费模板)
*
* 3 types: free / fixed / amount (满额包邮)
* Inheritance: product → category → theme defaults
* ============================================================ */
public function resolve_shipping_fee(int $product_id, float $order_amount = 0, array $address = []): array {
$config = $this->get_product_shipping_config($product_id);
$fee_type = (string) ($config['fee_type'] ?? 'fixed');
$fee_amount = (float) ($config['fee_amount'] ?? 0);
$free_threshold = (float) ($config['free_threshold'] ?? 0);
if ($fee_type === 'free') {
return ['fee' => 0.0, 'type' => 'free', 'label' => __('包邮', 'aksrt')];
}
if ($fee_type === 'amount' && $free_threshold > 0 && $order_amount >= $free_threshold) {
return ['fee' => 0.0, 'type' => 'free_threshold', 'label' => sprintf(__('满%.0f包邮', 'aksrt'), $free_threshold)];
}
$region_fee = $this->resolve_region_fee($config, $address);
if ($region_fee !== null) {
return ['fee' => $region_fee, 'type' => 'region', 'label' => __('地区运费', 'aksrt')];
}
return ['fee' => $fee_amount, 'type' => 'fixed', 'label' => sprintf(__('运费 ¥%.2f', 'aksrt'), $fee_amount)];
}
private function get_product_shipping_config(int $product_id): array {
$meta = get_post_meta($product_id, '_aksrt_shipping_config', true);
if (is_array($meta) && !empty($meta['fee_type'])) {
return $meta;
}
if (class_exists('\AKSRT\Shop\TermMeta')) {
$cat_config = \AKSRT\Shop\TermMeta::resolve_inheritance($product_id, 'shipping_fee');
if (is_array($cat_config) && !empty($cat_config['fee_type'])) {
return $cat_config;
}
}
if (class_exists('\AKSRT\Settings\Settings')) {
$shop = \AKSRT\Settings\Settings::get_shop_settings();
return [
'fee_type' => sanitize_key((string) ($shop['defaults']['shipping_fee_type'] ?? 'fixed')),
'fee_amount' => (float) ($shop['defaults']['shipping_fee_amount'] ?? 12),
'free_threshold' => (float) ($shop['defaults']['shipping_free_threshold'] ?? 0),
'region_fees' => (array) ($shop['defaults']['region_fees'] ?? []),
];
}
return ['fee_type' => 'fixed', 'fee_amount' => 12, 'free_threshold' => 0, 'region_fees' => []];
}
private function resolve_region_fee(array $config, array $address): ?float {
$region_fees = (array) ($config['region_fees'] ?? []);
if (empty($region_fees) || empty($address['province'])) {
return null;
}
$province = (string) $address['province'];
foreach ($region_fees as $region) {
$provinces = (array) ($region['provinces'] ?? []);
if (in_array($province, $provinces, true)) {
return (float) ($region['fee'] ?? 0);
}
}
return null;
}
public static function get_china_regions(): array {
$file = get_template_directory() . '/assets/data/china-regions.json';
if (!file_exists($file)) {
return self::get_builtin_provinces();
}
$data = json_decode((string) file_get_contents($file), true);
return is_array($data) ? $data : self::get_builtin_provinces();
}
private static function get_builtin_provinces(): array {
return [
['code' => '11', 'name' => '北京市'], ['code' => '12', 'name' => '天津市'],
['code' => '13', 'name' => '河北省'], ['code' => '14', 'name' => '山西省'],
['code' => '15', 'name' => '内蒙古自治区'], ['code' => '21', 'name' => '辽宁省'],
['code' => '22', 'name' => '吉林省'], ['code' => '23', 'name' => '黑龙江省'],
['code' => '31', 'name' => '上海市'], ['code' => '32', 'name' => '江苏省'],
['code' => '33', 'name' => '浙江省'], ['code' => '34', 'name' => '安徽省'],
['code' => '35', 'name' => '福建省'], ['code' => '36', 'name' => '江西省'],
['code' => '37', 'name' => '山东省'], ['code' => '41', 'name' => '河南省'],
['code' => '42', 'name' => '湖北省'], ['code' => '43', 'name' => '湖南省'],
['code' => '44', 'name' => '广东省'], ['code' => '45', 'name' => '广西壮族自治区'],
['code' => '46', 'name' => '海南省'], ['code' => '50', 'name' => '重庆市'],
['code' => '51', 'name' => '四川省'], ['code' => '52', 'name' => '贵州省'],
['code' => '53', 'name' => '云南省'], ['code' => '54', 'name' => '西藏自治区'],
['code' => '61', 'name' => '陕西省'], ['code' => '62', 'name' => '甘肃省'],
['code' => '63', 'name' => '青海省'], ['code' => '64', 'name' => '宁夏回族自治区'],
['code' => '65', 'name' => '新疆维吾尔自治区'],
['code' => '71', 'name' => '台湾省'],
['code' => '81', 'name' => '香港特别行政区'],
['code' => '82', 'name' => '澳门特别行政区'],
];
}
}
+8 -36
View File
@@ -52,9 +52,7 @@ class UserOrders {
if ($status !== '') {
$repo_args['status'] = $status;
}
if (in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_PENDING_REVIEW], true)) {
$repo_args['product_type'] = \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP;
} elseif ($product_type !== '' && $product_type !== 'all') {
if ($product_type !== '' && $product_type !== 'all') {
$repo_args['product_type'] = $product_type;
}
@@ -124,8 +122,6 @@ class UserOrders {
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
$status = (string) ($order['status'] ?? Order::STATUS_PENDING);
$product_id = (int) ($order['product_id'] ?? 0);
$is_shop = $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP
|| $product_type === 'shop';
$currency = sanitize_key((string) ($order['currency'] ?? 'CNY'));
$is_points = $currency === 'points'
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'
@@ -139,17 +135,11 @@ class UserOrders {
$permalink = $this->resolve_order_permalink($product_type, $product_id, $meta);
$order_items = [];
if ($is_shop && class_exists('\AKSRT\Shop\Shop')) {
$order_items = \AKSRT\Shop\Shop::get_instance()->get_order_items($order_number);
}
$has_structured_items = !empty($order_items);
$product = [];
if ($has_structured_items && class_exists('\AKSRT\Shop\Shop')) {
$product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0));
} elseif ($is_shop && $product_id > 0 && class_exists('\AKSRT\Shop\Shop')) {
$product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view($product_id);
}
// Shop product cards were removed with the shop feature; content/VIP
// orders fall through to the shared meta-based fields below.
if (!empty($product['title'])) {
$title = (string) $product['title'];
@@ -163,20 +153,6 @@ class UserOrders {
$shipping_records = [];
$after_sale_items = [];
if ($is_shop) {
if (class_exists(Shipping::class)) {
$shipping_records = Shipping::get_instance()->get_by_order($order_number);
}
if (class_exists(AfterSale::class)) {
$as = AfterSale::get_instance()->get_list([
'limit' => 20,
'offset' => 0,
'order_number' => $order_number,
]);
$after_sale_items = $as['items'] ?? [];
}
}
$subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount'] ?? 0);
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
$discount = (float) ($meta['discount'] ?? 0);
@@ -186,10 +162,10 @@ class UserOrders {
$can_pay = in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true)
&& $payment_value !== 'manual_review';
$can_cancel = in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true);
$can_after_sale = $is_shop && in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED], true);
$can_receipt = $is_shop && $status === Order::STATUS_DELIVERED;
$can_review = $is_shop && $status === Order::STATUS_PENDING_REVIEW;
$can_rebuy = $is_shop && in_array($status, [Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW], true);
$can_after_sale = false;
$can_receipt = false;
$can_review = false;
$can_rebuy = false;
// Order detail modal is always available; product permalink is optional.
$can_view = true;
$can_view_product = $permalink !== '' && in_array($status, [
@@ -306,11 +282,7 @@ class UserOrders {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
// Prefer shop checkout when available (handles points + shop meta).
if (class_exists('\AKSRT\Shop\Shop')) {
return \AKSRT\Shop\Shop::get_instance()->pay_order($user_id, $order_number, $payment_method);
}
// Resume via shared gateway stack (any product_type; shop removed).
$repo = OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
-9
View File
@@ -27,15 +27,6 @@ trait DomainGroups {
Integration::update_wechat_settings($settings);
}
/**
* 获取商城设置
*
* @return array
*/
public static function get_shop_settings(): array {
return ForumShop::get_shop_settings();
}
/**
* 获取文档模式设置
*/
-185
View File
@@ -1,185 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Settings;
defined('ABSPATH') || exit;
class ForumShop {
/**
* 获取商城设置
*
* @return array
*/
public static function get_shop_settings(): array {
$s = Settings::get_all();
// -- basic / general --
$enabled = !empty($s['shop_s']);
$home_url = esc_url_raw((string) ($s['shop_home_url'] ?? ''));
// breadcrumbs
$breadcrumbs_s = !isset($s['shop_breadcrumbs_s']) || !empty($s['shop_breadcrumbs_s']);
$breadcrumbs_home = !isset($s['shop_breadcrumbs_home']) || !empty($s['shop_breadcrumbs_home']);
$breadcrumbs_shop_home = !isset($s['shop_breadcrumbs_shop_home']) || !empty($s['shop_breadcrumbs_shop_home']);
$breadcrumbs_shop_home_name = sanitize_text_field((string) ($s['shop_breadcrumbs_shop_home_name'] ?? '商城首页'));
// rewrite
$product_rewrite_slug = sanitize_key((string) ($s['shop_product_rewrite_slug'] ?? 'product'));
$cart_rewrite_slug = sanitize_key((string) ($s['shop_cart_rewrite_slug'] ?? 'cart'));
$rewrite_suffix_html = !empty($s['shop_rewrite_suffix_html_s']);
// -- list --
$list_orderby = sanitize_key((string) ($s['shop_list_orderby'] ?? 'date'));
if (!in_array($list_orderby, ['date', 'sales', 'price_asc', 'price_desc', 'comments'], true)) {
$list_orderby = 'date';
}
$list_count = max(4, min(100, (int) ($s['shop_list_count'] ?? 12)));
$list_paging = sanitize_key((string) ($s['shop_list_paging'] ?? 'number'));
if (!in_array($list_paging, ['number', 'ajax'], true)) {
$list_paging = 'number';
}
$list_ias_s = !empty($s['shop_list_ias_s']);
$list_ias_max = max(1, min(50, (int) ($s['shop_list_ias_max'] ?? 5)));
$list_style = sanitize_key((string) ($s['shop_list_style'] ?? 'default'));
if (!in_array($list_style, ['default', 'card-lg', 'card-sm'], true)) {
$list_style = 'default';
}
$sales_show = sanitize_key((string) ($s['shop_sales_show'] ?? 'show'));
if (!in_array($sales_show, ['show', 'hide', 'threshold'], true)) {
$sales_show = 'show';
}
$sales_show_min = max(0, min(99999, (int) ($s['shop_sales_show_min'] ?? 10)));
// -- contact --
$contact_msg_s = !isset($s['shop_contact_msg_s']) || !empty($s['shop_contact_msg_s']);
$contact_name = sanitize_text_field((string) ($s['shop_contact_name'] ?? '联系客服'));
$contact_note = sanitize_textarea_field((string) ($s['shop_contact_note'] ?? ''));
$contact_more = sanitize_textarea_field((string) ($s['shop_contact_more'] ?? ''));
// -- single --
$content_layout = sanitize_key((string) ($s['shop_content_layout'] ?? 'adapt'));
if (!in_array($content_layout, ['adapt', 'full', 'sidebar'], true)) {
$content_layout = 'adapt';
}
$content_show_bg = !empty($s['shop_content_show_bg']);
$single_tab = sanitize_textarea_field((string) ($s['shop_single_tab'] ?? ''));
$content_after = wp_kses_post((string) ($s['shop_content_after'] ?? ''));
$related_s = !isset($s['shop_single_related_s']) || !empty($s['shop_single_related_s']);
$related_opt = sanitize_textarea_field((string) ($s['shop_single_related_opt'] ?? ''));
$main_image_default = esc_url_raw((string) ($s['shop_main_image_default'] ?? ''));
// -- product defaults --
$limit_buy = max(-1, min(9999, (int) ($s['shop_limit_buy'] ?? -1)));
$guest_buy = !empty($s['shop_guest_buy']);
$email_fill = !empty($s['shop_email_fill']);
$rebate = !isset($s['shop_rebate']) || !empty($s['shop_rebate']);
$shipping_fee = sanitize_key((string) ($s['shop_shipping_fee'] ?? 'free'));
if (!in_array($shipping_fee, ['free', 'fixed', 'amount'], true)) {
$shipping_fee = 'free';
}
$after_sale = sanitize_key((string) ($s['shop_after_sale'] ?? 'refund_return'));
if (!in_array($after_sale, ['refund', 'refund_return', 'none'], true)) {
$after_sale = 'refund_return';
}
$service = sanitize_textarea_field((string) ($s['shop_service'] ?? ''));
$params_default = sanitize_textarea_field((string) ($s['shop_product_params_default'] ?? ''));
// -- shipping config --
$manual_address = !empty($s['shop_manual_address']);
$express_api_sdk = sanitize_key((string) ($s['shop_express_api_sdk'] ?? 'kuaidi100'));
if (!in_array($express_api_sdk, ['kuaidi100', 'aliyun', 'kdniao'], true)) {
$express_api_sdk = 'kuaidi100';
}
$express_query_interval = max(30, min(240, (int) ($s['shop_express_query_interval'] ?? 60)));
$express_kdniao_opt = sanitize_textarea_field((string) ($s['shop_express_kdniao_opt'] ?? ''));
$express_kuaidi100_opt = sanitize_textarea_field((string) ($s['shop_express_kuaidi100_opt'] ?? ''));
$express_aliyun_opt = sanitize_textarea_field((string) ($s['shop_express_aliyun_opt'] ?? ''));
$shipping_company = sanitize_textarea_field((string) ($s['shop_shipping_company'] ?? ''));
// -- timing --
$receipt_max_day = max(1, min(60, (int) ($s['shop_order_receipt_max_day'] ?? 15)));
$comment_max_day = max(1, min(60, (int) ($s['shop_comment_max_day'] ?? 7)));
$comment_placeholder = sanitize_text_field((string) ($s['shop_comment_placeholder'] ?? ''));
$comment_img_num = max(0, min(9, (int) ($s['shop_comment_img_num'] ?? 3)));
$after_sale_return_max_day = max(1, min(30, (int) ($s['shop_after_sale_return_express_max_day'] ?? 7)));
$after_sale_reason_refund = sanitize_textarea_field((string) ($s['shop_after_sale_reason_refund'] ?? ''));
$after_sale_reason_return = sanitize_textarea_field((string) ($s['shop_after_sale_reason_return'] ?? ''));
$author_show = !isset($s['shop_author_show']) || !empty($s['shop_author_show']);
return [
'enabled' => $enabled,
'home_url' => $home_url,
'breadcrumbs' => [
'enabled' => $breadcrumbs_s,
'home' => $breadcrumbs_home,
'shop_home' => $breadcrumbs_shop_home,
'shop_home_name' => $breadcrumbs_shop_home_name,
],
'rewrite' => [
'product_slug' => $product_rewrite_slug,
'cart_slug' => $cart_rewrite_slug,
'suffix_html' => $rewrite_suffix_html,
],
'list' => [
'orderby' => $list_orderby,
'count' => $list_count,
'paging' => $list_paging,
'ias_s' => $list_ias_s,
'ias_max' => $list_ias_max,
'style' => $list_style,
],
'sales' => [
'show' => $sales_show,
'show_min' => $sales_show_min,
],
'contact' => [
'msg_s' => $contact_msg_s,
'name' => $contact_name,
'note' => $contact_note,
'more' => $contact_more,
],
'single' => [
'content_layout' => $content_layout,
'content_show_bg' => $content_show_bg,
'tab' => $single_tab,
'content_after' => $content_after,
'related_s' => $related_s,
'related_opt' => $related_opt,
'main_image_default' => $main_image_default,
],
'product_defaults' => [
'limit_buy' => $limit_buy,
'guest_buy' => $guest_buy,
'email_fill' => $email_fill,
'rebate' => $rebate,
'shipping_fee' => $shipping_fee,
'after_sale' => $after_sale,
'service' => $service,
'params_default' => $params_default,
],
'shipping' => [
'manual_address' => $manual_address,
'express_api_sdk' => $express_api_sdk,
'express_query_interval' => $express_query_interval,
'express_kdniao_opt' => $express_kdniao_opt,
'express_kuaidi100_opt' => $express_kuaidi100_opt,
'express_aliyun_opt' => $express_aliyun_opt,
'shipping_company' => $shipping_company,
],
'timing' => [
'receipt_max_day' => $receipt_max_day,
'comment_max_day' => $comment_max_day,
'comment_placeholder' => $comment_placeholder,
'comment_img_num' => $comment_img_num,
'after_sale_return_max_day' => $after_sale_return_max_day,
'after_sale_reason_refund' => $after_sale_reason_refund,
'after_sale_reason_return' => $after_sale_reason_return,
],
'author_show' => $author_show,
];
}
}
-204
View File
@@ -1,204 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class AddressService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
/**
* 获取用户地址簿(公开接口)
*/
public function get_address_book(int $user_id): array {
return $this->get_user_address_book($user_id);
}
public function get_user_address_book(int $user_id): array {
$saved = get_user_meta($user_id, 'aksrt_shop_addresses', true);
if (is_array($saved) && !empty($saved)) {
return array_values(array_filter(array_map([$this, 'sanitize_address'], $saved)));
}
return [];
}
private function sanitize_address($address): array {
if (!is_array($address)) {
return [];
}
$name = sanitize_text_field((string) ($address['name'] ?? ''));
$phone = sanitize_text_field((string) ($address['phone'] ?? ''));
$province = sanitize_text_field((string) ($address['province'] ?? ''));
$city = sanitize_text_field((string) ($address['city'] ?? ''));
$district = sanitize_text_field((string) ($address['district'] ?? ''));
$street = sanitize_text_field((string) ($address['address'] ?? ''));
return [
'id' => (int) ($address['id'] ?? 0),
'name' => $name,
'phone' => $phone,
'province' => $province,
'city' => $city,
'district' => $district,
'address' => $street,
'is_default' => !empty($address['is_default']),
'full_text' => trim($name . ' / ' . $province . $city . $district . $street),
];
}
/**
* 新增地址
*
* @param int $user_id
* @param array $data 地址数据(name, phone, province, city, district, address, is_default)
* @return array{success:bool, address:array|null, error:string|null}
*/
public function add_address(int $user_id, array $data): array {
$address = $this->sanitize_address($data);
if ($address['name'] === '' || $address['phone'] === '' || $address['address'] === '') {
return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')];
}
$book = $this->get_user_address_book($user_id);
$address['id'] = $this->next_address_id($book);
if (!empty($data['is_default']) || empty($book)) {
foreach ($book as &$item) {
$item['is_default'] = false;
}
$address['is_default'] = true;
}
$book[] = $address;
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'address' => $address, 'error' => null];
}
/**
* 更新地址
*
* @param int $user_id
* @param int $address_id
* @param array $data
* @return array{success:bool, address:array|null, error:string|null}
*/
public function update_address(int $user_id, int $address_id, array $data): array {
$book = $this->get_user_address_book($user_id);
$found = false;
foreach ($book as &$item) {
if ((int) ($item['id'] ?? 0) !== $address_id) {
continue;
}
$found = true;
$merged = array_merge($item, $this->sanitize_address(array_merge($item, $data)));
$merged['id'] = $address_id;
if ($merged['name'] === '' || $merged['phone'] === '' || $merged['address'] === '') {
return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')];
}
if (!empty($data['is_default'])) {
foreach ($book as &$clear) {
$clear['is_default'] = false;
}
$merged['is_default'] = true;
}
$item = $merged;
break;
}
if (!$found) {
return ['success' => false, 'address' => null, 'error' => __('地址不存在。', 'aksrt')];
}
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'address' => $merged, 'error' => null];
}
/**
* 删除地址
*
* @param int $user_id
* @param int $address_id
* @return array{success:bool, error:string|null}
*/
public function delete_address(int $user_id, int $address_id): array {
$book = $this->get_user_address_book($user_id);
$found = false;
$was_default = false;
$filtered = [];
foreach ($book as $item) {
if ((int) ($item['id'] ?? 0) === $address_id) {
$found = true;
$was_default = !empty($item['is_default']);
continue;
}
$filtered[] = $item;
}
if (!$found) {
return ['success' => false, 'error' => __('地址不存在。', 'aksrt')];
}
if ($was_default && !empty($filtered)) {
$filtered[0]['is_default'] = true;
}
update_user_meta($user_id, 'aksrt_shop_addresses', array_values($filtered));
return ['success' => true, 'error' => null];
}
/**
* 设置默认地址
*
* @param int $user_id
* @param int $address_id
* @return array{success:bool, error:string|null}
*/
public function set_default_address(int $user_id, int $address_id): array {
$book = $this->get_user_address_book($user_id);
$found = false;
foreach ($book as &$item) {
$item['is_default'] = ((int) ($item['id'] ?? 0) === $address_id);
if ($item['is_default']) {
$found = true;
}
}
if (!$found) {
return ['success' => false, 'error' => __('地址不存在。', 'aksrt')];
}
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'error' => null];
}
/**
* 生成下一个地址 ID
*/
private function next_address_id(array $book): int {
$max = 0;
foreach ($book as $item) {
$max = max($max, (int) ($item['id'] ?? 0));
}
return $max + 1;
}
}
-173
View File
@@ -1,173 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class CartService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
public function get_cart(int $user_id): array {
if ($user_id <= 0) {
return [];
}
$cart = get_user_meta($user_id, 'shop_cart', true);
return is_array($cart) ? $cart : [];
}
public function get_cart_count(int $user_id): int {
$cart = $this->get_cart($user_id);
$count = 0;
foreach ($cart as $opts) {
if (is_array($opts)) {
foreach ($opts as $qty) {
$count += (int) $qty;
}
}
}
return $count;
}
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
if ($user_id <= 0 || $product_id <= 0 || $count <= 0) {
return ['error' => 'invalid_params'];
}
$validation = $this->validate_cart_item($product_id, $options_string);
if ($validation !== true) {
return ['error' => $validation];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if (!isset($cart[$pid_key]) || !is_array($cart[$pid_key])) {
$cart[$pid_key] = [];
}
$cart[$pid_key][$opt_key] = ((int) ($cart[$pid_key][$opt_key] ?? 0)) + $count;
// Enforce limit_buy if set
$limit = (int) get_post_meta($product_id, '_aksrt_limit_buy', true);
if ($limit > 0 && $cart[$pid_key][$opt_key] > $limit) {
$cart[$pid_key][$opt_key] = $limit;
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
if ($user_id <= 0 || $product_id <= 0 || $count <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if (isset($cart[$pid_key][$opt_key])) {
$cart[$pid_key][$opt_key] -= $count;
if ($cart[$pid_key][$opt_key] <= 0) {
unset($cart[$pid_key][$opt_key]);
}
}
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if ($options_string === '' && isset($cart[$pid_key])) {
// Remove all options for this product
unset($cart[$pid_key]);
} elseif (isset($cart[$pid_key][$opt_key])) {
unset($cart[$pid_key][$opt_key]);
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function remove_multi_from_cart(int $user_id, array $items): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
foreach ($items as $item) {
$pid_key = (string) ($item['product_id'] ?? 0);
$opt_key = !empty($item['options_string']) ? $item['options_string'] : '_default';
if (isset($cart[$pid_key][$opt_key])) {
unset($cart[$pid_key][$opt_key]);
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
}
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function empty_cart(int $user_id): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
delete_user_meta($user_id, 'shop_cart');
return ['success' => true, 'cart_count' => 0];
}
/**
* Validate a product for cart addition.
* Returns true if valid, or a string error code.
*/
public function validate_cart_item(int $product_id, string $options_string = '') {
$post = get_post($product_id);
if (!$post || $post->post_type !== 'aksrt_product') {
return 'invalid_product';
}
if ($post->post_status !== 'publish') {
return 'product_not_published';
}
// Check stock (unless unlimited)
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
if ($stock > -1 && $stock <= 0) {
return 'out_of_stock';
}
// Check guest_buy — if disabled and user not logged in
if (!is_user_logged_in()) {
$guest = get_post_meta($product_id, '_aksrt_guest_buy', true);
if ($guest === '0' || $guest === '') {
// Fall back to inheritance chain via TermMeta
if (class_exists('AKSRT\Shop\TermMeta')) {
$resolved = TermMeta::resolve_inheritance($product_id, 'guest_buy');
if (empty($resolved)) {
return 'login_required';
}
}
}
}
return true;
}
}
-414
View File
@@ -1,414 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class CheckoutService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
public function preview_checkout(int $user_id, array $payload): array {
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录后再结算。', 'aksrt')];
}
$items = $this->normalize_checkout_items($payload['items'] ?? []);
if (empty($items)) {
return ['success' => false, 'message' => __('请选择至少一个商品。', 'aksrt')];
}
$product_views = [];
$subtotal = 0.0;
foreach ($items as $item) {
$product = $this->shop->get_product_detail_view((int) $item['product_id']);
if (empty($product) || empty($product['purchase_enabled'])) {
return ['success' => false, 'message' => __('存在不可购买商品,请刷新后重试。', 'aksrt')];
}
if ((int) $product['stock'] < (int) $item['quantity']) {
return ['success' => false, 'message' => sprintf(__('商品 %s 库存不足。', 'aksrt'), $product['title'])];
}
$line_total = round(((float) $product['price']) * (int) $item['quantity'], 2);
// R69: Apply discount engine (shop_discount taxonomy campaigns)
$discount_result = ['discount_amount' => 0, 'gifts' => [], 'applied' => []];
if (class_exists('\AKSRT\Payment\Discount')) {
$discount_result = \AKSRT\Payment\Discount::get_instance()->calculate(
(float) $product['price'],
(int) $item['quantity'],
(int) $product['id'],
$user_id
);
$line_total = $discount_result['final_price'];
}
$subtotal += $line_total;
$product_views[] = [
'product_id' => (int) $product['id'],
'title' => $product['title'],
'price' => (float) $product['price'],
'quantity' => (int) $item['quantity'],
'line_total' => $line_total,
'image' => $product['image'],
'shipping' => $product['shipping'],
'discount_amount' => (float) ($discount_result['discount_amount'] ?? 0),
'gifts' => (array) ($discount_result['gifts'] ?? []),
'applied_discounts' => (array) ($discount_result['applied'] ?? []),
];
}
$shipping_method = sanitize_key((string) ($payload['shipping_method'] ?? 'express'));
$shipping_profile = $this->resolve_shipping_quote($product_views, $shipping_method);
$coupon_code = sanitize_text_field((string) ($payload['coupon_code'] ?? ''));
$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;
}
$total = max(0, round($subtotal + $shipping_profile['fee'] - $coupon_result['discount'], 2));
$address = $this->resolve_checkout_address($user_id, (int) ($payload['address_id'] ?? 0));
$payment_method = $this->resolve_payment_method((string) ($payload['payment_method'] ?? 'balance'));
return [
'success' => true,
'data' => [
'items' => $product_views,
'subtotal' => round($subtotal, 2),
'shipping_fee' => $shipping_profile['fee'],
'shipping_method' => $shipping_profile['method'],
'discount' => $coupon_result['discount'],
'coupon' => $coupon_result['coupon'],
'address' => $address,
'payment_method' => $payment_method,
'total' => $total,
],
];
}
public function create_checkout_order(int $user_id, array $payload): array {
$preview = $this->preview_checkout($user_id, $payload);
if (empty($preview['success'])) {
return $preview;
}
$data = $preview['data'];
$primary_item = $data['items'][0] ?? null;
if (!$primary_item || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')];
}
$payment_method = sanitize_key((string) ($data['payment_method']['value'] ?? 'balance'));
$meta_payload = [
'customer_note' => sanitize_textarea_field((string) ($payload['customer_note'] ?? '')),
'items' => $data['items'],
'coupon' => $data['coupon'],
'shipping_fee' => $data['shipping_fee'],
'shipping_method' => $data['shipping_method'],
'address' => $data['address'],
'payment_method' => $data['payment_method'],
'discount' => $data['discount'],
];
$expiry_minutes = 30;
if (class_exists('\AKSRT\Settings\Settings')) {
$order_settings = \AKSRT\Settings\Settings::get_order_settings();
$expiry_minutes = max(5, min(1440, (int) ($order_settings['expiry'] ?? 30)));
}
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
'user_id' => $user_id,
'product_type' => 'shop_product',
'product_id' => (int) $primary_item['product_id'],
'amount' => (float) $data['total'],
'currency' => 'CNY',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
'expires_at' => date('Y-m-d H:i:s', strtotime('+' . $expiry_minutes . ' minutes')),
'note' => wp_json_encode($meta_payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
if ($order_number === '') {
return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')];
}
$this->shop->save_order_items($order_number, $data['items']);
if (!empty($data['coupon']['code']) && class_exists('\AKSRT\Payment\Coupon')) {
$coupon_instance = \AKSRT\Payment\Coupon::get_instance();
$coupon_instance->use_coupon($data['coupon']['code']);
// R69: Log coupon usage for per-user tracking
$coupon_row = $coupon_instance->validate($data['coupon']['code']);
$coupon_id = (int) ($coupon_row['coupon']['id'] ?? 0);
if ($coupon_id > 0) {
$coupon_instance->log_usage(
$coupon_id,
$user_id,
$order_number,
(float) ($data['discount'] ?? 0)
);
}
}
$detail = $this->shop->get_order_detail_view($user_id, $order_number);
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'order' => $detail,
],
];
}
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [
\AKSRT\Payment\Order::STATUS_CREATED,
\AKSRT\Payment\Order::STATUS_PENDING,
], true)) {
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
}
// Payment attempts are bound to the method selected when the order was created.
$stored_method = sanitize_key((string) ($order['payment_method'] ?? ''));
$payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance'));
if ($stored_method !== '' && $payment_method !== $stored_method) {
return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')];
}
$gateway_manager = class_exists('\AKSRT\Payment\Gateway') ? \AKSRT\Payment\Gateway::get_instance() : null;
$gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null;
if (!$gateway) {
return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')];
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')];
}
// Points payment is a purely internal settlement (no provider attempt),
// but still needs the attempt lock against double-click double deduction.
if ($payment_method === 'points') {
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
: false;
if ($lock === false) {
return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')];
}
try {
return $this->pay_with_points($user_id, $order_number, $order);
} finally {
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
}
}
// 携带订单号与摘要,供网关生成支付单。
// PaymentOrchestrator 内部持有 attempt 锁并分配 provider out_trade_no。
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
$order['payment_method'] = $payment_method;
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($gateway, $order, [
'paid_note' => __('用户在前台完成支付。', 'aksrt'),
]);
if (empty($payment_result['success'])) {
return ['success' => false, 'message' => $payment_result['message']];
}
// 跳转/扫码型网关(支付宝/微信/PayPal/Stripe):返回支付凭据,订单保持待支付,
// 由 /payment/{gateway}/notify|return 回调确认到账;此处绝不预先标记已支付。
if (!empty($payment_result['redirect'])) {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
'order_number' => $order_number,
'message' => $payment_result['message'],
],
];
}
return [
'success' => true,
'data' => [
'order' => $this->shop->get_order_detail_view($user_id, $order_number),
'message' => $payment_result['message'],
],
];
}
private function pay_with_points(int $user_id, string $order_number, array $order): array {
if (!class_exists('\AKSRT\Credits\Credits')) {
return ['success' => false, 'message' => __('积分系统不可用。', 'aksrt')];
}
$amount = (int) round((float) ($order['amount'] ?? 0));
if ($amount <= 0) {
return ['success' => false, 'message' => __('订单金额无效。', 'aksrt')];
}
$balance = \AKSRT\Credits\Credits::balance($user_id);
if ($balance < $amount) {
return ['success' => false, 'message' => sprintf(__('积分不足,需要 %d 积分,当前余额 %d。', 'aksrt'), $amount, $balance)];
}
$deducted = \AKSRT\Credits\Credits::deduct(
$user_id,
$amount,
'shop_points_pay',
['order' => $order_number],
sprintf(__('积分购买订单 %s', 'aksrt'), $order_number)
);
if (is_wp_error($deducted)) {
return ['success' => false, 'message' => $deducted->get_error_message() ?: __('积分扣除失败。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$paid = $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
'require_transition' => true,
]);
if (!$paid) {
$compensation = \AKSRT\Credits\Credits::add(
$user_id,
$amount,
'shop_points_pay_rollback',
['order' => $order_number],
sprintf(__('订单 %s 支付失败退回积分', 'aksrt'), $order_number)
);
if (is_wp_error($compensation)) {
error_log(sprintf(
'[AKSRT Payment] points rollback failed order=%s: %s',
$order_number,
$compensation->get_error_message()
));
return ['success' => false, 'message' => __('订单状态已变化,积分补偿失败,请联系管理员处理。', 'aksrt')];
}
return ['success' => false, 'message' => __('订单状态已变化,积分已退回。', 'aksrt')];
}
return [
'success' => true,
'data' => [
'order' => $this->shop->get_order_detail_view($user_id, $order_number),
'message' => __('积分支付成功。', 'aksrt'),
],
];
}
private function normalize_checkout_items($items): array {
if (!is_array($items)) {
return [];
}
$normalized = [];
foreach ($items as $item) {
$product_id = (int) ($item['product_id'] ?? 0);
$quantity = max(1, (int) ($item['quantity'] ?? 1));
if ($product_id <= 0) {
continue;
}
$normalized[] = [
'product_id' => $product_id,
'quantity' => $quantity,
];
}
return $normalized;
}
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' => []];
}
if (!class_exists('\AKSRT\Payment\Coupon')) {
return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')];
}
$result = \AKSRT\Payment\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')];
}
return [
'success' => true,
'discount' => (float) ($result['discount'] ?? 0),
'coupon' => [
'code' => (string) ($result['coupon']['code'] ?? $coupon_code),
'type' => (string) ($result['coupon']['type'] ?? ''),
'value' => (float) ($result['coupon']['value'] ?? 0),
],
];
}
private function resolve_shipping_quote(array $items, string $shipping_method): array {
$methods = $this->shop->get_shipping_method_options();
$method = $methods[$shipping_method] ?? $methods['express'];
$base_fee = (float) ($method['fee'] ?? 0);
foreach ($items as $item) {
$base_fee += (float) ($item['shipping']['fee'] ?? 0);
}
return [
'method' => $method,
'fee' => round($base_fee, 2),
];
}
private function resolve_checkout_address(int $user_id, int $address_id): array {
$addresses = $this->shop->get_address_book($user_id);
foreach ($addresses as $address) {
if ((int) ($address['id'] ?? 0) === $address_id) {
return $address;
}
}
return $addresses[0] ?? [];
}
private function resolve_payment_method(string $value): array {
$methods = $this->shop->get_payment_method_options();
return $methods[$value] ?? reset($methods);
}
public function get_checkout_hint(array $product): array {
return [
'delivery' => $product['shipping']['delivery_note'] ?: __('付款后自动进入订单中心,可继续查看物流和售后进度。', 'aksrt'),
'service' => $product['after_sale']['policy'] ?: __('支持在订单中心发起售后申请。', 'aksrt'),
];
}
}
-677
View File
@@ -1,677 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part1 {
public function create_after_sale_request(int $user_id, array $payload): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$order_number = sanitize_text_field((string) ($payload['order_number'] ?? ''));
$reason = sanitize_textarea_field((string) ($payload['reason'] ?? ''));
$type = sanitize_key((string) ($payload['type'] ?? \AKSRT\Payment\AfterSale::TYPE_REFUND));
if ($order_number === '' || $reason === '') {
return ['success' => false, 'message' => __('请填写售后原因。', 'aksrt')];
}
$order = $this->get_order_detail_view($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$ticket_id = \AKSRT\Payment\AfterSale::get_instance()->create([
'order_number' => $order_number,
'applicant_user_id' => $user_id,
'type' => $type,
'reason' => $reason,
'amount' => (float) ($payload['amount'] ?? $order['amount'] ?? 0),
'note' => sanitize_textarea_field((string) ($payload['note'] ?? '')),
]);
if (is_wp_error($ticket_id)) {
return ['success' => false, 'message' => $ticket_id->get_error_message()];
}
if ($ticket_id <= 0) {
return ['success' => false, 'message' => __('售后申请提交失败。', 'aksrt')];
}
return [
'success' => true,
'data' => [
'ticket_id' => $ticket_id,
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
/**
* List after-sale tickets for current user.
*/
public function get_user_after_sales(int $user_id): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['items' => []];
}
$items = \AKSRT\Payment\AfterSale::get_instance()->get_user_tickets($user_id, 50);
return ['items' => $items];
}
/**
* User submits return shipping info.
*/
public function submit_after_sale_return(int $user_id, int $ticket_id, array $payload): array {
if ($user_id <= 0 || $ticket_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$carrier = sanitize_text_field((string) ($payload['carrier'] ?? ''));
$tracking = sanitize_text_field((string) ($payload['tracking_no'] ?? ''));
if ($carrier === '' || $tracking === '') {
return ['success' => false, 'message' => __('请填写快递公司与运单号。', 'aksrt')];
}
$ok = \AKSRT\Payment\AfterSale::get_instance()->user_submit_return_shipping($ticket_id, $user_id, $carrier, $tracking);
if (!$ok) {
return ['success' => false, 'message' => __('提交退货物流失败,请确认工单状态。', 'aksrt')];
}
$ticket = \AKSRT\Payment\AfterSale::get_instance()->get($ticket_id);
return [
'success' => true,
'message' => __('退货物流已提交', 'aksrt'),
'data' => $ticket ? \AKSRT\Payment\AfterSale::get_instance()->decorate_user_ticket($ticket) : ['id' => $ticket_id],
];
}
/**
* User cancels after-sale ticket.
*/
public function cancel_after_sale(int $user_id, int $ticket_id, string $reason = ''): array {
if ($user_id <= 0 || $ticket_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$ok = \AKSRT\Payment\AfterSale::get_instance()->user_cancel($ticket_id, $user_id, $reason);
if (!$ok) {
return ['success' => false, 'message' => __('无法取消该售后工单。', 'aksrt')];
}
return ['success' => true, 'message' => __('已取消售后申请', 'aksrt')];
}
public function get_product_archive_view(array $args = []): array {
$taxonomy = sanitize_key((string) ($args['taxonomy'] ?? ''));
$term_id = (int) ($args['term_id'] ?? 0);
$paged = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1));
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : [];
$per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12))));
// Normalize orderby aliases (zibll + legacy AKSRT keys).
$orderby_raw = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date')));
$order = strtoupper(sanitize_key((string) ($args['order'] ?? '')));
if (!in_array($order, ['ASC', 'DESC'], true)) {
$order = '';
}
// Legacy split price keys → price + order.
if ($orderby_raw === 'price_asc') {
$orderby_raw = 'price';
$order = 'ASC';
} elseif ($orderby_raw === 'price_desc') {
$orderby_raw = 'price';
$order = 'DESC';
} elseif ($orderby_raw === 'sales_volume') {
$orderby_raw = 'sales';
} elseif ($orderby_raw === 'zibpay_price') {
$orderby_raw = 'price';
} elseif ($orderby_raw === 'comments') {
$orderby_raw = 'score';
}
$allowed_orderby = ['date', 'views', 'favorite_count', 'score', 'sales', 'price', 'modified'];
if (!in_array($orderby_raw, $allowed_orderby, true)) {
$orderby_raw = 'date';
}
if ($orderby_raw === 'price' && $order === '') {
$order = 'ASC';
}
if ($order === '' && $orderby_raw !== 'price') {
$order = 'DESC';
}
$orderby = $orderby_raw;
$paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number')));
if (!in_array($paging, ['number', 'ajax'], true)) {
$paging = 'number';
}
$ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']);
$ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5))));
$style = sanitize_key((string) ($list['style'] ?? 'default'));
$empty = [
'products' => [],
'term' => null,
'total' => 0,
'max_pages' => 0,
'page' => $paged,
'orderby' => $orderby,
'order' => $order,
'paging' => $paging,
'ias_s' => $ias_s,
'ias_max' => $ias_max,
'per_page' => $per_page,
'style' => $style,
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
'filter' => null,
];
if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) {
return $empty;
}
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $per_page,
'paged' => $paged,
'post_status' => 'publish',
'order' => $order ?: 'DESC',
'orderby' => 'date',
];
$term = null;
if ($taxonomy !== '' && $term_id > 0) {
$term = get_term($term_id, $taxonomy);
if (is_wp_error($term) || !$term) {
return $empty;
}
$query_args['tax_query'] = [[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $term_id,
]];
}
if ($orderby === 'sales') {
$query_args['meta_key'] = '_aksrt_sales';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'price') {
$query_args['meta_key'] = '_aksrt_price';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = $order === 'ASC' ? 'ASC' : 'DESC';
} elseif ($orderby === 'favorite_count') {
$query_args['meta_key'] = '_aksrt_favorite_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'views') {
$query_args['meta_key'] = '_aksrt_views';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'score') {
$query_args['meta_key'] = '_aksrt_score_average';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'modified') {
$query_args['orderby'] = 'modified';
$query_args['order'] = 'DESC';
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
$filter = null;
if ($taxonomy === 'aksrt_product_cat' && $term) {
$filter = $this->build_product_cat_filter((int) $term->term_id, $orderby, $order);
}
return [
'products' => $products,
'term' => $term ? [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'count' => (int) $term->count,
'taxonomy' => $taxonomy,
'parent' => (int) $term->parent,
'url' => get_term_link($term),
] : null,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
'page' => $paged,
'orderby' => $orderby,
'order' => $order,
'paging' => $paging,
'ias_s' => $ias_s,
'ias_max' => $ias_max,
'per_page' => $per_page,
'style' => $style,
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
'filter' => $filter,
];
}
/**
* Hierarchical cat filter data (zibll zib_shop_get_cat_filter).
*
* @return array{siblings:array<int,array>,children:array<int,array>,parent_id:int,mode:string}
*/
public function build_product_cat_filter(int $cat_id, string $orderby = 'date', string $order = 'DESC'): array {
$taxonomy = 'aksrt_product_cat';
$cat = get_term($cat_id, $taxonomy);
if (!$cat || is_wp_error($cat)) {
return ['siblings' => [], 'children' => [], 'parent_id' => 0, 'mode' => 'flat', 'orderby' => $orderby, 'order' => $order];
}
$map_term = static function ($t) use ($orderby, $order): array {
$url = get_term_link($t);
if (!is_wp_error($url) && $orderby) {
$url = add_query_arg(array_filter([
'orderby' => $orderby,
'order' => $orderby === 'price' ? $order : null,
]), $url);
}
return [
'id' => (int) $t->term_id,
'name' => (string) $t->name,
'url' => is_wp_error($url) ? '' : (string) $url,
'count' => (int) $t->count,
];
};
$parent_id = (int) $cat->parent;
$children = get_terms([
'taxonomy' => $taxonomy,
'parent' => $cat_id,
'hide_empty' => false,
]);
if (is_wp_error($children)) {
$children = [];
}
if ($parent_id > 0) {
$siblings = get_terms([
'taxonomy' => $taxonomy,
'parent' => $parent_id,
'hide_empty' => false,
]);
} else {
$siblings = get_terms([
'taxonomy' => $taxonomy,
'parent' => 0,
'hide_empty' => false,
]);
}
if (is_wp_error($siblings)) {
$siblings = [];
}
$mode = 'siblings';
$parent_terms = [];
if (empty($children) && $parent_id > 0) {
$parent_term = get_term($parent_id, $taxonomy);
if ($parent_term && !is_wp_error($parent_term) && (int) $parent_term->parent > 0) {
$parent_terms = get_terms([
'taxonomy' => $taxonomy,
'parent' => (int) $parent_term->parent,
'hide_empty' => false,
]);
if (is_wp_error($parent_terms)) {
$parent_terms = [];
}
$mode = 'parent_siblings';
} elseif ($parent_id > 0) {
$parent_terms = get_terms([
'taxonomy' => $taxonomy,
'parent' => 0,
'hide_empty' => false,
]);
if (is_wp_error($parent_terms)) {
$parent_terms = [];
}
$mode = 'parent_siblings';
}
} elseif (!empty($children)) {
$mode = 'with_children';
}
$parent_url = '';
if ($parent_id > 0) {
$plink = get_term_link($parent_id, $taxonomy);
if (!is_wp_error($plink)) {
$parent_url = add_query_arg(array_filter([
'orderby' => $orderby,
'order' => $orderby === 'price' ? $order : null,
]), $plink);
}
}
return [
'mode' => $mode,
'current_id' => $cat_id,
'parent_id' => $parent_id,
'parent_url' => $parent_url,
'siblings' => array_map($map_term, empty($parent_terms) ? $siblings : $parent_terms),
'children' => array_map($map_term, $mode === 'with_children' ? $children : ($mode === 'parent_siblings' ? $siblings : [])),
'child_tabs' => array_map($map_term, $children),
'orderby' => $orderby,
'order' => $order,
];
}
public function get_product_categories(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_product_cat',
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
]);
if (is_wp_error($terms)) {
return [];
}
return array_map(static function ($t) {
return [
'id' => $t->term_id,
'name' => $t->name,
'count' => $t->count,
'url' => get_term_link($t),
];
}, $terms);
}
/* ============================================================
* Merchant / Store Mode (§6.10)
* ============================================================ */
public function get_store_profile(int $author_id): array {
if ($author_id <= 0) return [];
$user = get_userdata($author_id);
if (!$user) return [];
$product_count = $this->count_author_products($author_id);
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$author_show = !isset($shop_settings['general']['shop_author_show']) || !empty($shop_settings['general']['shop_author_show']);
if (!$author_show) return [];
$contact = $this->get_store_contact($author_id);
return [
'author_id' => $author_id,
'name' => (string) $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id) : get_avatar_url($author_id),
'description' => (string) get_the_author_meta('description', $author_id),
'url' => get_author_posts_url($author_id),
'product_count' => $product_count,
'contact' => $contact,
'addresses' => $this->get_address_book($author_id),
];
}
public function get_store_products(int $author_id, int $page = 1, int $per_page = 12): array {
$query = new \WP_Query([
'post_type' => 'aksrt_product',
'author' => $author_id,
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
]);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
return [
'items' => $products,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
public function count_author_products(int $author_id): int {
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_product' AND post_status = 'publish' AND post_author = %d",
$author_id
));
}
private function get_store_contact(int $author_id): array {
$raw = get_user_meta($author_id, 'aksrt_shop_contact', true);
if (!is_array($raw)) return [];
$contacts = [];
foreach ($raw as $c) {
$name = sanitize_text_field((string) ($c['name'] ?? ''));
if ($name === '') continue;
$contacts[] = [
'name' => $name,
'icon' => sanitize_text_field((string) ($c['icon'] ?? '')),
'link' => esc_url_raw((string) ($c['link'] ?? '')),
'qr' => esc_url_raw((string) ($c['qr'] ?? '')),
];
}
return $contacts;
}
public function get_stats(): array {
$product_count = 0;
$post_type_obj = get_post_type_object('aksrt_product');
if ($post_type_obj) {
$counts = wp_count_posts('aksrt_product');
$product_count = (int) ($counts->publish ?? 0);
}
return [
'product_count' => $product_count,
'order_count' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['total'] : 0,
'revenue_total' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['revenue'] : 0,
];
}
public function resolve_main_image(int $product_id): string {
return $this->product_service()->resolve_main_image($product_id);
}
public function build_product_card_view(int $product_id): array {
return $this->product_service()->build_product_card_view($product_id);
}
private function decorate_order_view(array $order, bool $with_detail = false): array {
$meta = $order['meta_payload'] ?? [];
$order_number = (string) ($order['order_number'] ?? '');
$order_items = $this->get_order_items($order_number);
$has_structured_items = !empty($order_items);
$product = $has_structured_items
? $this->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0))
: $this->build_product_card_view((int) ($order['product_id'] ?? 0));
$shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : [];
$after_sale_records = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list([
'limit' => 20,
'offset' => 0,
'order_number' => $order_number,
]) : ['items' => []];
$status = (string) ($order['status'] ?? \AKSRT\Payment\Order::STATUS_PENDING);
$subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount']);
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
$discount = (float) ($meta['discount'] ?? 0);
$expires_at = (string) ($order['expires_at'] ?? '');
$payment_value = (string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance');
$view = [
'id' => (int) ($order['id'] ?? 0),
'order_number' => (string) ($order['order_number'] ?? ''),
'amount' => (float) ($order['amount'] ?? 0),
'status' => $status,
'status_label' => \AKSRT\Payment\Order::get_valid_statuses()[$status] ?? $status,
'product_name' => $product['title'] ?? __('商品订单', 'aksrt'),
'product' => $product,
'created_at' => (string) ($order['created_at'] ?? ''),
'paid_at' => (string) ($order['paid_at'] ?? ''),
'expires_at' => $expires_at,
'currency_symbol' => '¥',
'subtotal' => $subtotal,
'shipping_fee' => $shipping_fee,
'discount' => $discount,
'address' => $meta['address'] ?? [],
'payment_method' => $meta['payment_method'] ?? ['value' => $order['payment_method'] ?? '', 'label' => __('站内余额', 'aksrt')],
'coupon' => $meta['coupon'] ?? [],
'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []),
'shipping_method' => $meta['shipping_method'] ?? [],
'customer_note' => $meta['customer_note'] ?? '',
'pay_url' => '#',
'actions' => [
'can_pay' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true) && $payment_value !== 'manual_review',
'can_cancel' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true),
'can_after_sale' => in_array($status, [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true),
'can_receipt' => $status === \AKSRT\Payment\Order::STATUS_DELIVERED,
],
'time_left' => $this->get_order_countdown($expires_at, $status),
];
if ($with_detail) {
$view['shipping_records'] = $shipping_records;
$view['after_sale_records'] = $after_sale_records['items'] ?? [];
$view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_records['items'] ?? []);
}
return $view;
}
/**
* Shop-side timeline; keep behaviour aligned with UserOrders.
*
* @param array<string, mixed> $order
* @param array<int, array> $shipping_records
* @param array<int, array> $after_sale_records
* @return array<int, array{time:string,label:string,key?:string}>
*/
private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array {
// Prefer the unified order-center builder when available.
if (class_exists('\AKSRT\Payment\UserOrders')) {
$view = \AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, true);
if (is_array($view['timeline'] ?? null)) {
return $view['timeline'];
}
}
$timeline = [];
$created = (string) ($order['created_at'] ?? '');
if ($created !== '') {
$timeline[] = ['key' => 'created', 'time' => $created, 'label' => __('订单创建', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
$paid_like = [
\AKSRT\Payment\Order::STATUS_PAID,
\AKSRT\Payment\Order::STATUS_DELIVERED,
\AKSRT\Payment\Order::STATUS_COMPLETED,
\AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
\AKSRT\Payment\Order::STATUS_REFUNDED,
\AKSRT\Payment\Order::STATUS_REFUNDING,
];
if (!empty($order['paid_at']) || in_array($status, $paid_like, true)) {
$timeline[] = [
'key' => 'paid',
'time' => (string) ($order['paid_at'] ?? $created),
'label' => __('完成支付', 'aksrt'),
];
}
foreach ($shipping_records as $record) {
$timeline[] = [
'key' => 'shipping',
'time' => (string) ($record['created_at'] ?? ''),
'label' => sprintf(__('物流更新:%s', 'aksrt'), $record['tracking_no'] ?? ''),
];
}
foreach ($after_sale_records as $record) {
$timeline[] = [
'key' => 'after_sale',
'time' => (string) ($record['created_at'] ?? ''),
'label' => sprintf(__('售后申请:%s', 'aksrt'), $record['reason'] ?? ''),
];
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
});
return $timeline;
}
private function get_order_countdown(string $expires_at, string $status): int {
if ($expires_at === '' || !in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) {
return 0;
}
$left = strtotime($expires_at) - time();
return max(0, $left);
}
public function get_limit_buy_config(int $product_id): array {
return $this->product_service()->get_limit_buy_config($product_id);
}
public function get_product_services(int $product_id): array {
return $this->product_service()->get_product_services($product_id);
}
public function get_product_params(int $product_id): array {
return $this->product_service()->get_product_params($product_id);
}
public function get_address_book(int $user_id): array {
return $this->address_service()->get_address_book($user_id);
}
public function add_address(int $user_id, array $data): array {
return $this->address_service()->add_address($user_id, $data);
}
public function update_address(int $user_id, int $address_id, array $data): array {
return $this->address_service()->update_address($user_id, $address_id, $data);
}
public function delete_address(int $user_id, int $address_id): array {
return $this->address_service()->delete_address($user_id, $address_id);
}
public function set_default_address(int $user_id, int $address_id): array {
return $this->address_service()->set_default_address($user_id, $address_id);
}
public function get_payment_method_options(): array {
return $this->product_service()->get_payment_method_options();
}
public function get_shipping_method_options(): array {
return $this->product_service()->get_shipping_method_options();
}
/* ============================================================
* Product Variants / SKU
* ============================================================ */
public function get_variants(int $product_id): array {
return $this->product_service()->get_variants($product_id);
}
public function get_variant(int $variant_id): ?array {
return $this->product_service()->get_variant($variant_id);
}
}
-436
View File
@@ -1,436 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part2 {
public function save_variants(int $product_id, array $variants): array {
return $this->product_service()->save_variants($product_id, $variants);
}
public function delete_variant(int $variant_id): bool {
return $this->product_service()->delete_variant($variant_id);
}
public function has_variants(int $product_id): bool {
return $this->product_service()->has_variants($product_id);
}
/**
* Get the price range for a product (considering variants).
*/
public function get_price_range(int $product_id): array {
$base_price = (float) get_post_meta($product_id, '_aksrt_price', true);
$variants = $this->get_variants($product_id);
if (empty($variants)) {
return ['min' => $base_price, 'max' => $base_price, 'has_variants' => false];
}
$prices = array_column($variants, 'price');
$prices[] = $base_price;
$prices = array_filter($prices, fn($p) => $p > 0);
if (empty($prices)) {
return ['min' => 0, 'max' => 0, 'has_variants' => true];
}
return [
'min' => min($prices),
'max' => max($prices),
'has_variants' => true,
];
}
// ── R68: Fulfillment hook (§6.5) ──
/**
* aksrt_order_paid listener: deduct stock + increment sales.
* Registered via \AKSRT\Core\Setup::init().
*/
public function handle_order_paid(string $order_number, array $order): void {
if ($this->order_item_table() !== '') {
$items = $this->get_order_items($order_number);
if (!empty($items)) {
$this->handle_order_paid_enhanced($order_number, $order);
return;
}
}
$note_raw = (string) ($order['note'] ?? '');
$meta = json_decode($note_raw, true);
if (!is_array($meta)) {
$meta = [];
}
$items = $meta['items'] ?? [];
if (empty($items) && !empty($order['product_id'])) {
$items = [
[
'product_id' => (int) $order['product_id'],
'quantity' => 1,
],
];
}
foreach ($items as $item) {
$pid = (int) ($item['product_id'] ?? 0);
$qty = max(1, (int) ($item['quantity'] ?? 1));
if ($pid <= 0) {
continue;
}
// Deduct stock (skip if unlimited, i.e. stock <= -1)
$stock = (int) get_post_meta($pid, '_aksrt_stock', true);
if ($stock > 0) {
$new_stock = max(0, $stock - $qty);
update_post_meta($pid, '_aksrt_stock', $new_stock);
}
// Increment sales_volume
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty);
}
/**
* Fires after shop fulfillment (stock + sales).
*
* @param string $order_number
* @param array $order
* @param array $items Normalized item list
*/
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
}
// ── R68: Confirm receipt (§6.5) ──
/**
* User confirms receipt: delivered -> completed.
*/
public function confirm_receipt(int $user_id, string $order_number): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if ($status !== \AKSRT\Payment\Order::STATUS_DELIVERED) {
return ['success' => false, 'message' => __('当前订单状态不可确认收货。', 'aksrt')];
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => __('用户确认收货。', 'aksrt'),
]);
return [
'success' => true,
'data' => [
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
/**
* Auto-confirm cron: complete orders delivered > receipt_max_day ago.
*/
public function run_auto_confirm_cron(): void {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = (int) ($shop['timing']['receipt_max_day'] ?? 15);
if ($max_day <= 0) {
$max_day = 15;
}
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin(['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'limit' => 500]);
foreach ($orders as $order) {
$delivered_at = (string) ($order['delivered_at'] ?? '');
if ($delivered_at === '' || $delivered_at > $cutoff) {
continue;
}
$repo->update_status((string) $order['order_number'], \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => sprintf(__('系统自动确认收货(超过 %d 天)。', 'aksrt'), $max_day),
]);
}
}
// ── R68: Order CSV export (§6.5) ──
/**
* Export orders as CSV rows. Admin-only.
*/
public function export_orders_csv(array $args = []): array {
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
return ['csv' => '', 'count' => 0];
}
$status = sanitize_key((string) ($args['status'] ?? ''));
$date_from = sanitize_text_field((string) ($args['date_from'] ?? ''));
$date_to = sanitize_text_field((string) ($args['date_to'] ?? ''));
$limit = max(1, min(5000, (int) ($args['limit'] ?? 1000)));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin(['status' => $status, 'limit' => $limit]);
if ($date_from !== '') {
$orders = array_filter($orders, function ($o) use ($date_from) {
return ($o['created_at'] ?? '') >= $date_from . ' 00:00:00';
});
}
if ($date_to !== '') {
$orders = array_filter($orders, function ($o) use ($date_to) {
return ($o['created_at'] ?? '') <= $date_to . ' 23:59:59';
});
}
$header = ['order_number', 'user_id', 'product_type', 'product_id', 'amount', 'currency', 'status', 'payment_method', 'created_at', 'paid_at'];
$rows = [implode(',', $header)];
foreach ($orders as $o) {
$rows[] = implode(',', [
(string) ($o['order_number'] ?? ''),
(int) ($o['user_id'] ?? 0),
(string) ($o['product_type'] ?? ''),
(int) ($o['product_id'] ?? 0),
(float) ($o['amount'] ?? 0),
(string) ($o['currency'] ?? 'CNY'),
(string) ($o['status'] ?? ''),
(string) ($o['payment_method'] ?? ''),
(string) ($o['created_at'] ?? ''),
(string) ($o['paid_at'] ?? ''),
]);
}
$bom = "\xEF\xBB\xBF";
$csv = $bom . implode("\r\n", $rows);
return ['csv' => $csv, 'count' => count($orders)];
}
// ── R68: Configurable shipping/payment (§6.11) ──
/**
* Get payment method options from settings, falling back to hardcoded defaults.
*/
public function get_payment_method_options_configurable(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->get_payment_method_options();
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$enabled = (string) ($shop['product_defaults']['payment_methods'] ?? '');
if ($enabled === '') {
return $this->get_payment_method_options();
}
$all = $this->get_payment_method_options();
$keys = array_map('trim', explode(',', $enabled));
$result = [];
foreach ($keys as $k) {
$k = sanitize_key($k);
if (isset($all[$k])) {
$result[$k] = $all[$k];
}
}
return $result ?: $this->get_payment_method_options();
}
/**
* Get shipping method options from settings, falling back to hardcoded defaults.
*/
public function get_shipping_method_options_configurable(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->get_shipping_method_options();
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$enabled = (string) ($shop['product_defaults']['shipping_methods'] ?? '');
$fee_override = (float) ($shop['product_defaults']['shipping_fee_fixed'] ?? 0);
if ($enabled === '' && $fee_override <= 0) {
return $this->get_shipping_method_options();
}
$all = $this->get_shipping_method_options();
if ($enabled !== '') {
$keys = array_map('trim', explode(',', $enabled));
$filtered = [];
foreach ($keys as $k) {
$k = sanitize_key($k);
if (isset($all[$k])) {
$filtered[$k] = $all[$k];
}
}
$all = $filtered ?: $all;
}
if ($fee_override > 0) {
foreach ($all as $k => &$v) {
if ($k !== 'download') {
$v['fee'] = $fee_override;
}
}
unset($v);
}
return $all;
}
/**
* Get after-sale reason options from settings.
*/
public function get_after_sale_reason_options(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return ['refund' => [], 'return' => []];
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$refund_raw = (string) ($shop['timing']['after_sale_reason_refund'] ?? '');
$return_raw = (string) ($shop['timing']['after_sale_reason_return'] ?? '');
return [
'refund' => $this->parse_reason_lines($refund_raw),
'return' => $this->parse_reason_lines($return_raw),
];
}
/**
* Parse newline-separated reason text into indexed array.
*/
private function parse_reason_lines(string $text): array {
if ($text === '') {
return [];
}
$lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $text)));
$result = [];
foreach (array_values($lines) as $i => $line) {
$result[] = [
'id' => $i + 1,
'label' => $line,
];
}
return $result;
}
/* ============================================================
* Product Options (§6.2 选项组机制)
*
* Storage: post_meta '_aksrt_product_options' = [
* [ 'name' => '颜色', 'view_mode' => 'list', 'values' => [
* [ 'label' => '红色', 'price_change' => 0, 'image' => '' ],
* [ 'label' => '蓝色', 'price_change' => 10, 'image' => 'url' ],
* ]],
* ]
*
* options_key = "红色|M" (pipe-delimited selected labels per group)
* ============================================================ */
public function get_product_options(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_product_options', true);
if (!is_array($raw) || empty($raw)) {
return [];
}
return array_values(array_filter(array_map([$this, 'sanitize_option_group'], $raw)));
}
public function save_product_options(int $product_id, array $groups): void {
$sanitized = array_values(array_filter(array_map([$this, 'sanitize_option_group'], $groups)));
update_post_meta($product_id, '_aksrt_product_options', $sanitized);
update_post_meta($product_id, '_aksrt_has_options', count($sanitized) > 0 ? '1' : '0');
}
public function has_options(int $product_id): bool {
return get_post_meta($product_id, '_aksrt_has_options', true) === '1';
}
private function sanitize_option_group($group): array {
if (!is_array($group)) return [];
$name = sanitize_text_field((string) ($group['name'] ?? ''));
if ($name === '') return [];
$view_mode_raw = $group['view_mode'] ?? '';
$view_mode = in_array($view_mode_raw, ['list', 'img'], true) ? $view_mode_raw : 'list';
$values = [];
foreach ((array) ($group['values'] ?? []) as $v) {
$label = sanitize_text_field((string) ($v['label'] ?? ''));
if ($label === '') continue;
$values[] = [
'label' => $label,
'price_change' => round((float) ($v['price_change'] ?? 0), 2),
'image' => esc_url_raw((string) ($v['image'] ?? '')),
];
}
if (empty($values)) return [];
return ['name' => $name, 'view_mode' => $view_mode, 'values' => $values];
}
public function build_options_key(array $selections): string {
return implode('|', array_map('sanitize_text_field', $selections));
}
public function resolve_option_price(int $product_id, string $options_key): float {
$base = (float) get_post_meta($product_id, '_aksrt_price', true);
$start = (float) get_post_meta($product_id, '_aksrt_start_price', true);
if ($start > 0) {
$base = $start;
}
if ($options_key === '' || $options_key === '_default') {
return $base;
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
foreach ($groups as $i => $group) {
$selected_label = $selections[$i] ?? '';
foreach ($group['values'] as $val) {
if ($val['label'] === $selected_label) {
$base += $val['price_change'];
break;
}
}
}
return max(0, round($base, 2));
}
public function resolve_option_labels(int $product_id, string $options_key): string {
if ($options_key === '' || $options_key === '_default') {
return '';
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
$parts = [];
foreach ($groups as $i => $group) {
$sel = $selections[$i] ?? '';
if ($sel !== '') {
$parts[] = $group['name'] . ':' . $sel;
}
}
return implode(', ', $parts);
}
public function resolve_option_stock(int $product_id, string $options_key): int {
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
$stock_matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
if (is_array($stock_matrix) && isset($stock_matrix[$options_key])) {
return (int) $stock_matrix[$options_key];
}
return 0;
}
return (int) get_post_meta($product_id, '_aksrt_stock', true);
}
}
-399
View File
@@ -1,399 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part3 {
public function deduct_option_stock(int $product_id, string $options_key, int $qty): void {
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
$matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
if (is_array($matrix) && isset($matrix[$options_key])) {
$matrix[$options_key] = max(0, (int) $matrix[$options_key] - $qty);
update_post_meta($product_id, '_aksrt_stock_opts', $matrix);
}
return;
}
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
if ($stock > 0) {
update_post_meta($product_id, '_aksrt_stock', max(0, $stock - $qty));
}
}
public function get_option_image(int $product_id, string $options_key): string {
if ($options_key === '' || $options_key === '_default') {
return '';
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
foreach ($groups as $i => $group) {
$sel = $selections[$i] ?? '';
foreach ($group['values'] as $val) {
if ($val['label'] === $sel && !empty($val['image'])) {
return $val['image'];
}
}
}
return '';
}
/* ============================================================
* Auto-delivery (§6.2 自动发货)
*
* Storage: post_meta '_aksrt_auto_delivery' = [
* 'type' => 'fixed'|'invit_code'|'card_pass'|'opts',
* 'content' => '固定内容文本',
* 'opts' => [ options_key => '该选项的发货内容' ],
* ]
* ============================================================ */
public function get_auto_delivery_config(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_auto_delivery', true);
if (!is_array($raw)) {
return ['type' => '', 'content' => '', 'opts' => []];
}
return [
'type' => sanitize_key((string) ($raw['type'] ?? '')),
'content' => (string) ($raw['content'] ?? ''),
'opts' => is_array($raw['opts'] ?? null) ? $raw['opts'] : [],
];
}
public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id = 0): string {
$config = $this->get_auto_delivery_config($product_id);
$type = $config['type'];
if ($type === 'fixed') {
return (string) $config['content'];
}
if ($type === 'opts') {
$key = ($options_key !== '' && $options_key !== '_default') ? $options_key : '_default';
return (string) ($config['opts'][$key] ?? $config['content']);
}
if ($type === 'card_pass') {
return $this->consume_card_keys($product_id, $options_key, $qty, $user_id, $order_number, $order_item_id);
}
if ($type === 'invit_code') {
return $this->create_invite_codes($qty, $user_id, $order_number);
}
return '';
}
private function auto_delivery_succeeded(int $product_id, string $content, int $qty): bool {
$config = $this->get_auto_delivery_config($product_id);
if (in_array($config['type'], ['card_pass', 'invit_code'], true)) {
$lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $content) ?: [])));
return count($lines) === max(1, $qty)
&& !in_array($content, [__('卡密已售罄', 'aksrt'), __('邀请系统不可用', 'aksrt')], true);
}
return trim($content) !== '';
}
private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id): string {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('card_key');
if ($table === '' || $order_item_id <= 0 || $wpdb->query('START TRANSACTION') === false) return '';
$where_opt = '';
$params = [$product_id];
if ($options_key !== '' && $options_key !== '_default') {
$where_opt = ' AND options_key = %s';
$params[] = $options_key;
} else {
$where_opt = " AND (options_key = '' OR options_key = '_default')";
}
$params[] = $qty;
$keys = $wpdb->get_results($wpdb->prepare(
"SELECT id, card_code FROM {$table} WHERE product_id = %d AND status = 'available'{$where_opt} ORDER BY id ASC LIMIT %d FOR UPDATE",
...$params
), ARRAY_A);
if (count((array) $keys) !== $qty) {
$wpdb->query('ROLLBACK');
return __('卡密已售罄', 'aksrt');
}
$codes = [];
$ids = [];
foreach ($keys as $key) {
$codes[] = (string) $key['card_code'];
$ids[] = (int) $key['id'];
}
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE status = 'available' AND id IN ({$placeholders})",
array_merge([$order_number, $user_id, current_time('mysql')], $ids)
));
$content = implode("\n", $codes);
$item_table = $this->order_item_table();
$persisted = $item_table !== '' ? $wpdb->update(
$item_table,
['auto_delivery_content' => $content],
['id' => $order_item_id, 'order_number' => $order_number],
['%s'],
['%d', '%s']
) : false;
if ($updated !== $qty || $persisted === false || $wpdb->query('COMMIT') === false) {
$wpdb->query('ROLLBACK');
return '';
}
return $content;
}
private function create_invite_codes(int $qty, int $user_id, string $order_number): string {
if (!class_exists('\AKSRT\Credits\Invite')) {
return __('邀请系统不可用', 'aksrt');
}
$invite = \AKSRT\Credits\Invite::get_instance();
$codes = [];
for ($i = 0; $i < $qty; $i++) {
$code = $invite->generate_code($user_id);
if ($code) {
$codes[] = $code;
}
}
return implode("\n", $codes);
}
/* ============================================================
* Order Items (§6.5 多商品订单子表)
* ============================================================ */
private function order_item_table(): string {
return \AKSRT\Core\DBMigrator::table('order_item');
}
public function save_order_items(string $order_number, array $items): void {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return;
foreach ($items as $item) {
$wpdb->insert($table, [
'order_number' => $order_number,
'product_id' => (int) ($item['product_id'] ?? 0),
'product_title' => sanitize_text_field((string) ($item['title'] ?? '')),
'options_key' => sanitize_text_field((string) ($item['options_key'] ?? '')),
'options_label' => sanitize_text_field((string) ($item['options_label'] ?? '')),
'quantity' => max(1, (int) ($item['quantity'] ?? 1)),
'unit_price' => (float) ($item['unit_price'] ?? $item['price'] ?? 0),
'total_price' => (float) ($item['total_price'] ?? $item['line_total'] ?? 0),
'discount_amount' => (float) ($item['discount_amount'] ?? 0),
'shipping_fee' => (float) ($item['shipping_fee'] ?? 0),
'pay_price' => (float) ($item['pay_price'] ?? $item['line_total'] ?? 0),
'shipping_type' => sanitize_key((string) ($item['shipping_type'] ?? 'express')),
'auto_delivery_content'=> ($item['auto_delivery_content'] ?? null),
'discount_hit' => isset($item['applied_discounts']) ? wp_json_encode($item['applied_discounts']) : null,
'gift_data' => isset($item['gifts']) ? wp_json_encode($item['gifts']) : null,
'image' => esc_url_raw((string) ($item['image'] ?? '')),
]);
}
}
public function get_order_items(string $order_number): array {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return [];
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table} WHERE order_number = %s ORDER BY id ASC",
$order_number
), ARRAY_A);
return array_map(static function ($row) {
return [
'id' => (int) $row['id'],
'product_id' => (int) $row['product_id'],
'product_title' => (string) $row['product_title'],
'options_key' => (string) $row['options_key'],
'options_label' => (string) $row['options_label'],
'quantity' => (int) $row['quantity'],
'unit_price' => (float) $row['unit_price'],
'total_price' => (float) $row['total_price'],
'discount_amount' => (float) $row['discount_amount'],
'shipping_fee' => (float) $row['shipping_fee'],
'pay_price' => (float) $row['pay_price'],
'shipping_type' => (string) $row['shipping_type'],
'auto_delivery_content' => $row['auto_delivery_content'],
'discount_hit' => json_decode($row['discount_hit'] ?? '', true) ?: [],
'gift_data' => json_decode($row['gift_data'] ?? '', true) ?: [],
'image' => (string) $row['image'],
];
}, (array) $rows);
}
/* ============================================================
* Enhanced Fulfillment (§6.5 履约链路)
* ============================================================ */
public function handle_order_paid_enhanced(string $order_number, array $order): void {
$items = $this->get_order_items($order_number);
if (empty($items)) {
$this->handle_order_paid($order_number, $order);
return;
}
$user_id = (int) ($order['user_id'] ?? 0);
$auto_delivery_parts = [];
$auto_delivery_successes = 0;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$qty = max(1, (int) $item['quantity']);
$opt_key = (string) $item['options_key'];
if ($pid <= 0) continue;
$this->deduct_option_stock($pid, $opt_key, $qty);
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty);
$shipping_type = (string) get_post_meta($pid, '_aksrt_shipping_type', true);
if ($shipping_type === 'auto') {
$content = $this->execute_auto_delivery($pid, $opt_key, $qty, $user_id, $order_number, (int) $item['id']);
if ($content !== '') {
$auto_delivery_parts[] = $content;
$config = $this->get_auto_delivery_config($pid);
$persisted = $config['type'] === 'card_pass'
? true
: $this->update_order_item_delivery($item['id'], $content);
if ($persisted && $this->auto_delivery_succeeded($pid, $content, $qty)) {
$auto_delivery_successes++;
}
}
}
}
$this->process_rebate($order_number, $order, $items);
$this->process_gifts($order_number, $order, $items);
if (!empty($auto_delivery_parts)) {
$this->send_auto_delivery_notification($user_id, $order_number, $auto_delivery_parts);
}
if ($auto_delivery_successes === count($items) && class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('自动发货订单已完成。', 'aksrt')]
);
}
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
}
private function update_order_item_delivery(int $item_id, string $content): bool {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return false;
$updated = $wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']);
if ($updated === false) return false;
if ($updated === 0) {
$stored = $wpdb->get_var($wpdb->prepare("SELECT auto_delivery_content FROM {$table} WHERE id = %d", $item_id));
return is_string($stored) && hash_equals($stored, $content);
}
return true;
}
private function send_auto_delivery_notification(int $user_id, string $order_number, array $parts): void {
if (!class_exists('AKSRT_Notification_Service')) return;
$content = sprintf(
__("订单 %s 已自动发货:\n\n%s", 'aksrt'),
$order_number,
implode("\n---\n", $parts)
);
AKSRT_Notification_Service::send($user_id, 'auto_delivery', $content, [
'order_number' => $order_number,
]);
if (function_exists('aksrt_send_event_email')) {
$user = get_userdata($user_id);
if ($user && !empty($user->user_email)) {
aksrt_send_event_email($user->user_email, sprintf(__('订单 %s 自动发货通知', 'aksrt'), $order_number), $content);
}
}
}
private function process_rebate(string $order_number, array $order, array $items): void {
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) return;
$referrer_id = 0;
if (class_exists('\AKSRT\Credits\Invite')) {
$referrer_id = \AKSRT\Credits\Invite::get_instance()->get_referrer($user_id);
}
if ($referrer_id <= 0) return;
$rebate_table = \AKSRT\Core\DBMigrator::table('rebate_log');
if ($rebate_table === '') return;
global $wpdb;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$pay_price = (float) $item['pay_price'];
if ($pid <= 0 || $pay_price <= 0) continue;
$rebate_rate = (float) get_post_meta($pid, '_aksrt_rebate_rate', true);
if ($rebate_rate <= 0 && class_exists('AKSRT\Shop\TermMeta')) {
$resolved = TermMeta::resolve_inheritance($pid, 'rebate');
$rebate_rate = (float) ($resolved ?: 0);
}
if ($rebate_rate <= 0) continue;
$amount = round($pay_price * $rebate_rate / 100, 2);
if ($amount <= 0) continue;
$wpdb->insert($rebate_table, [
'user_id' => $referrer_id,
'order_number' => $order_number,
'source_user_id' => $user_id,
'amount' => $amount,
'type' => 'order',
'status' => 'pending',
'created_at' => current_time('mysql'),
]);
}
}
private function process_gifts(string $order_number, array $order, array $items): void {
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) return;
$claim_key = '_aksrt_shop_gifts_' . sanitize_key($order_number);
if (!add_user_meta($user_id, $claim_key, 'processing', true)) return;
foreach ($items as $item) {
$gifts = $item['gift_data'] ?? [];
if (empty($gifts)) continue;
foreach ($gifts as $gift) {
$gift_type = (string) ($gift['type'] ?? '');
$gift_value = $gift['value'] ?? 0;
if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) {
\AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number));
} elseif (in_array($gift_type, ['level_integral', 'experience'], true) && class_exists('\AKSRT\User\Level') && (int) $gift_value > 0) {
\AKSRT\User\Level::add($user_id, 'shop_gift', (int) $gift_value, true);
} elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) {
$current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$base = max($current, time());
update_user_meta($user_id, 'aksrt_vip_expires', $base + ((int) $gift_value * DAY_IN_SECONDS));
// 赠送 VIP 天数时确保至少 VIP1;不降级已有 VIP2。
$level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
if ($level < 1) {
update_user_meta($user_id, 'aksrt_vip_level', 1);
}
}
}
}
update_user_meta($user_id, $claim_key, '1');
}
}
-341
View File
@@ -1,341 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductMetaBox {
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_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 10, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_commerce',
__('商品商业设置', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'high'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_product_meta_save', 'aksrt_product_meta_nonce');
$price = (float) get_post_meta($post->ID, '_aksrt_price', true);
$stock = (int) get_post_meta($post->ID, '_aksrt_stock', true);
$image = (string) get_post_meta($post->ID, '_aksrt_product_image', true);
$gallery = get_post_meta($post->ID, '_aksrt_product_gallery', true);
$gallery = is_array($gallery) ? $gallery : [];
$shipping_mode = (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true);
$shipping_fee = (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true);
$shipping_fee_type = (string) (get_post_meta($post->ID, '_aksrt_shipping_fee_type', true) ?: 'fixed');
$shipping_fee_free = (float) get_post_meta($post->ID, '_aksrt_shipping_fee_free_amount', true);
$shipping_delivery = (string) get_post_meta($post->ID, '_aksrt_shipping_delivery_desc', true);
$shipping_tip = (string) get_post_meta($post->ID, '_aksrt_shipping_tip', true);
$after_sale_policy = (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true);
$support_refund = get_post_meta($post->ID, '_aksrt_support_refund', true);
$support_refund = $support_refund === '' ? '1' : (string) $support_refund;
$service = (string) get_post_meta($post->ID, '_aksrt_service', true);
$service_tip = (string) get_post_meta($post->ID, '_aksrt_service_tip', true);
$limit_buy = get_post_meta($post->ID, '_aksrt_limit_buy', true);
$limit_buy = $limit_buy === '' ? -1 : (int) $limit_buy;
$limit_buy_note = (string) get_post_meta($post->ID, '_aksrt_limit_buy_note', true);
$stock_type = (string) (get_post_meta($post->ID, '_aksrt_stock_type', true) ?: 'all');
$pay_modo = (string) (get_post_meta($post->ID, '_aksrt_pay_modo', true) ?: '0');
$start_price = (float) get_post_meta($post->ID, '_aksrt_start_price', true);
$product_rebate = (string) get_post_meta($post->ID, '_aksrt_product_rebate', true);
$product_params = (string) get_post_meta($post->ID, '_aksrt_product_params', true);
$user_required = (string) get_post_meta($post->ID, '_aksrt_user_required', true);
$delivery_note = (string) get_post_meta($post->ID, '_aksrt_delivery_note', true);
$marketing = (string) get_post_meta($post->ID, '_aksrt_marketing_headline', true);
$shipping_modes = [
'' => __('默认(继承设置)', 'aksrt'),
'express' => __('快递物流', 'aksrt'),
'auto' => __('自动发货', 'aksrt'),
'manual' => __('手动发货', 'aksrt'),
'virtual' => __('虚拟商品', 'aksrt'),
];
$stock_types = [
'all' => __('统一库存', 'aksrt'),
'opts' => __('按选项/变体', 'aksrt'),
];
$fee_types = [
'free' => __('包邮', 'aksrt'),
'fixed' => __('固定运费', 'aksrt'),
'amount_free' => __('满额包邮', 'aksrt'),
];
?>
<style>
.aksrt-pmb-product .row { display:grid; grid-template-columns: 140px 1fr; gap: 10px 14px; margin: 0 0 12px; align-items:start; }
.aksrt-pmb-product .row label { font-weight:600; padding-top:6px; }
.aksrt-pmb-product input[type="text"],
.aksrt-pmb-product input[type="number"],
.aksrt-pmb-product input[type="url"],
.aksrt-pmb-product select,
.aksrt-pmb-product textarea { width:100%; max-width:560px; }
.aksrt-pmb-product .hint { color:#646970; font-size:12px; margin:4px 0 0; }
.aksrt-pmb-product .grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; max-width:720px; }
.aksrt-pmb-product h4 { margin:18px 0 10px; padding-top:12px; border-top:1px solid #dcdcde; }
</style>
<div class="aksrt-pmb-product">
<h4><?php esc_html_e('价格与库存', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_price"><?php esc_html_e('价格(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_price" name="aksrt_price" min="0" step="0.01" value="<?php echo esc_attr((string) $price); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock"><?php esc_html_e('库存', 'aksrt'); ?></label>
<input type="number" id="aksrt_stock" name="aksrt_stock" min="-1" step="1" value="<?php echo esc_attr((string) $stock); ?>">
<p class="hint"><?php esc_html_e('-1 表示无限库存。', 'aksrt'); ?></p>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_pay_modo"><?php esc_html_e('支付方式', 'aksrt'); ?></label>
<select id="aksrt_pay_modo" name="aksrt_pay_modo">
<option value="0" <?php selected($pay_modo, '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected($pay_modo, 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_start_price"><?php esc_html_e('划线价 / 起售价', 'aksrt'); ?></label>
<input type="number" id="aksrt_start_price" name="aksrt_start_price" min="0" step="0.01" value="<?php echo esc_attr((string) $start_price); ?>">
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock_type"><?php esc_html_e('库存类型', 'aksrt'); ?></label>
<select id="aksrt_stock_type" name="aksrt_stock_type">
<?php foreach ($stock_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($stock_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_limit_buy"><?php esc_html_e('限购数量', 'aksrt'); ?></label>
<input type="number" id="aksrt_limit_buy" name="aksrt_limit_buy" min="-1" step="1" value="<?php echo esc_attr((string) $limit_buy); ?>">
<p class="hint"><?php esc_html_e('-1 不限购。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_limit_buy_note"><?php esc_html_e('限购说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_limit_buy_note" name="aksrt_limit_buy_note" value="<?php echo esc_attr($limit_buy_note); ?>">
</div>
<div class="row">
<label for="aksrt_product_rebate"><?php esc_html_e('推广返佣', 'aksrt'); ?></label>
<div>
<input type="text" id="aksrt_product_rebate" name="aksrt_product_rebate" value="<?php echo esc_attr($product_rebate); ?>" placeholder="<?php esc_attr_e('如 10 或 10%', 'aksrt'); ?>">
<p class="hint"><?php esc_html_e('可填固定金额或百分比,具体解析由返佣服务处理。', 'aksrt'); ?></p>
</div>
</div>
<h4><?php esc_html_e('素材', 'aksrt'); ?></h4>
<div class="row">
<label for="aksrt_product_image"><?php esc_html_e('主图 URL', 'aksrt'); ?></label>
<div>
<input type="url" id="aksrt_product_image" name="aksrt_product_image" value="<?php echo esc_attr($image); ?>" placeholder="https://">
<p class="hint"><?php esc_html_e('也可使用特色图片;此处优先作为商品主图。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_product_gallery"><?php esc_html_e('图集 URL', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_gallery" name="aksrt_product_gallery" rows="4" placeholder="<?php esc_attr_e('每行一个图片 URL', 'aksrt'); ?>"><?php echo esc_textarea(implode("\n", $gallery)); ?></textarea>
</div>
</div>
<div class="row">
<label for="aksrt_product_params"><?php esc_html_e('商品参数', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_params" name="aksrt_product_params" rows="4" placeholder="<?php esc_attr_e('每行一条参数,如 品牌:AKSRT', 'aksrt'); ?>"><?php echo esc_textarea($product_params); ?></textarea>
</div>
</div>
<h4><?php esc_html_e('发货与售后', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_mode"><?php esc_html_e('发货方式', 'aksrt'); ?></label>
<select id="aksrt_shipping_mode" name="aksrt_shipping_mode">
<?php foreach ($shipping_modes as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_mode, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_type"><?php esc_html_e('运费类型', 'aksrt'); ?></label>
<select id="aksrt_shipping_fee_type" name="aksrt_shipping_fee_type">
<?php foreach ($fee_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_fee_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee"><?php esc_html_e('运费金额(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee" name="aksrt_shipping_fee" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_free_amount"><?php esc_html_e('满额包邮门槛', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee_free_amount" name="aksrt_shipping_fee_free_amount" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee_free); ?>">
</div>
</div>
<div class="row">
<label for="aksrt_shipping_delivery_desc"><?php esc_html_e('发货时效说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_delivery_desc" name="aksrt_shipping_delivery_desc" value="<?php echo esc_attr($shipping_delivery); ?>" placeholder="<?php esc_attr_e('如 付款后 24 小时内发货', 'aksrt'); ?>">
</div>
<div class="row">
<label for="aksrt_shipping_tip"><?php esc_html_e('物流提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_tip" name="aksrt_shipping_tip" value="<?php echo esc_attr($shipping_tip); ?>">
</div>
<div class="row">
<label for="aksrt_after_sale_policy"><?php esc_html_e('售后说明', 'aksrt'); ?></label>
<textarea id="aksrt_after_sale_policy" name="aksrt_after_sale_policy" rows="3"><?php echo esc_textarea($after_sale_policy); ?></textarea>
</div>
<div class="row">
<label for="aksrt_support_refund"><?php esc_html_e('支持退款', 'aksrt'); ?></label>
<div>
<label>
<input type="checkbox" id="aksrt_support_refund" name="aksrt_support_refund" value="1" <?php checked($support_refund, '1'); ?>>
<?php esc_html_e('允许申请退款/售后', 'aksrt'); ?>
</label>
</div>
</div>
<div class="row">
<label for="aksrt_service"><?php esc_html_e('服务承诺', 'aksrt'); ?></label>
<textarea id="aksrt_service" name="aksrt_service" rows="3" placeholder="<?php esc_attr_e('每行一条服务承诺', 'aksrt'); ?>"><?php echo esc_textarea($service); ?></textarea>
</div>
<div class="row">
<label for="aksrt_service_tip"><?php esc_html_e('服务提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_service_tip" name="aksrt_service_tip" value="<?php echo esc_attr($service_tip); ?>">
</div>
<div class="row">
<label for="aksrt_user_required"><?php esc_html_e('下单需填信息', 'aksrt'); ?></label>
<textarea id="aksrt_user_required" name="aksrt_user_required" rows="3" placeholder="<?php esc_attr_e('每行一项,如 QQ / 游戏角色名', 'aksrt'); ?>"><?php echo esc_textarea($user_required); ?></textarea>
</div>
<div class="row">
<label for="aksrt_delivery_note"><?php esc_html_e('发货备注', 'aksrt'); ?></label>
<input type="text" id="aksrt_delivery_note" name="aksrt_delivery_note" value="<?php echo esc_attr($delivery_note); ?>">
</div>
<div class="row">
<label for="aksrt_marketing_headline"><?php esc_html_e('营销标题', 'aksrt'); ?></label>
<input type="text" id="aksrt_marketing_headline" name="aksrt_marketing_headline" value="<?php echo esc_attr($marketing); ?>">
</div>
<p class="hint">
<?php esc_html_e('SKU 变体请使用下方「商品变体」面板;列表列已显示价格/库存/销量。', 'aksrt'); ?>
</p>
</div>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_meta_nonce'])), 'aksrt_product_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product') {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$price = isset($_POST['aksrt_price']) ? max(0, (float) wp_unslash($_POST['aksrt_price'])) : 0;
$stock = isset($_POST['aksrt_stock']) ? (int) wp_unslash($_POST['aksrt_stock']) : 0;
if ($stock < -1) {
$stock = -1;
}
$image = isset($_POST['aksrt_product_image']) ? esc_url_raw((string) wp_unslash($_POST['aksrt_product_image'])) : '';
$gallery_raw = isset($_POST['aksrt_product_gallery']) ? (string) wp_unslash($_POST['aksrt_product_gallery']) : '';
$gallery = array_values(array_filter(array_map(
static function ($line) {
return esc_url_raw(trim((string) $line));
},
preg_split('/\r\n|\r|\n/', $gallery_raw) ?: []
)));
$shipping_mode = isset($_POST['aksrt_shipping_mode']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_mode'])) : '';
$shipping_fee = isset($_POST['aksrt_shipping_fee']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee'])) : 0;
$shipping_fee_type = isset($_POST['aksrt_shipping_fee_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_fee_type'])) : 'fixed';
if (!in_array($shipping_fee_type, ['free', 'fixed', 'amount_free'], true)) {
$shipping_fee_type = 'fixed';
}
$shipping_fee_free = isset($_POST['aksrt_shipping_fee_free_amount']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee_free_amount'])) : 0;
$shipping_delivery = isset($_POST['aksrt_shipping_delivery_desc']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_delivery_desc'])) : '';
$shipping_tip = isset($_POST['aksrt_shipping_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_tip'])) : '';
$policy = isset($_POST['aksrt_after_sale_policy']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_after_sale_policy'])) : '';
$support_refund = !empty($_POST['aksrt_support_refund']) ? '1' : '0';
$service = isset($_POST['aksrt_service']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_service'])) : '';
$service_tip = isset($_POST['aksrt_service_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_service_tip'])) : '';
$limit_buy = isset($_POST['aksrt_limit_buy']) ? max(-1, min(9999, (int) wp_unslash($_POST['aksrt_limit_buy']))) : -1;
$limit_buy_note = isset($_POST['aksrt_limit_buy_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_limit_buy_note'])) : '';
$stock_type = isset($_POST['aksrt_stock_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_stock_type'])) : 'all';
if (!in_array($stock_type, ['all', 'opts'], true)) {
$stock_type = 'all';
}
$pay_modo = isset($_POST['aksrt_pay_modo']) ? sanitize_key((string) wp_unslash($_POST['aksrt_pay_modo'])) : '0';
if (!in_array($pay_modo, ['0', 'points'], true)) {
$pay_modo = '0';
}
$start_price = isset($_POST['aksrt_start_price']) ? max(0, (float) wp_unslash($_POST['aksrt_start_price'])) : 0;
$product_rebate = isset($_POST['aksrt_product_rebate']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_product_rebate'])) : '';
$product_params = isset($_POST['aksrt_product_params']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_product_params'])) : '';
$user_required = isset($_POST['aksrt_user_required']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_user_required'])) : '';
$delivery_note = isset($_POST['aksrt_delivery_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_delivery_note'])) : '';
$marketing = isset($_POST['aksrt_marketing_headline']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_marketing_headline'])) : '';
update_post_meta($post_id, '_aksrt_price', $price);
update_post_meta($post_id, '_aksrt_stock', $stock);
update_post_meta($post_id, '_aksrt_product_image', $image);
update_post_meta($post_id, '_aksrt_product_gallery', $gallery);
update_post_meta($post_id, '_aksrt_shipping_mode', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_type', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_fee', $shipping_fee);
update_post_meta($post_id, '_aksrt_shipping_fee_type', $shipping_fee_type);
update_post_meta($post_id, '_aksrt_shipping_fee_free_amount', $shipping_fee_free);
update_post_meta($post_id, '_aksrt_shipping_delivery_desc', $shipping_delivery);
update_post_meta($post_id, '_aksrt_shipping_tip', $shipping_tip);
update_post_meta($post_id, '_aksrt_after_sale_policy', $policy);
update_post_meta($post_id, '_aksrt_support_refund', $support_refund);
update_post_meta($post_id, '_aksrt_service', $service);
update_post_meta($post_id, '_aksrt_service_tip', $service_tip);
update_post_meta($post_id, '_aksrt_limit_buy', $limit_buy);
update_post_meta($post_id, '_aksrt_limit_buy_note', $limit_buy_note);
update_post_meta($post_id, '_aksrt_stock_type', $stock_type);
update_post_meta($post_id, '_aksrt_pay_modo', $pay_modo);
update_post_meta($post_id, '_aksrt_start_price', $start_price);
update_post_meta($post_id, '_aksrt_product_rebate', $product_rebate);
update_post_meta($post_id, '_aksrt_product_params', $product_params);
update_post_meta($post_id, '_aksrt_user_required', $user_required);
update_post_meta($post_id, '_aksrt_delivery_note', $delivery_note);
update_post_meta($post_id, '_aksrt_marketing_headline', $marketing);
// Keep sales counter present for list columns.
if (get_post_meta($post_id, '_aksrt_sales', true) === '') {
update_post_meta($post_id, '_aksrt_sales', 0);
}
}
}
-512
View File
@@ -1,512 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductReview {
private static $instance = null;
const COMMENT_TYPE = 'aksrt_product_review';
const RATING_LEVELS = [
5 => '超赞',
4 => '很棒',
3 => '不错',
2 => '一般',
1 => '较差',
0 => '很差',
];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('aksrt_daily_cron', [$this, 'run_auto_positive_cron']);
}
public function submit_review(int $user_id, string $order_number, int $product_id, array $data): array {
if ($user_id <= 0 || $order_number === '' || $product_id <= 0) {
return ['success' => false, 'message' => __('参数错误', 'aksrt')];
}
if (!$this->can_review($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')];
}
if ($this->has_reviewed($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('已评价过该商品', 'aksrt')];
}
$rating_product = max(0, min(5, (int) ($data['rating_product'] ?? 5)));
$rating_service = max(0, min(5, (int) ($data['rating_service'] ?? 5)));
$rating_shipping = max(0, min(5, (int) ($data['rating_shipping'] ?? 5)));
$average = round(($rating_product + $rating_service + $rating_shipping) / 3, 1);
$content = sanitize_textarea_field((string) ($data['content'] ?? ''));
$images = [];
$max_images = $this->get_max_images();
foreach ((array) ($data['images'] ?? []) as $img) {
if (count($images) >= $max_images) {
break;
}
$stored = $this->normalize_review_image((string) $img, $user_id);
if ($stored !== '') {
$images[] = $stored;
}
}
$comment_id = wp_insert_comment([
'comment_post_ID' => $product_id,
'user_id' => $user_id,
'comment_content' => $content ?: __('好评!', 'aksrt'),
'comment_type' => self::COMMENT_TYPE,
'comment_approved' => 1,
]);
if (!$comment_id) {
return ['success' => false, 'message' => __('评价提交失败', 'aksrt')];
}
update_comment_meta($comment_id, '_aksrt_order_number', $order_number);
update_comment_meta($comment_id, '_aksrt_rating_product', $rating_product);
update_comment_meta($comment_id, '_aksrt_rating_service', $rating_service);
update_comment_meta($comment_id, '_aksrt_rating_shipping', $rating_shipping);
update_comment_meta($comment_id, '_aksrt_rating_average', $average);
if (!empty($images)) {
update_comment_meta($comment_id, '_aksrt_review_images', $images);
}
$scores = $this->refresh_product_scores($product_id);
update_post_meta($product_id, '_aksrt_score_average', (float) ($scores['average'] ?? $average));
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
$status = (string) ($order['status'] ?? '');
if ($status === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('用户已评价', 'aksrt')]
);
}
}
return ['success' => true, 'comment_id' => $comment_id];
}
public function can_review(int $user_id, string $order_number, int $product_id): bool {
if (!class_exists('\AKSRT\Payment\OrderRepository')) return false;
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) return false;
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [
\AKSRT\Payment\Order::STATUS_COMPLETED,
\AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
\AKSRT\Payment\Order::STATUS_DELIVERED,
], true)) {
return false;
}
if (class_exists('\AKSRT\Payment\AfterSale')) {
$tickets = \AKSRT\Payment\AfterSale::get_instance()->get_list([
'order_number' => $order_number,
'limit' => 1,
]);
foreach (($tickets['items'] ?? []) as $ticket) {
if (in_array(($ticket['status'] ?? ''), ['pending', 'processing'], true)) {
return false;
}
}
}
return true;
}
public function has_reviewed(int $user_id, string $order_number, int $product_id): bool {
$comments = get_comments([
'post_id' => $product_id,
'user_id' => $user_id,
'type' => self::COMMENT_TYPE,
'number' => 1,
'meta_query' => [
['key' => '_aksrt_order_number', 'value' => $order_number],
],
]);
return !empty($comments);
}
public function get_reviews(int $product_id, array $args = []): array {
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 10)));
$filter = sanitize_key((string) ($args['filter'] ?? ''));
$query_args = [
'post_id' => $product_id,
'type' => self::COMMENT_TYPE,
'status' => 'approve',
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
'orderby' => 'comment_date',
'order' => 'DESC',
];
if ($filter === 'good') {
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 4, 'compare' => '>=', 'type' => 'NUMERIC']];
} elseif ($filter === 'bad') {
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 3, 'compare' => '<', 'type' => 'NUMERIC']];
} elseif ($filter === 'has_image') {
$query_args['meta_query'] = [['key' => '_aksrt_review_images', 'compare' => 'EXISTS']];
}
$comments = get_comments($query_args);
$total = (int) get_comments(array_merge($query_args, ['count' => true, 'number' => 0, 'offset' => 0]));
$items = [];
foreach ($comments as $comment) {
$cid = (int) $comment->comment_ID;
$items[] = [
'id' => $cid,
'user_id' => (int) $comment->user_id,
'user_name' => (string) $comment->comment_author,
'user_avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url((int) $comment->user_id) : get_avatar_url((int) $comment->user_id),
'content' => (string) $comment->comment_content,
'date' => (string) $comment->comment_date,
'rating_product' => (int) get_comment_meta($cid, '_aksrt_rating_product', true),
'rating_service' => (int) get_comment_meta($cid, '_aksrt_rating_service', true),
'rating_shipping' => (int) get_comment_meta($cid, '_aksrt_rating_shipping', true),
'rating_average' => (float) get_comment_meta($cid, '_aksrt_rating_average', true),
'images' => (array) (get_comment_meta($cid, '_aksrt_review_images', true) ?: []),
'order_number' => (string) get_comment_meta($cid, '_aksrt_order_number', true),
];
}
return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
}
public function get_product_scores(int $product_id): array {
$cached = get_post_meta($product_id, '_aksrt_review_scores', true);
if (is_array($cached)) {
return $cached;
}
return $this->refresh_product_scores($product_id);
}
public function refresh_product_scores(int $product_id): array {
global $wpdb;
$results = $wpdb->get_row($wpdb->prepare(
"SELECT COUNT(*) as total,
AVG(CAST(m.meta_value AS DECIMAL(3,1))) as average
FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
$product_id, self::COMMENT_TYPE
), ARRAY_A);
$total = (int) ($results['total'] ?? 0);
$average = $total > 0 ? round((float) ($results['average'] ?? 0), 1) : 0;
$good_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
AND CAST(m.meta_value AS DECIMAL(3,1)) >= 4",
$product_id, self::COMMENT_TYPE
));
$bad_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
AND CAST(m.meta_value AS DECIMAL(3,1)) < 3",
$product_id, self::COMMENT_TYPE
));
$has_image = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_review_images'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
$product_id, self::COMMENT_TYPE
));
$scores = [
'total' => $total,
'average' => $average,
'good' => $good_count,
'bad' => $bad_count,
'has_image' => $has_image,
'level' => self::RATING_LEVELS[min(5, max(0, (int) round($average)))] ?? '',
];
update_post_meta($product_id, '_aksrt_review_scores', $scores);
return $scores;
}
public function run_auto_positive_cron(): void {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15));
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin([
'status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
'limit' => 200,
]);
foreach ($orders as $order) {
$completed_at = (string) ($order['completed_at'] ?? $order['delivered_at'] ?? '');
if ($completed_at === '' || $completed_at > $cutoff) {
continue;
}
$user_id = (int) ($order['user_id'] ?? 0);
$order_number = (string) ($order['order_number'] ?? '');
if ($user_id <= 0 || $order_number === '') continue;
$items = class_exists('AKSRT\\Shop\\Shop')
? Shop::get_instance()->get_order_items($order_number)
: [];
$product_ids = array_column($items, 'product_id');
if (empty($product_ids) && !empty($order['product_id'])) {
$product_ids = [(int) $order['product_id']];
}
foreach ($product_ids as $pid) {
if (!$this->has_reviewed($user_id, $order_number, (int) $pid)) {
$this->submit_review($user_id, $order_number, (int) $pid, [
'rating_product' => 5,
'rating_service' => 5,
'rating_shipping' => 5,
'content' => __('系统默认好评', 'aksrt'),
]);
}
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day),
]);
}
}
/**
* Remaining seconds until auto-positive review, or 0 if unknown/closed.
*/
public function get_auto_review_remaining_seconds(string $order_number): int {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return 0;
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order) {
return 0;
}
$anchor = (string) ($order['delivered_at'] ?? $order['completed_at'] ?? $order['updated_at'] ?? $order['created_at'] ?? '');
if ($anchor === '') {
return 0;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15));
$deadline = strtotime($anchor . ' +' . $max_day . ' days');
if (!$deadline) {
return 0;
}
$remain = $deadline - time();
return $remain > 0 ? (int) $remain : 0;
}
/**
* Whether shipping score row should be shown for this order.
*/
public function order_needs_shipping_score(array $order): bool {
$method = is_array($order['shipping_method'] ?? null) ? $order['shipping_method'] : [];
$value = strtolower((string) ($method['value'] ?? $method['type'] ?? $order['delivery_type'] ?? ''));
if ($value === '') {
// Prefer showing logistics when shipping records or fee exist.
if (!empty($order['shipping_records']) || !empty($order['shipping']['count'])) {
return true;
}
$fee = (float) ($order['shipping_fee'] ?? ($order['shipping_method']['fee'] ?? 0));
return $fee > 0;
}
if (in_array($value, ['virtual', 'auto', 'none', 'offline', 'self', 'pickup', 'no_shipping'], true)) {
return false;
}
return true;
}
/**
* Build order review modal HTML (zibll order_comment_modal parity).
*
* @return array{success:bool,html?:string,message?:string}
*/
public function get_review_modal_html(int $user_id, string $order_number): array {
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
if ($order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在', 'aksrt')];
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) {
return ['success' => false, 'message' => __('订单数据异常', 'aksrt')];
}
$product_id = 0;
$items = class_exists('\AKSRT\Shop\Shop')
? Shop::get_instance()->get_order_items($order_number)
: [];
if (!empty($items[0]['product_id'])) {
$product_id = (int) $items[0]['product_id'];
} elseif (!empty($order['product_id'])) {
$product_id = (int) $order['product_id'];
}
if ($product_id <= 0) {
return ['success' => false, 'message' => __('订单商品无效', 'aksrt')];
}
if ($this->has_reviewed($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('订单已评价', 'aksrt')];
}
if (!$this->can_review($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')];
}
$shop = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_shop_settings()
: [];
$placeholder = (string) ($shop['timing']['comment_placeholder'] ?? '');
if ($placeholder === '') {
$placeholder = __('展开说说对商品的看法吧~', 'aksrt');
}
$img_num = max(0, min(9, (int) ($shop['timing']['comment_img_num'] ?? 3)));
$need_shipping = $this->order_needs_shipping_score($order);
$remain = $this->get_auto_review_remaining_seconds($order_number);
$score_html = '';
$score_html .= $this->render_score_row(__('商品评分', 'aksrt'), 'product');
if ($need_shipping) {
$score_html .= $this->render_score_row(__('物流服务', 'aksrt'), 'shipping');
}
$score_html .= $this->render_score_row(__('服务态度', 'aksrt'), 'service');
$img_html = '';
if ($img_num > 0) {
$img_html = '<div class="form-upload mb10">';
$img_html .= '<div class="em09 muted-2-color mb6">' . esc_html__('图片', 'aksrt') . '</div>';
$img_html .= '<div class="preview" data-aksrt-review-preview data-max="' . esc_attr((string) $img_num) . '">';
$img_html .= '<div class="add" data-aksrt-review-add role="button" tabindex="0" aria-label="' . esc_attr__('添加图片', 'aksrt') . '"></div>';
$img_html .= '</div>';
$img_html .= '<input class="hide" type="file" data-aksrt-review-file multiple accept="image/gif,image/jpeg,image/jpg,image/png,image/webp" />';
$img_html .= '</div>';
}
$title = '<b>' . esc_html__('评价订单', 'aksrt') . '</b>';
if ($remain > 0) {
$title .= '<span class="px12 muted-2-color ml6">'
. '<span data-aksrt-review-countdown="' . esc_attr((string) $remain) . '">' . esc_html($this->format_countdown($remain)) . '</span>'
. esc_html__('后自动好评', 'aksrt')
. '</span>';
}
$html = '<div class="touch border-title">' . $title . '</div>';
$html .= '<button type="button" class="close abs-close aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">&times;</button>';
$html .= '<form class="order-comment-from form-upload" data-aksrt-order-review-form data-order-number="' . esc_attr($order_number) . '" data-product-id="' . esc_attr((string) $product_id) . '" data-need-shipping="' . ($need_shipping ? '1' : '0') . '">';
$html .= '<div class="mb20">' . $score_html . '</div>';
$html .= '<div class="mb20"><textarea class="form-control" name="comment" rows="4" placeholder="' . esc_attr($placeholder) . '"></textarea></div>';
$html .= $img_html;
$html .= '<div class="review-error" data-aksrt-review-error hidden></div>';
$html .= '<div class="but-average modal-buts">';
$html .= '<button type="button" class="but" data-aksrt-remote-modal-close>' . esc_html__('取消', 'aksrt') . '</button>';
$html .= '<button type="submit" class="but c-blue btn-primary" data-aksrt-review-submit><i class="fa fa-check" aria-hidden="true"></i> ' . esc_html__('确认提交', 'aksrt') . '</button>';
$html .= '</div></form>';
return [
'success' => true,
'html' => $html,
'product_id' => $product_id,
'need_shipping' => $need_shipping,
'img_max' => $img_num,
'remain_seconds' => $remain,
];
}
private function render_score_row(string $label, string $key): string {
return '<div class="mb10 shop-score-box flex ac" data-aksrt-score-box data-score-key="' . esc_attr($key) . '" score-value="0">'
. '<div class="em09 muted-2-color score-title">' . esc_html($label) . '</div>'
. '<div class="score-icon em16 mr10 ml20" style="height:30px;"></div>'
. '<div class="score-text em09 muted-2-color"></div>'
. '<input class="score-input" type="hidden" name="score[' . esc_attr($key) . ']" value="0">'
. '</div>';
}
private function format_countdown(int $seconds): string {
$seconds = max(0, $seconds);
$d = intdiv($seconds, 86400);
$h = intdiv($seconds % 86400, 3600);
$m = intdiv($seconds % 3600, 60);
$s = $seconds % 60;
if ($d > 0) {
return sprintf('%d天%02d:%02d:%02d', $d, $h, $m, $s);
}
return sprintf('%02d:%02d:%02d', $h, $m, $s);
}
/**
* Accept https URL or data:image/*;base64,... and return a public URL.
*/
private function normalize_review_image(string $raw, int $user_id): string {
$raw = trim($raw);
if ($raw === '') {
return '';
}
if (preg_match('#^https?://#i', $raw)) {
return esc_url_raw($raw);
}
if (!preg_match('#^data:image/(png|jpe?g|gif|webp);base64,#i', $raw, $m)) {
return '';
}
$binary = base64_decode(substr($raw, strpos($raw, ',') + 1), true);
if ($binary === false || $binary === '') {
return '';
}
if (strlen($binary) > 5 * 1024 * 1024) {
return '';
}
$ext = strtolower($m[1]);
if ($ext === 'jpeg') {
$ext = 'jpg';
}
$filename = 'review-' . $user_id . '-' . wp_generate_password(8, false) . '.' . $ext;
$upload = wp_upload_bits($filename, null, $binary);
if (!empty($upload['error']) || empty($upload['url'])) {
return '';
}
return esc_url_raw((string) $upload['url']);
}
private function get_max_images(): int {
if (!class_exists('\AKSRT\Settings\Settings')) {
return 9;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
return max(0, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9)));
}
}
-356
View File
@@ -1,356 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
/* ============================================================
* Product meta helpers
* ============================================================ */
public function get_term_options(string $taxonomy): array {
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
return array_map(static function ($term) {
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
];
}, $terms);
}
public function get_product_gallery(int $product_id): array {
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
if (!is_array($gallery)) {
return [];
}
return array_values(array_filter(array_map('esc_url_raw', $gallery)));
}
public function get_product_shipping_profile(int $product_id): array {
return [
'mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
'fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
'delivery_note' => (string) get_post_meta($product_id, '_aksrt_delivery_note', true),
];
}
public function get_product_after_sale_profile(int $product_id): array {
return [
'policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
];
}
public function get_product_marketing_profile(int $product_id): array {
return [
'headline' => (string) get_post_meta($product_id, '_aksrt_marketing_headline', true),
'shipping_tip' => (string) get_post_meta($product_id, '_aksrt_shipping_tip', true),
'service_tip' => (string) get_post_meta($product_id, '_aksrt_service_tip', true),
];
}
public function get_payment_method_options(): array {
$methods = [];
if (class_exists('\AKSRT\Payment\Gateway')) {
foreach (\AKSRT\Payment\Gateway::get_instance()->get_all() as $id => $gateway) {
if (!method_exists($gateway, 'is_configured') || !$gateway->is_configured()) {
continue;
}
$methods[$id] = [
'value' => $id,
'label' => method_exists($gateway, 'get_name') ? (string) $gateway->get_name() : $id,
'description' => $this->gateway_method_description((string) $id),
];
}
}
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled()) {
$methods['points'] = [
'value' => 'points',
'label' => __('积分兑换', 'aksrt'),
'description' => __('使用积分购买积分商品。', 'aksrt'),
];
}
// Fallback only when no configured gateways (and no points).
if (empty($methods)) {
$methods['manual_review'] = [
'value' => 'manual_review',
'label' => __('人工确认', 'aksrt'),
'description' => __('暂无可用支付网关,保留待支付订单由管理员确认。', 'aksrt'),
];
}
return $methods;
}
private function gateway_method_description(string $id): string {
switch ($id) {
case 'balance':
return __('即时完成支付并写回订单状态。', 'aksrt');
case 'alipay':
return __('跳转支付宝完成支付。', 'aksrt');
case 'wechatpay':
return __('微信扫码或跳转完成支付。', 'aksrt');
case 'paypal':
return __('跳转 PayPal 完成支付。', 'aksrt');
case 'stripe':
return __('跳转 Stripe Checkout 完成支付。', 'aksrt');
case 'epay_alipay':
return __('经易支付跳转支付宝完成支付。', 'aksrt');
case 'epay_wxpay':
return __('经易支付跳转微信完成支付。', 'aksrt');
default:
return '';
}
}
public function get_shipping_method_options(): array {
return [
'express' => [
'value' => 'express',
'label' => __('快递配送', 'aksrt'),
'description' => __('标准快递,支持物流跟踪。', 'aksrt'),
'fee' => 12.0,
],
'ems' => [
'value' => 'ems',
'label' => __('EMS', 'aksrt'),
'description' => __('偏远地区可选。', 'aksrt'),
'fee' => 18.0,
],
'download' => [
'value' => 'download',
'label' => __('自动发货', 'aksrt'),
'description' => __('适用于数字商品。', 'aksrt'),
'fee' => 0.0,
],
];
}
public function get_limit_buy_config(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_limit_buy_config', true);
if (!is_array($raw)) {
$legacy = (int) get_post_meta($product_id, '_aksrt_limit_buy', true);
return ['default' => $legacy > 0 ? $legacy : -1, 'desc' => ''];
}
return [
'default' => (int) ($raw['default'] ?? -1),
'vip' => (int) ($raw['vip'] ?? -1),
'vip2' => (int) ($raw['vip2'] ?? -1),
'auth' => (int) ($raw['auth'] ?? -1),
'desc' => sanitize_text_field((string) ($raw['desc'] ?? '')),
];
}
public function get_product_services(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_services', true);
if (!is_array($raw)) return [];
return array_values(array_filter(array_map(static function ($s) {
$name = sanitize_text_field((string) ($s['name'] ?? ''));
if ($name === '') return null;
return [
'name' => $name,
'desc' => sanitize_text_field((string) ($s['desc'] ?? '')),
'icon' => esc_url_raw((string) ($s['icon'] ?? '')),
];
}, $raw)));
}
public function get_product_params(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_product_params', true);
if (!is_array($raw)) return [];
return array_values(array_filter(array_map(static function ($p) {
$name = sanitize_text_field((string) ($p['name'] ?? ''));
if ($name === '') return null;
return [
'name' => $name,
'value' => sanitize_text_field((string) ($p['value'] ?? '')),
];
}, $raw)));
}
/**
* Resolve product main image with 3-tier fallback:
* 1. _aksrt_product_image (custom main image)
* 2. WordPress featured image (post thumbnail)
* 3. First gallery image from _aksrt_product_gallery
*/
public function resolve_main_image(int $product_id): string {
$custom = (string) get_post_meta($product_id, '_aksrt_product_image', true);
if ($custom !== '') {
return esc_url_raw($custom);
}
$thumb = get_the_post_thumbnail_url($product_id, 'large');
if ($thumb) {
return (string) $thumb;
}
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
if (is_array($gallery) && !empty($gallery)) {
$first = reset($gallery);
if ($first) {
return esc_url_raw((string) $first);
}
}
return '';
}
public function build_product_card_view(int $product_id): array {
$post = get_post($product_id);
if (!$post || $post->post_type !== 'aksrt_product') {
return [];
}
$stock_raw = (int) get_post_meta($product_id, '_aksrt_stock', true);
$is_unlimited = $stock_raw <= -1;
return [
'id' => $product_id,
'title' => (string) get_the_title($product_id),
'excerpt' => (string) get_the_excerpt($product_id),
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
'image' => $this->resolve_main_image($product_id),
'stock' => $stock_raw,
'is_unlimited' => $is_unlimited,
'sales' => (int) get_post_meta($product_id, '_aksrt_sales', true),
'status' => (string) get_post_status($product_id),
'created' => get_the_date('Y-m-d H:i', $product_id),
'permalink' => get_permalink($product_id) ?: '',
];
}
/* ============================================================
* Product Variants / SKU
* ============================================================ */
/**
* Get variants table name.
*/
public function variants_table(): string {
global $wpdb;
return $wpdb->prefix . 'aksrt_product_variant';
}
/**
* Get all variants for a product.
*/
public function get_variants(int $product_id): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->variants_table()} WHERE product_id = %d ORDER BY sort_order ASC, id ASC",
$product_id
), ARRAY_A);
if (!$rows) return [];
return array_map(function ($row) {
$row['id'] = (int) $row['id'];
$row['product_id'] = (int) $row['product_id'];
$row['price'] = (float) $row['price'];
$row['stock'] = (int) $row['stock'];
$row['sort_order'] = (int) $row['sort_order'];
$row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: [];
return $row;
}, $rows);
}
/**
* Get a single variant by ID.
*/
public function get_variant(int $variant_id): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->variants_table()} WHERE id = %d",
$variant_id
), ARRAY_A);
if (!$row) return null;
$row['id'] = (int) $row['id'];
$row['product_id'] = (int) $row['product_id'];
$row['price'] = (float) $row['price'];
$row['stock'] = (int) $row['stock'];
$row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: [];
return $row;
}
/**
* Save variants for a product (replace all).
*/
public function save_variants(int $product_id, array $variants): array {
global $wpdb;
$table = $this->variants_table();
// Delete existing variants
$wpdb->delete($table, ['product_id' => $product_id]);
$saved = [];
foreach ($variants as $index => $v) {
$data = [
'product_id' => $product_id,
'sku' => sanitize_text_field((string) ($v['sku'] ?? '')),
'attributes' => wp_json_encode($v['attributes'] ?? []),
'price' => max(0, (float) ($v['price'] ?? 0)),
'stock' => max(0, (int) ($v['stock'] ?? 0)),
'image' => esc_url_raw((string) ($v['image'] ?? '')),
'sort_order' => $index,
'created_at' => current_time('mysql', true),
];
$wpdb->insert($table, $data);
$saved[] = array_merge($data, ['id' => (int) $wpdb->insert_id]);
}
// Update product meta flag
update_post_meta($product_id, '_aksrt_has_variants', count($saved) > 0 ? '1' : '0');
return $saved;
}
/**
* Delete a single variant.
*/
public function delete_variant(int $variant_id): bool {
global $wpdb;
$variant = $this->get_variant($variant_id);
if (!$variant) return false;
$result = $wpdb->delete($this->variants_table(), ['id' => $variant_id]);
if ($result) {
// Update product meta flag
$remaining = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->variants_table()} WHERE product_id = %d",
$variant['product_id']
));
update_post_meta($variant['product_id'], '_aksrt_has_variants', $remaining > 0 ? '1' : '0');
}
return (bool) $result;
}
/**
* Check if a product has variants.
*/
public function has_variants(int $product_id): bool {
return get_post_meta($product_id, '_aksrt_has_variants', true) === '1';
}
}
-188
View File
@@ -1,188 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductVariantsMetaBox {
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_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 30, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_variants',
__('SKU 变体', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'default'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_product_variants_save', 'aksrt_product_variants_nonce');
$variants = [];
if (class_exists('AKSRT\\Shop\\Shop')) {
$variants = Shop::get_instance()->get_variants((int) $post->ID);
}
if (!$variants) {
$variants = [[
'sku' => '',
'attributes' => [],
'price' => 0,
'stock' => 0,
'image' => '',
]];
}
?>
<style>
.aksrt-variants-table { width:100%; border-collapse:collapse; }
.aksrt-variants-table th, .aksrt-variants-table td { border:1px solid #dcdcde; padding:6px; vertical-align:top; }
.aksrt-variants-table input { width:100%; }
.aksrt-variants-table .col-sku { width:14%; }
.aksrt-variants-table .col-attrs { width:28%; }
.aksrt-variants-table .col-price, .aksrt-variants-table .col-stock { width:12%; }
.aksrt-variants-table .col-image { width:28%; }
.aksrt-variants-hint { color:#646970; font-size:12px; margin:8px 0; }
</style>
<p class="aksrt-variants-hint">
<?php esc_html_e('属性格式:名称:值,多项用分号分隔,例如 颜色:红;尺寸:L。保存时会整体覆盖该商品变体列表。', 'aksrt'); ?>
</p>
<table class="aksrt-variants-table" id="aksrt-variants-table">
<thead>
<tr>
<th class="col-sku"><?php esc_html_e('SKU', 'aksrt'); ?></th>
<th class="col-attrs"><?php esc_html_e('属性', 'aksrt'); ?></th>
<th class="col-price"><?php esc_html_e('价格', 'aksrt'); ?></th>
<th class="col-stock"><?php esc_html_e('库存', 'aksrt'); ?></th>
<th class="col-image"><?php esc_html_e('图片 URL', 'aksrt'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $i => $row) :
$attrs = $row['attributes'] ?? [];
$attrs_text = '';
if (is_array($attrs)) {
$parts = [];
foreach ($attrs as $k => $v) {
if (is_int($k) && is_array($v)) {
$parts[] = ($v['name'] ?? $v['key'] ?? '') . ':' . ($v['value'] ?? '');
} else {
$parts[] = $k . ':' . $v;
}
}
$attrs_text = implode(';', array_filter($parts));
} elseif (is_string($attrs)) {
$attrs_text = $attrs;
}
?>
<tr>
<td><input type="text" name="aksrt_variant_sku[]" value="<?php echo esc_attr((string) ($row['sku'] ?? '')); ?>"></td>
<td><input type="text" name="aksrt_variant_attrs[]" value="<?php echo esc_attr($attrs_text); ?>" placeholder="颜色:红;尺寸:L"></td>
<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="<?php echo esc_attr((string) ($row['price'] ?? 0)); ?>"></td>
<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="<?php echo esc_attr((string) ($row['stock'] ?? 0)); ?>"></td>
<td><input type="url" name="aksrt_variant_image[]" value="<?php echo esc_attr((string) ($row['image'] ?? '')); ?>"></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" class="button" id="aksrt-add-variant-row"><?php esc_html_e('添加变体行', 'aksrt'); ?></button>
</p>
<script>
(function(){
var btn = document.getElementById('aksrt-add-variant-row');
var tbody = document.querySelector('#aksrt-variants-table tbody');
if (!btn || !tbody) return;
btn.addEventListener('click', function(){
var tr = document.createElement('tr');
tr.innerHTML = '<td><input type="text" name="aksrt_variant_sku[]" value=""></td>' +
'<td><input type="text" name="aksrt_variant_attrs[]" value="" placeholder="颜色:红;尺寸:L"></td>' +
'<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="0"></td>' +
'<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="0"></td>' +
'<td><input type="url" name="aksrt_variant_image[]" value=""></td>';
tbody.appendChild(tr);
});
})();
</script>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_variants_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_variants_nonce'])), 'aksrt_product_variants_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product' || !current_user_can('edit_post', $post_id)) {
return;
}
if (!class_exists('AKSRT\\Shop\\Shop')) {
return;
}
$skus = isset($_POST['aksrt_variant_sku']) && is_array($_POST['aksrt_variant_sku']) ? wp_unslash($_POST['aksrt_variant_sku']) : [];
$attrs = isset($_POST['aksrt_variant_attrs']) && is_array($_POST['aksrt_variant_attrs']) ? wp_unslash($_POST['aksrt_variant_attrs']) : [];
$prices = isset($_POST['aksrt_variant_price']) && is_array($_POST['aksrt_variant_price']) ? wp_unslash($_POST['aksrt_variant_price']) : [];
$stocks = isset($_POST['aksrt_variant_stock']) && is_array($_POST['aksrt_variant_stock']) ? wp_unslash($_POST['aksrt_variant_stock']) : [];
$images = isset($_POST['aksrt_variant_image']) && is_array($_POST['aksrt_variant_image']) ? wp_unslash($_POST['aksrt_variant_image']) : [];
$variants = [];
$count = max(count($skus), count($attrs), count($prices), count($stocks), count($images));
for ($i = 0; $i < $count; $i++) {
$sku = sanitize_text_field((string) ($skus[$i] ?? ''));
$attr_raw = sanitize_text_field((string) ($attrs[$i] ?? ''));
$price = max(0, (float) ($prices[$i] ?? 0));
$stock = max(0, (int) ($stocks[$i] ?? 0));
$image = esc_url_raw((string) ($images[$i] ?? ''));
if ($sku === '' && $attr_raw === '' && $price <= 0 && $stock <= 0 && $image === '') {
continue;
}
$attributes = [];
if ($attr_raw !== '') {
foreach (preg_split('/\s*;\s*/', $attr_raw) ?: [] as $pair) {
if ($pair === '' || !str_contains($pair, ':')) {
continue;
}
[$k, $v] = array_map('trim', explode(':', $pair, 2));
if ($k !== '') {
$attributes[$k] = $v;
}
}
}
$variants[] = [
'sku' => $sku,
'attributes' => $attributes,
'price' => $price,
'stock' => $stock,
'image' => $image,
];
}
Shop::get_instance()->save_variants($post_id, $variants);
}
}
-397
View File
@@ -1,397 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class Shop {
use Part1;
use Part2;
use Part3;
private static $instance = null;
private ?CartService $cart_svc = null;
private ?CheckoutService $checkout_svc = null;
private ?AddressService $address_svc = null;
private ?ProductService $product_svc = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function cart_service(): CartService {
if (!$this->cart_svc) {
$this->cart_svc = new CartService($this);
}
return $this->cart_svc;
}
private function checkout_service(): CheckoutService {
if (!$this->checkout_svc) {
$this->checkout_svc = new CheckoutService($this);
}
return $this->checkout_svc;
}
private function address_service(): AddressService {
if (!$this->address_svc) {
$this->address_svc = new AddressService($this);
}
return $this->address_svc;
}
private function product_service(): ProductService {
if ($this->product_svc === null) {
$this->product_svc = new ProductService($this);
}
return $this->product_svc;
}
public function get_config(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_shop_settings();
}
return ['enabled' => false];
}
public function get_products(array $args = []): array {
$limit = max(1, (int) ($args['limit'] ?? 20));
$offset = max(0, (int) ($args['offset'] ?? 0));
$search = sanitize_text_field((string) ($args['search'] ?? ''));
$status = sanitize_key((string) ($args['status'] ?? ''));
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $limit,
'offset' => $offset,
'post_status' => $status !== '' ? $status : ['publish', 'draft', 'pending'],
'orderby' => 'date',
'order' => 'DESC',
];
if ($search !== '') {
$query_args['s'] = $search;
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
return $products;
}
public function get_product_detail_view(int $product_id): array {
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return [];
}
$detail = $this->build_product_card_view($product_id);
$detail['content'] = (string) $product->post_content;
$detail['excerpt'] = (string) $product->post_excerpt;
$detail['permalink'] = get_permalink($product_id) ?: '';
$detail['status'] = (string) get_post_status($product_id);
$detail['gallery'] = $this->product_service()->get_product_gallery($product_id);
$detail['categories'] = wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'all']) ?: [];
$detail['tags'] = wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'all']) ?: [];
$detail['shipping'] = $this->product_service()->get_product_shipping_profile($product_id);
$detail['after_sale'] = $this->product_service()->get_product_after_sale_profile($product_id);
$detail['marketing'] = $this->product_service()->get_product_marketing_profile($product_id);
$detail['checkout_hint'] = $this->checkout_service()->get_checkout_hint($detail);
$detail['purchase_enabled'] = ($detail['is_unlimited'] || $detail['stock'] > 0) && $detail['status'] === 'publish';
$detail['default_quantity'] = 1;
$detail['options'] = $this->get_product_options($product_id);
$detail['has_options'] = $this->has_options($product_id);
$detail['price_range'] = $this->get_price_range($product_id);
$detail['pay_modo'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0';
$detail['start_price'] = (float) get_post_meta($product_id, '_aksrt_start_price', true);
$detail['shipping_type'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_type', true)) ?: 'express';
$detail['auto_delivery'] = $this->get_auto_delivery_config($product_id);
$detail['limit_buy'] = $this->get_limit_buy_config($product_id);
$detail['services'] = $this->get_product_services($product_id);
$detail['params'] = $this->get_product_params($product_id);
return $detail;
}
public function get_product_edit_view(int $user_id, int $product_id = 0): array {
$view = [
'allowed' => true,
'is_edit' => false,
'product' => [
'id' => 0,
'title' => '',
'price' => 0,
'stock' => 0,
'excerpt' => '',
'content' => '',
'image' => '',
'gallery' => [],
'categories' => [],
'tags' => [],
'shipping_mode' => 'express',
'shipping_fee' => 0,
'after_sale_policy' => '',
'support_refund' => true,
'status' => 'draft',
'options' => [],
'pay_modo' => '0',
'start_price' => 0,
'product_video' => [],
'product_params' => '',
],
'taxonomy' => [
'categories' => $this->product_service()->get_term_options('aksrt_product_cat'),
'tags' => $this->product_service()->get_term_options('aksrt_product_tag'),
],
'shipping_modes' => $this->get_shipping_method_options(),
'status_options' => [
['value' => 'draft', 'label' => __('草稿', 'aksrt')],
['value' => 'publish', 'label' => __('发布', 'aksrt')],
['value' => 'pending', 'label' => __('待审核', 'aksrt')],
],
];
if ($product_id <= 0) {
return $view;
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $view;
}
$is_owner = (int) $product->post_author === $user_id || current_user_can('manage_options');
if (!$is_owner) {
$view['allowed'] = false;
return $view;
}
$view['is_edit'] = true;
$videos = get_post_meta($product_id, '_aksrt_product_video', true);
if (!is_array($videos)) {
$videos = [];
}
$view['product'] = [
'id' => $product_id,
'title' => (string) $product->post_title,
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
'stock' => (int) get_post_meta($product_id, '_aksrt_stock', true),
'excerpt' => (string) $product->post_excerpt,
'content' => (string) $product->post_content,
'image' => $this->resolve_main_image($product_id),
'gallery' => $this->product_service()->get_product_gallery($product_id),
'categories' => wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'ids']) ?: [],
'tags' => wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
'shipping_mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
'shipping_fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
'after_sale_policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
'status' => (string) get_post_status($product_id),
'options' => method_exists($this, 'get_product_options') ? $this->get_product_options($product_id) : [],
'pay_modo' => sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0',
'start_price' => (float) get_post_meta($product_id, '_aksrt_start_price', true),
'product_video' => array_values(array_filter(array_map('esc_url_raw', $videos))),
'product_params' => (string) get_post_meta($product_id, '_aksrt_product_params', true),
];
return $view;
}
public function get_cart_page_view(int $user_id): array {
$pending_orders = $this->get_order_center_view($user_id, [
'page' => 1,
'per_page' => 5,
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
]);
return [
'payment_methods' => $this->get_payment_method_options(),
'shipping_methods' => $this->get_shipping_method_options(),
'address_book' => $this->get_address_book($user_id),
'coupon_enabled' => class_exists('\AKSRT\Payment\Coupon'),
'currency_symbol' => '¥',
'pending_orders' => $pending_orders['items'] ?? [],
'cart_items' => $this->get_cart($user_id),
'cart_count' => $this->get_cart_count($user_id),
];
}
public function get_cart(int $user_id): array {
return $this->cart_service()->get_cart($user_id);
}
public function get_cart_count(int $user_id): int {
return $this->cart_service()->get_cart_count($user_id);
}
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
return $this->cart_service()->add_to_cart($user_id, $product_id, $options_string, $count);
}
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
return $this->cart_service()->reduce_from_cart($user_id, $product_id, $options_string, $count);
}
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
return $this->cart_service()->remove_from_cart($user_id, $product_id, $options_string);
}
public function remove_multi_from_cart(int $user_id, array $items): array {
return $this->cart_service()->remove_multi_from_cart($user_id, $items);
}
public function empty_cart(int $user_id): array {
return $this->cart_service()->empty_cart($user_id);
}
public function validate_cart_item(int $product_id, string $options_string = '') {
return $this->cart_service()->validate_cart_item($product_id, $options_string);
}
public function get_order_center_view(int $user_id, array $args = []): array {
if (class_exists('\AKSRT\Payment\UserOrders')) {
return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args);
}
return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []];
}
public function get_order_detail_view(int $user_id, string $order_number): ?array {
if (class_exists('\AKSRT\Payment\UserOrders')) {
return \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view($user_id, $order_number);
}
return null;
}
public function preview_checkout(int $user_id, array $payload): array {
return $this->checkout_service()->preview_checkout($user_id, $payload);
}
public function create_checkout_order(int $user_id, array $payload): array {
return $this->checkout_service()->create_checkout_order($user_id, $payload);
}
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
return $this->checkout_service()->pay_order($user_id, $order_number, $payment_method);
}
public function get_user_favorites(int $user_id, int $page = 1, int $per_page = 20): array {
if ($user_id <= 0) {
return ['items' => [], 'total' => 0];
}
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
if (!is_array($fav_ids) || empty($fav_ids)) {
return ['items' => [], 'total' => 0];
}
$total = count($fav_ids);
$paged = array_slice(array_reverse($fav_ids), ($page - 1) * $per_page, $per_page);
$items = [];
foreach ($paged as $pid) {
$card = $this->build_product_card_view((int) $pid);
if (!empty($card)) {
$items[] = $card;
}
}
return ['items' => $items, 'total' => $total];
}
public function toggle_favorite(int $user_id, int $product_id): array {
if ($user_id <= 0 || $product_id <= 0) {
return ['success' => false];
}
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
if (!is_array($fav_ids)) {
$fav_ids = [];
}
$key = array_search($product_id, $fav_ids, true);
if ($key !== false) {
unset($fav_ids[$key]);
$fav_ids = array_values($fav_ids);
$is_fav = false;
} else {
$fav_ids[] = $product_id;
$is_fav = true;
}
update_user_meta($user_id, 'aksrt_product_favorites', $fav_ids);
$count = (int) get_post_meta($product_id, '_aksrt_favorite_count', true);
update_post_meta($product_id, '_aksrt_favorite_count', max(0, $count + ($is_fav ? 1 : -1)));
if ($is_fav) {
do_action('aksrt_product_favorited', $user_id, $product_id);
} else {
do_action('aksrt_product_unfavorited', $user_id, $product_id);
}
return ['success' => true, 'is_favorite' => $is_fav, 'count' => count($fav_ids)];
}
public function is_favorite(int $user_id, int $product_id): bool {
if ($user_id <= 0) return false;
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
return is_array($fav_ids) && in_array($product_id, $fav_ids, true);
}
public function re_purchase(int $user_id, string $order_number): array {
$items = $this->get_order_items($order_number);
if (empty($items)) {
return ['success' => false, 'message' => __('订单无商品记录', 'aksrt')];
}
$added = 0;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$qty = max(1, (int) $item['quantity']);
$opt_key = (string) $item['options_key'];
$result = $this->add_to_cart($user_id, $pid, $opt_key, $qty);
if (!empty($result['success'])) {
$added++;
}
}
return ['success' => $added > 0, 'added' => $added, 'cart_count' => $this->get_cart_count($user_id)];
}
public function cancel_order(int $user_id, string $order_number): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) {
return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')];
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
'force' => true,
'note' => __('用户在前台取消订单。', 'aksrt'),
]);
return [
'success' => true,
'data' => [
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
}
-748
View File
@@ -1,748 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class TermMeta {
private static $instance = null;
/** @var string Meta key for product_cat/product_tag unified storage */
const META_KEY = 'aksrt_shop_term_meta';
/** @var string Meta key for shop_discount unified storage */
const DISCOUNT_META_KEY = 'aksrt_shop_discount_meta';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Register hooks.
*/
public function init(): void {
add_action('init', [$this, 'register_discount_taxonomy'], 5);
add_action('init', [$this, 'register_term_meta_fields'], 10);
// Native term UI for shop_discount (zibll-style admin ops).
add_action('shop_discount_add_form_fields', [$this, 'render_discount_add_fields']);
add_action('shop_discount_edit_form_fields', [$this, 'render_discount_edit_fields'], 10, 1);
add_action('created_shop_discount', [$this, 'save_discount_term_fields']);
add_action('edited_shop_discount', [$this, 'save_discount_term_fields']);
// Product category inheritance fields.
add_action('aksrt_product_cat_add_form_fields', [$this, 'render_cat_add_fields']);
add_action('aksrt_product_cat_edit_form_fields', [$this, 'render_cat_edit_fields'], 10, 1);
add_action('created_aksrt_product_cat', [$this, 'save_cat_term_fields']);
add_action('edited_aksrt_product_cat', [$this, 'save_cat_term_fields']);
// Product tag display fields.
add_action('aksrt_product_tag_add_form_fields', [$this, 'render_tag_add_fields']);
add_action('aksrt_product_tag_edit_form_fields', [$this, 'render_tag_edit_fields'], 10, 1);
add_action('created_aksrt_product_tag', [$this, 'save_tag_term_fields']);
add_action('edited_aksrt_product_tag', [$this, 'save_tag_term_fields']);
}
// ─── Taxonomy Registration ───────────────────────────────────
/**
* Register the shop_discount taxonomy for discount campaigns.
*/
public function register_discount_taxonomy(): void {
if (taxonomy_exists('shop_discount')) {
return;
}
$shop_enabled = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$shop_settings = \AKSRT\Settings\Settings::get_shop_settings();
$shop_enabled = !empty($shop_settings['enabled']);
}
$shop_show_ui = true;
$shop_show_menu = is_admin() ? $shop_enabled : false;
register_taxonomy('shop_discount', 'aksrt_product', [
'labels' => [
'name' => __('优惠活动', 'aksrt'),
'singular_name' => __('优惠活动', 'aksrt'),
'search_items' => __('搜索优惠活动', 'aksrt'),
'all_items' => __('所有优惠活动', 'aksrt'),
'edit_item' => __('编辑优惠活动', 'aksrt'),
'add_new_item' => __('新增优惠活动', 'aksrt'),
'menu_name' => __('优惠活动', 'aksrt'),
],
'public' => true,
'hierarchical' => false,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'shop-discount', 'with_front' => false],
]);
}
// ─── Term Meta Registration ──────────────────────────────────
/**
* Register all term meta fields for REST API exposure.
*/
public function register_term_meta_fields(): void {
$auth = function () {
return current_user_can('manage_categories');
};
// ── aksrt_product_cat ──
$cat_fields = $this->get_cat_field_definitions();
foreach ($cat_fields as $key => $def) {
register_term_meta('aksrt_product_cat', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
// ── aksrt_product_tag ──
$tag_fields = $this->get_tag_field_definitions();
foreach ($tag_fields as $key => $def) {
register_term_meta('aksrt_product_tag', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
// ── shop_discount ──
$discount_fields = $this->get_discount_field_definitions();
foreach ($discount_fields as $key => $def) {
register_term_meta('shop_discount', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
}
// ─── Field Definitions ───────────────────────────────────────
/**
* Product category term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_cat_field_definitions(): array {
return [
'shop_cat_limit_buy' => [
'type' => 'integer', 'description' => '分类级限购 (-1 不限)', 'default' => -1,
'sanitize' => function ($v) { return max(-1, min(9999, (int) $v)); },
],
'shop_cat_shipping_fee' => [
'type' => 'string', 'description' => '分类级运费类型', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'free', 'fixed', 'amount'], true) ? $v : '';
},
],
'shop_cat_guest_buy' => [
'type' => 'string', 'description' => '分类级免登录购买', 'default' => '',
],
'shop_cat_email_fill' => [
'type' => 'string', 'description' => '分类级邮箱必留', 'default' => '',
],
'shop_cat_after_sale' => [
'type' => 'string', 'description' => '分类级售后政策', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'refund', 'refund_return', 'none'], true) ? $v : '';
},
],
'shop_cat_rebate' => [
'type' => 'string', 'description' => '分类级返佣覆盖', 'default' => '',
],
'shop_cat_service' => [
'type' => 'string', 'description' => '分类级服务保障', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_cat_single_tab' => [
'type' => 'string', 'description' => '分类级自定义 TAB', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_cat_content_after' => [
'type' => 'string', 'description' => '分类级详情页底部内容', 'default' => '',
'sanitize' => 'wp_kses_post',
],
'shop_cat_content_layout' => [
'type' => 'string', 'description' => '分类级内容布局', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'adapt', 'full', 'sidebar'], true) ? $v : '';
},
],
'shop_cat_content_show_bg' => [
'type' => 'string', 'description' => '分类级详情背景盒子', 'default' => '',
],
'shop_cat_list_style' => [
'type' => 'string', 'description' => '分类页列表样式', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'default', 'card-lg', 'card-sm'], true) ? $v : '';
},
],
];
}
/**
* Product tag term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_tag_field_definitions(): array {
return [
'shop_tag_priority' => [
'type' => 'integer', 'description' => '标签优先级(影响显示顺序)', 'default' => 0,
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
],
'shop_tag_class' => [
'type' => 'string', 'description' => '标签颜色 class', 'default' => '',
'sanitize' => 'sanitize_html_class',
],
'shop_tag_is_important' => [
'type' => 'string', 'description' => '重点标签(商品价格位着重显示)', 'default' => '',
],
];
}
/**
* Shop discount term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_discount_field_definitions(): array {
return [
'shop_discount_small_badge' => [
'type' => 'string', 'description' => '小标签文案', 'default' => '',
],
'shop_discount_priority' => [
'type' => 'integer', 'description' => '优先级(数字越大越优先)', 'default' => 0,
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
],
'shop_discount_is_important' => [
'type' => 'string', 'description' => '重点活动', 'default' => '',
],
'shop_discount_important_class' => [
'type' => 'string', 'description' => '重点活动背景色 class', 'default' => '',
'sanitize' => 'sanitize_html_class',
],
'shop_discount_scope' => [
'type' => 'string', 'description' => '优惠范围', 'default' => 'item',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['item', 'product', 'author', 'order'], true) ? $v : 'item';
},
],
'shop_discount_type' => [
'type' => 'string', 'description' => '优惠类型', 'default' => 'reduction',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['reduction', 'discount', 'gift'], true) ? $v : 'reduction';
},
],
'shop_discount_value' => [
'type' => 'string', 'description' => '优惠数值(立减金额/折扣 0.01-9.99)', 'default' => '0',
'sanitize' => function ($v) { return sanitize_text_field((string) $v); },
],
'shop_discount_gift_config' => [
'type' => 'string', 'description' => '赠品配置 JSON', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_discount_price_limit' => [
'type' => 'string', 'description' => '金额门槛', 'default' => '0',
'sanitize' => function ($v) { return max(0, (float) $v); },
],
'shop_discount_user_limit' => [
'type' => 'string', 'description' => '用户身份限制', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'vip', 'vip_2', 'auth'], true) ? $v : '';
},
],
'shop_discount_start' => [
'type' => 'string', 'description' => '活动开始时间', 'default' => '',
'sanitize' => 'sanitize_text_field',
],
'shop_discount_end' => [
'type' => 'string', 'description' => '活动结束时间', 'default' => '',
'sanitize' => 'sanitize_text_field',
],
'shop_discount_countdown' => [
'type' => 'string', 'description' => '倒计时显示', 'default' => '',
],
];
}
// ─── Getters ─────────────────────────────────────────────────
/**
* Get all shop term meta for a product category.
*
* @param int $term_id
* @return array
*/
public static function get_cat_meta(int $term_id): array {
$defaults = [
'limit_buy' => -1,
'shipping_fee' => '',
'guest_buy' => '',
'email_fill' => '',
'after_sale' => '',
'rebate' => '',
'service' => '',
'single_tab' => '',
'content_after' => '',
'content_layout' => '',
'content_show_bg' => '',
'list_style' => '',
];
$result = [];
foreach ($defaults as $key => $default) {
$meta_key = "shop_cat_{$key}";
$val = get_term_meta($term_id, $meta_key, true);
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
}
return $result;
}
/**
* Get all shop term meta for a product tag.
*
* @param int $term_id
* @return array
*/
public static function get_tag_meta(int $term_id): array {
$priority = get_term_meta($term_id, 'shop_tag_priority', true);
$class = get_term_meta($term_id, 'shop_tag_class', true);
$is_important = get_term_meta($term_id, 'shop_tag_is_important', true);
return [
'priority' => $priority !== '' ? (int) $priority : 0,
'class' => is_string($class) ? $class : '',
'is_important' => !empty($is_important),
];
}
/**
* Get all shop discount term meta.
*
* @param int $term_id
* @return array
*/
public static function get_discount_meta(int $term_id): array {
$defaults = [
'small_badge' => '',
'priority' => 0,
'is_important' => false,
'important_class' => '',
'scope' => 'item',
'type' => 'reduction',
'value' => '0',
'gift_config' => '',
'price_limit' => '0',
'user_limit' => '',
'start' => '',
'end' => '',
'countdown' => '',
];
$result = [];
foreach ($defaults as $key => $default) {
$meta_key = "shop_discount_{$key}";
$val = get_term_meta($term_id, $meta_key, true);
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
}
$result['priority'] = (int) $result['priority'];
$result['is_important'] = !empty($result['is_important']);
$result['price_limit'] = (float) $result['price_limit'];
return $result;
}
/**
* Resolve parameter inheritance: product → category → theme settings.
*
* Returns the effective value for a given parameter key by checking:
* 1. Product-level override (from post meta)
* 2. Category-level override (from term meta)
* 3. Theme-level default (from \AKSRT\Settings\Settings::get_shop_settings())
*
* @param int $product_id Product post ID
* @param string $param_key Parameter key (e.g. 'limit_buy', 'shipping_fee')
* @param mixed $product_value Product-level value (null if not set)
* @return mixed
*/
public static function resolve_inheritance(int $product_id, string $param_key, $product_value = null) {
// 1. Product-level override
if ($product_value !== null && $product_value !== '' && $product_value !== -1) {
return $product_value;
}
// 2. Category-level override
$terms = get_the_terms($product_id, 'aksrt_product_cat');
if (is_array($terms) && !empty($terms)) {
$cat_meta_key = "shop_cat_{$param_key}";
foreach ($terms as $term) {
$val = get_term_meta($term->term_id, $cat_meta_key, true);
if ($val !== '' && $val !== false && $val !== -1) {
return $val;
}
}
}
// 3. Theme-level default
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$defaults = $shop['product_defaults'] ?? [];
return $defaults[$param_key] ?? $product_value;
}
/**
* Add form fields for shop_discount taxonomy.
*/
public function render_discount_add_fields(): void {
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$default = $def['default'] ?? '';
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html($label) . '</label>';
$this->render_discount_field_control($key, $default, false);
echo '</div>';
}
}
/**
* Edit form fields for shop_discount taxonomy.
*/
public function render_discount_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field">';
echo '<th scope="row"><label for="' . esc_attr($key) . '">' . esc_html($label) . '</label></th>';
echo '<td>';
$this->render_discount_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param string|int|float $value
*/
private function render_discount_field_control(string $key, $value, bool $edit): void {
$id = esc_attr($key);
$name = esc_attr($key);
$val = (string) $value;
if ($key === 'shop_discount_type') {
$opts = [
'reduction' => __('立减', 'aksrt'),
'discount' => __('折扣', 'aksrt'),
'gift' => __('赠品', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_scope') {
$opts = [
'item' => __('单品', 'aksrt'),
'product' => __('商品', 'aksrt'),
'author' => __('商家', 'aksrt'),
'order' => __('整单', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_user_limit') {
$opts = [
'' => __('不限', 'aksrt'),
'vip' => __('VIP1', 'aksrt'),
'vip_2' => __('VIP2', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
echo '<label><input type="checkbox" name="' . $name . '" id="' . $id . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if ($key === 'shop_discount_gift_config') {
echo '<textarea name="' . $name . '" id="' . $id . '" rows="3" class="large-text">' . esc_textarea($val) . '</textarea>';
return;
}
$type = in_array($key, ['shop_discount_priority'], true) ? 'number' : 'text';
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true)) {
$type = 'datetime-local';
// Convert mysql-ish values to datetime-local if needed.
if ($val !== '' && str_contains($val, ' ')) {
$val = str_replace(' ', 'T', substr($val, 0, 16));
}
}
printf(
'<input type="%s" name="%s" id="%s" value="%s" class="%s" />',
esc_attr($type),
$name,
$id,
esc_attr($val),
$edit ? 'regular-text' : ''
);
}
public function save_discount_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true) && is_string($value) && str_contains($value, 'T')) {
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
}
update_term_meta($term_id, $key, $value);
}
}
public function render_cat_add_fields(): void {
foreach ($this->get_cat_field_definitions() as $key => $def) {
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
$this->render_cat_field_control($key, $def['default'] ?? '', false);
echo '</div>';
}
}
public function render_cat_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
foreach ($this->get_cat_field_definitions() as $key => $def) {
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
$this->render_cat_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param mixed $value
*/
private function render_cat_field_control(string $key, $value, bool $edit): void {
$val = (string) $value;
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if (in_array($key, ['shop_cat_service', 'shop_cat_single_tab', 'shop_cat_content_after'], true)) {
echo '<textarea class="large-text" rows="3" name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">' . esc_textarea($val) . '</textarea>';
return;
}
if ($key === 'shop_cat_shipping_fee') {
$opts = ['' => __('继承', 'aksrt'), 'free' => __('包邮', 'aksrt'), 'fixed' => __('固定运费', 'aksrt'), 'amount' => __('满额包邮', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_after_sale') {
$opts = ['' => __('继承', 'aksrt'), 'refund' => __('仅退款', 'aksrt'), 'refund_return' => __('退货退款', 'aksrt'), 'none' => __('不支持', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_content_layout') {
$opts = ['' => __('继承', 'aksrt'), 'adapt' => __('自适应', 'aksrt'), 'full' => __('通栏', 'aksrt'), 'sidebar' => __('侧栏', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_list_style') {
$opts = ['' => __('继承', 'aksrt'), 'default' => __('默认', 'aksrt'), 'card-lg' => __('大卡片', 'aksrt'), 'card-sm' => __('小卡片', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
$type = $key === 'shop_cat_limit_buy' ? 'number' : 'text';
printf(
'<input type="%s" class="%s" name="%s" id="%s" value="%s" />',
esc_attr($type),
$edit ? 'regular-text' : '',
esc_attr($key),
esc_attr($key),
esc_attr($val)
);
}
public function save_cat_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
foreach ($this->get_cat_field_definitions() as $key => $def) {
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
update_term_meta($term_id, $key, $value);
}
}
public function render_tag_add_fields(): void {
foreach ($this->get_tag_field_definitions() as $key => $def) {
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
$this->render_tag_field_control($key, $def['default'] ?? '');
echo '</div>';
}
}
public function render_tag_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
foreach ($this->get_tag_field_definitions() as $key => $def) {
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
$this->render_tag_field_control($key, $value);
echo '</td></tr>';
}
}
/**
* @param mixed $value
*/
private function render_tag_field_control(string $key, $value): void {
$val = (string) $value;
if ($key === 'shop_tag_is_important') {
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
$type = $key === 'shop_tag_priority' ? 'number' : 'text';
printf(
'<input type="%s" class="regular-text" name="%s" id="%s" value="%s" />',
esc_attr($type),
esc_attr($key),
esc_attr($key),
esc_attr($val)
);
}
public function save_tag_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
foreach ($this->get_tag_field_definitions() as $key => $def) {
if ($key === 'shop_tag_is_important') {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
update_term_meta($term_id, $key, $value);
}
}
}
-401
View File
@@ -1,401 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
/**
* Shop product list widget — zibll `zib_shop_widget_ui_product_lists` lite.
*
* Supports: include/exclude category, discount, tag; sort order; list style;
* AJAX load-more (via rest widget list endpoint); per-page count.
*/
class WidgetProductList extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_product_list',
__('商品列表', 'aksrt'),
['description' => __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')]
);
}
/**
* Build WP_Query args from instance + page.
*
* @param array $instance
* @param int $paged
* @return array
*/
public static function build_query_args(array $instance, int $paged = 1): array {
$per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8)));
$orderby = sanitize_key($instance['orderby'] ?? 'date');
$order = in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true)
? strtoupper((string) $instance['order'])
: 'DESC';
$cat_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? '')))));
$tag_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? '')))));
$discount_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? '')))));
$exclude_cat = !empty($instance['exclude_cat']);
$query_args = [
'post_type' => 'aksrt_product',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => max(1, $paged),
'orderby' => $orderby === 'sales' ? 'meta_value_num' : ($orderby === 'views' ? 'meta_value_num' : $orderby),
'order' => $order,
'ignore_sticky_posts' => true,
];
if ($orderby === 'sales') {
$query_args['meta_key'] = '_aksrt_sales';
} elseif ($orderby === 'views') {
$query_args['meta_key'] = 'aksrt_post_views';
}
$tax_query = [];
if (!empty($cat_ids)) {
$tax_query[] = [
'taxonomy' => 'aksrt_product_cat',
'field' => 'term_id',
'terms' => $cat_ids,
'operator' => $exclude_cat ? 'NOT IN' : 'IN',
];
}
if (!empty($tag_ids)) {
$tax_query[] = [
'taxonomy' => 'aksrt_product_tag',
'field' => 'term_id',
'terms' => $tag_ids,
];
}
if (!empty($discount_ids)) {
$tax_query[] = [
'taxonomy' => 'shop_discount',
'field' => 'term_id',
'terms' => $discount_ids,
];
}
if (!empty($tax_query)) {
$query_args['tax_query'] = $tax_query;
}
return $query_args;
}
/**
* Render product cards HTML for a query.
*/
public static function render_items_html(\WP_Query $products, string $style = 'default'): string {
if (!$products->have_posts()) {
return '';
}
$oneline = $style === 'oneline';
$mini = $style === 'mini';
$list = $style === 'list';
ob_start();
while ($products->have_posts()) {
$products->the_post();
$pid = get_the_ID();
$card = class_exists('\AKSRT\Shop\Shop')
? \AKSRT\Shop\Shop::get_instance()->build_product_card_view($pid)
: [];
$price = (float) ($card['price'] ?? 0);
$image = (string) ($card['image'] ?? '');
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
if ($oneline) {
echo '<div class="swiper-slide ajax-item">';
}
if ($mini || $list) {
$row_cls = $list ? 'aksrt-product-widget__item aksrt-product-widget__item--list ajax-item flex ac' : 'aksrt-product-widget__item aksrt-product-widget__item--mini ajax-item flex ac';
$thumb_cls = $list ? 'aksrt-product-widget__thumb list' : 'aksrt-product-widget__thumb mini';
$wh = $list ? 80 : 56;
echo '<div class="' . esc_attr($row_cls) . '">';
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="' . esc_attr($thumb_cls) . '">';
if ($image !== '') {
echo '<img class="aksrt-lazy lazyload fit-cover" data-src="' . esc_url($image) . '" src="' . esc_url($ph) . '" alt="' . esc_attr(get_the_title($pid)) . '" loading="lazy" width="' . (int) $wh . '" height="' . (int) $wh . '">';
}
echo '</a>';
echo '<div class="aksrt-product-widget__info flex1 min-w0">';
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__title' . ($mini ? ' text-ellipsis' : ' text-ellipsis-2') . '">' . esc_html(get_the_title($pid)) . '</a>';
echo '<div class="aksrt-product-widget__meta flex jsb ac">';
echo '<span class="aksrt-product-widget__price c-red">&yen;' . esc_html(number_format_i18n($price, 2)) . '</span>';
if (!empty($card['sales'])) {
echo '<span class="aksrt-product-widget__sales muted-2-color em09">' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . '</span>';
}
echo '</div></div></div>';
} elseif (function_exists('get_template_part') && !empty($card['id'])) {
get_template_part('templates/parts/shop/product', 'card', [
'product' => $card,
'show_sales' => true,
]);
} else {
// Fallback card (lazy thumbs).
echo '<div class="aksrt-product-widget__item ajax-item">';
if ($image !== '') {
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__thumb">';
echo '<img class="aksrt-lazy lazyload" data-src="' . esc_url($image) . '" src="' . esc_url($ph) . '" alt="' . esc_attr(get_the_title($pid)) . '" loading="lazy">';
echo '</a>';
}
echo '<div class="aksrt-product-widget__info">';
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__title">' . esc_html(get_the_title($pid)) . '</a>';
echo '<div class="aksrt-product-widget__meta">';
echo '<span class="aksrt-product-widget__price">&yen;' . esc_html(number_format_i18n($price, 2)) . '</span>';
if (!empty($card['sales'])) {
echo '<span class="aksrt-product-widget__sales">' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . '</span>';
}
echo '</div></div></div>';
}
if ($oneline) {
echo '</div>';
}
}
wp_reset_postdata();
return (string) ob_get_clean();
}
/**
* Shared payload for REST widget list.
*
* @param array $instance
* @param int $page
* @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int}
*/
public static function query_payload(array $instance, int $page = 1): array {
$page = max(1, $page);
$args = self::build_query_args($instance, $page);
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$products = new \WP_Query($args);
$html = self::render_items_html($products, $style);
if ($html === '' && $page === 1) {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无商品', 'aksrt'))
: '<p class="aksrt-product-widget__empty ajax-item">' . esc_html__('暂无商品', 'aksrt') . '</p>';
}
$max_pages = max(1, (int) $products->max_num_pages);
$total = (int) $products->found_posts;
$has_more = $page < $max_pages;
$paginate_html = '';
if ($paginate === 'ajax' && $has_more) {
$next = add_query_arg(
array_filter([
'page' => $page + 1,
'per_page' => max(1, (int) ($instance['per_page'] ?? 8)),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')),
'cat_ids' => (string) ($instance['cat_ids'] ?? ''),
'tag_ids' => (string) ($instance['tag_ids'] ?? ''),
'discount_ids' => (string) ($instance['discount_ids'] ?? ''),
'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0,
'style' => sanitize_key((string) ($instance['style'] ?? 'default')),
'paginate' => 'ajax',
], static function ($v) {
return $v !== '' && $v !== null;
}),
rest_url('aksrt/v1/shop/frontend/widgets/products')
);
if (function_exists('aksrt_get_ajax_next_paginate')) {
$paginate_html = aksrt_get_ajax_next_paginate(
$total,
$page,
max(1, (int) ($instance['per_page'] ?? 8)),
$next
);
} else {
$paginate_html = '<div class="text-center theme-pagination ajax-pag"><div class="next-page ajax-next"><a href="' . esc_url($next) . '">' . esc_html__('加载更多', 'aksrt') . '</a></div></div>';
}
} elseif ($paginate === 'number' && $max_pages > 1) {
$num_base = add_query_arg(
array_filter([
'per_page' => max(1, (int) ($instance['per_page'] ?? 8)),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')),
'cat_ids' => (string) ($instance['cat_ids'] ?? ''),
'tag_ids' => (string) ($instance['tag_ids'] ?? ''),
'discount_ids' => (string) ($instance['discount_ids'] ?? ''),
'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0,
'style' => sanitize_key((string) ($instance['style'] ?? 'default')),
'paginate' => 'number',
], static function ($v) {
return $v !== '' && $v !== null;
}),
rest_url('aksrt/v1/shop/frontend/widgets/products')
);
$paginate_html = function_exists('aksrt_get_ajax_number_paginate')
? aksrt_get_ajax_number_paginate($total, $page, max(1, (int) ($instance['per_page'] ?? 8)), $num_base)
: '<div class="aksrt-product-widget__pagination">' . paginate_links([
'base' => add_query_arg('aksrt_product_page', '%#%'),
'format' => '',
'current' => $page,
'total' => $max_pages,
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
]) . '</div>';
} else {
$paginate_html = '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
}
return [
'html' => $html,
'paginate_html' => $paginate_html,
'has_more' => $has_more,
'page' => $page,
'max_pages' => $max_pages,
'total' => $total,
];
}
public function widget($args, $instance): void {
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$page = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$cfg = [
'per_page' => max(1, min(50, (int) ($instance['per_page'] ?? 8))),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'order' => in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $instance['order']) : 'DESC',
'cat_ids' => (string) ($instance['cat_ids'] ?? ''),
'tag_ids' => (string) ($instance['tag_ids'] ?? ''),
'discount_ids' => (string) ($instance['discount_ids'] ?? ''),
'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0,
'style' => $style,
'paginate' => $paginate,
];
$payload = self::query_payload($instance, $page);
$ajax_url = rest_url('aksrt/v1/shop/frontend/widgets/products');
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$style_class = 'aksrt-product-widget--' . $style;
echo '<div class="aksrt-product-widget ' . esc_attr($style_class) . '" data-aksrt-product-widget data-aksrt-product-config="' . esc_attr(wp_json_encode($cfg)) . '">';
if ($style === 'oneline') {
// zibll-style horizontal swiper-scroll (shared slider.js Type A).
echo '<div class="swiper-container swiper-scroll aksrt-product-oneline-swiper">';
echo '<div class="swiper-wrapper product-lists-row">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo '<div class="swiper-button-prev" aria-label="' . esc_attr__('上一页', 'aksrt') . '"></div>';
echo '<div class="swiper-button-next" aria-label="' . esc_attr__('下一页', 'aksrt') . '"></div>';
echo '</div>';
} else {
// Items sit directly under .ajaxpager so ajax-list inject/append works (zibll product-lists-row).
echo '<div class="ajaxpager aksrt-product-widget__grid product-lists-row" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($ajax_url) . '">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped — cards escaped upstream
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$defaults = [
'title' => __('商品推荐', 'aksrt'),
'per_page' => 8,
'orderby' => 'date',
'order' => 'DESC',
'cat_ids' => '',
'tag_ids' => '',
'discount_ids' => '',
'exclude_cat' => false,
'style' => 'default',
'paginate' => 'ajax',
];
$instance = wp_parse_args($instance, $defaults);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('per_page')); ?>"><?php esc_html_e('每页数量:', 'aksrt'); ?></label>
<input class="tiny-text" type="number" min="1" max="50" id="<?php echo esc_attr($this->get_field_id('per_page')); ?>" name="<?php echo esc_attr($this->get_field_name('per_page')); ?>" value="<?php echo esc_attr((string) $instance['per_page']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('orderby')); ?>"><?php esc_html_e('排序:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('orderby')); ?>" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="date" <?php selected($instance['orderby'], 'date'); ?>><?php esc_html_e('最新', 'aksrt'); ?></option>
<option value="sales" <?php selected($instance['orderby'], 'sales'); ?>><?php esc_html_e('销量', 'aksrt'); ?></option>
<option value="views" <?php selected($instance['orderby'], 'views'); ?>><?php esc_html_e('热度', 'aksrt'); ?></option>
<option value="title" <?php selected($instance['orderby'], 'title'); ?>><?php esc_html_e('标题', 'aksrt'); ?></option>
<option value="rand" <?php selected($instance['orderby'], 'rand'); ?>><?php esc_html_e('随机', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('order')); ?>"><?php esc_html_e('方向:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('order')); ?>" name="<?php echo esc_attr($this->get_field_name('order')); ?>">
<option value="DESC" <?php selected($instance['order'], 'DESC'); ?>><?php esc_html_e('降序', 'aksrt'); ?></option>
<option value="ASC" <?php selected($instance['order'], 'ASC'); ?>><?php esc_html_e('升序', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>"><?php esc_html_e('分类ID(逗号分隔):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('cat_ids')); ?>" value="<?php echo esc_attr((string) $instance['cat_ids']); ?>">
</p>
<p>
<label>
<input type="checkbox" name="<?php echo esc_attr($this->get_field_name('exclude_cat')); ?>" value="1" <?php checked(!empty($instance['exclude_cat'])); ?>>
<?php esc_html_e('排除上述分类', 'aksrt'); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>"><?php esc_html_e('标签ID(逗号分隔):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('tag_ids')); ?>" value="<?php echo esc_attr((string) $instance['tag_ids']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>"><?php esc_html_e('优惠活动ID(逗号分隔):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('discount_ids')); ?>" value="<?php echo esc_attr((string) $instance['discount_ids']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('style')); ?>"><?php esc_html_e('列表样式:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('style')); ?>" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="default" <?php selected($instance['style'], 'default'); ?>><?php esc_html_e('默认', 'aksrt'); ?></option>
<option value="card" <?php selected($instance['style'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="list" <?php selected($instance['style'], 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
<option value="oneline" <?php selected($instance['style'], 'oneline'); ?>><?php esc_html_e('横滑', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('paginate')); ?>"><?php esc_html_e('分页:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('paginate')); ?>" name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected($instance['paginate'], 'ajax'); ?>><?php esc_html_e('AJAX 加载更多', 'aksrt'); ?></option>
<option value="number" <?php selected($instance['paginate'], 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected($instance['paginate'], 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))),
'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'date')),
'order' => in_array(strtoupper((string) ($new_instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $new_instance['order']) : 'DESC',
'cat_ids' => sanitize_text_field((string) ($new_instance['cat_ids'] ?? '')),
'tag_ids' => sanitize_text_field((string) ($new_instance['tag_ids'] ?? '')),
'discount_ids' => sanitize_text_field((string) ($new_instance['discount_ids'] ?? '')),
'exclude_cat' => !empty($new_instance['exclude_cat']) ? 1 : 0,
'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')),
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
@@ -1,52 +0,0 @@
<?php
/**
* Horizontal swiper product widget — uses shared product list payload.
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetProductOneline extends WidgetProductList {
public function __construct() {
\WP_Widget::__construct(
'aksrt_product_oneline',
__('横滑商品', 'aksrt'),
[
'description' => __('AKSRT 单行横滑商品(swiper-scroll,同源 product list)。', 'aksrt'),
'classname' => 'widget_aksrt_product_oneline',
]
);
}
public function widget($args, $instance): void {
$instance['style'] = 'oneline';
// Prefer no paginate for horizontal scroller (zibll loads fixed count).
if (!isset($instance['paginate'])) {
$instance['paginate'] = 'none';
}
parent::widget($args, $instance);
}
public function form($instance): void {
$instance = wp_parse_args($instance, [
'title' => __('横滑商品', 'aksrt'),
'style' => 'oneline',
'paginate' => 'none',
'per_page' => 8,
]);
parent::form($instance);
}
public function update($new_instance, $old_instance): array {
$out = parent::update($new_instance, $old_instance);
$out['style'] = 'oneline';
if (($out['paginate'] ?? '') === '') {
$out['paginate'] = 'none';
}
return $out;
}
}
-179
View File
@@ -1,179 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
/**
* Multi-tab product widget — zibll `zib_shop_widget_ui_tab_product` lite.
* First tab renders immediately via shared payload; other tabs lazy-load via REST.
*/
class WidgetProductTab extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_product_tab',
__('多TAB商品', 'aksrt'),
['description' => __('AKSRT 多 TAB 商品列表组件(每 tab 独立筛选 + AJAX)。', 'aksrt')]
);
}
/**
* @return list<array{label:string,cat_ids:string,discount_ids:string,tag_ids:string,orderby:string}>
*/
private static function parse_tabs(string $tabs_raw): array {
$tabs = [];
foreach (array_filter(explode("\n", $tabs_raw)) as $line) {
$parts = explode('|', trim($line));
if (count($parts) >= 1 && trim($parts[0]) !== '') {
$tabs[] = [
'label' => trim($parts[0]),
'cat_ids' => trim($parts[1] ?? ''),
'discount_ids' => trim($parts[2] ?? ''),
'tag_ids' => trim($parts[3] ?? ''),
'orderby' => trim($parts[4] ?? 'date'),
];
}
}
return $tabs;
}
public function widget($args, $instance): void {
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
$tabs_raw = (string) ($instance['tabs'] ?? '');
$per_page = max(1, min(20, (int) ($instance['per_page'] ?? 8)));
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$tabs = self::parse_tabs($tabs_raw);
if (!$tabs) {
if (current_user_can('manage_options')) {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo function_exists('aksrt_get_widget_null')
? aksrt_get_widget_null(__('请至少配置一个 TAB', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
: '<p class="widget-empty">' . esc_html__('请至少配置一个 TAB', 'aksrt') . '</p>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$rest = rest_url('aksrt/v1/shop/frontend/widgets/products');
echo '<div class="aksrt-product-tab-widget" data-aksrt-product-tab data-per-page="' . esc_attr((string) $per_page) . '" data-style="' . esc_attr($style) . '" data-paginate="' . esc_attr($paginate) . '" data-rest="' . esc_url($rest) . '">';
echo '<div class="aksrt-product-tab__headers index-tab">';
foreach ($tabs as $i => $tab) {
$active = $i === 0 ? ' is-active' : '';
echo '<button type="button" class="aksrt-product-tab__header' . esc_attr($active) . '" data-tab-index="' . esc_attr((string) $i) . '"';
echo ' data-cat-ids="' . esc_attr($tab['cat_ids']) . '"';
echo ' data-discount-ids="' . esc_attr($tab['discount_ids']) . '"';
echo ' data-tag-ids="' . esc_attr($tab['tag_ids']) . '"';
echo ' data-orderby="' . esc_attr(sanitize_key($tab['orderby'] ?: 'date')) . '"';
echo '>' . esc_html($tab['label']) . '</button>';
}
echo '</div>';
foreach ($tabs as $i => $tab) {
$hidden = $i > 0 ? ' style="display:none"' : '';
$loaded = $i === 0 ? '1' : '0';
echo '<div class="aksrt-product-tab__panel" data-tab-index="' . esc_attr((string) $i) . '" data-loaded="' . esc_attr($loaded) . '"' . $hidden . '>';
if ($i === 0) {
$inst = [
'per_page' => $per_page,
'orderby' => sanitize_key($tab['orderby'] ?: 'date'),
'order' => 'DESC',
'cat_ids' => $tab['cat_ids'],
'tag_ids' => $tab['tag_ids'],
'discount_ids' => $tab['discount_ids'],
'style' => $style,
'paginate' => $paginate,
];
$payload = class_exists('\AKSRT\Widgets\WidgetProductList')
? WidgetProductList::query_payload($inst, 1)
: ['html' => '', 'paginate_html' => ''];
echo '<div class="ajaxpager aksrt-product-widget__grid product-lists-row aksrt-product-widget--' . esc_attr($style) . '" data-aksrt-ajaxpager>';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
} else {
echo '<div class="ajaxpager aksrt-product-widget__grid product-lists-row aksrt-product-widget--' . esc_attr($style) . ' is-lazy" data-aksrt-ajaxpager>';
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader>';
echo '<div class="aksrt-product-tab__skeleton">';
for ($si = 0; $si < min(4, $per_page); $si++) {
echo '<div class="placeholder-row product-card-placeholder"><div class="placeholder thumb"></div><div class="placeholder k1"></div><div class="placeholder s1"></div></div>';
}
echo '</div></div>';
echo '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
echo '</div>';
}
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$defaults = [
'title' => __('商品精选', 'aksrt'),
'tabs' => "热门推荐|\n最新上架|",
'per_page' => 8,
'style' => 'default',
'paginate' => 'ajax',
];
$instance = wp_parse_args($instance, $defaults);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('tabs')); ?>"><?php esc_html_e('TAB配置(每行: 名称|分类ID|优惠ID|标签ID|排序):', 'aksrt'); ?></label>
<textarea class="widefat" rows="5" id="<?php echo esc_attr($this->get_field_id('tabs')); ?>" name="<?php echo esc_attr($this->get_field_name('tabs')); ?>"><?php echo esc_textarea((string) $instance['tabs']); ?></textarea>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('per_page')); ?>"><?php esc_html_e('每页数量:', 'aksrt'); ?></label>
<input class="tiny-text" type="number" min="1" max="20" id="<?php echo esc_attr($this->get_field_id('per_page')); ?>" name="<?php echo esc_attr($this->get_field_name('per_page')); ?>" value="<?php echo esc_attr((string) $instance['per_page']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('style')); ?>"><?php esc_html_e('样式:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('style')); ?>" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="default" <?php selected($instance['style'], 'default'); ?>><?php esc_html_e('默认', 'aksrt'); ?></option>
<option value="card" <?php selected($instance['style'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('paginate')); ?>"><?php esc_html_e('分页:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('paginate')); ?>" name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected($instance['paginate'], 'ajax'); ?>><?php esc_html_e('AJAX', 'aksrt'); ?></option>
<option value="number" <?php selected($instance['paginate'], 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected($instance['paginate'], 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'tabs' => sanitize_textarea_field((string) ($new_instance['tabs'] ?? '')),
'per_page' => max(1, min(20, (int) ($new_instance['per_page'] ?? 8))),
'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')),
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}