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:
kate522 committed 2026-08-02 21:25:52 +08:00
1 parent 2bbd7c01a3
commit 1d2505356e
103 files changed
+46 -21267

No files matched your search

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) {