导入 AKSRT 主题开发源码
This commit is contained in:
1 parent
9681dd3d48
commit
1268a55dd0
595 files changed
+127705
No files matched your search
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/**
|
||||
* Shop address book service.
|
||||
*
|
||||
* Extracted from AKSRT_Shop to isolate user address-book CRUD
|
||||
* (user_meta['aksrt_shop_addresses']) into a cohesive unit.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Shop_Address_Service {
|
||||
|
||||
private AKSRT_Shop $shop;
|
||||
|
||||
public function __construct(AKSRT_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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* Shop cart service.
|
||||
*
|
||||
* Extracted from AKSRT_Shop to isolate server-side cart storage
|
||||
* (user_meta['shop_cart']) and validation into a cohesive unit.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Shop_Cart_Service {
|
||||
|
||||
private AKSRT_Shop $shop;
|
||||
|
||||
public function __construct(AKSRT_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 AKSRT_Shop_Term_Meta
|
||||
if (class_exists('AKSRT_Shop_Term_Meta')) {
|
||||
$resolved = AKSRT_Shop_Term_Meta::resolve_inheritance($product_id, 'guest_buy');
|
||||
if (empty($resolved)) {
|
||||
return 'login_required';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
/**
|
||||
* Shop checkout service.
|
||||
*
|
||||
* Extracted from AKSRT_Shop to isolate checkout preview, order
|
||||
* creation, and payment orchestration into a cohesive unit.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Shop_Checkout_Service {
|
||||
|
||||
private AKSRT_Shop $shop;
|
||||
|
||||
public function __construct(AKSRT_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_Discount')) {
|
||||
$discount_result = AKSRT_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'] ?? ''));
|
||||
$coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee']);
|
||||
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_Order_Repository')) {
|
||||
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'],
|
||||
];
|
||||
|
||||
$order_number = AKSRT_Order_Repository::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_Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'expires_at' => date('Y-m-d H:i:s', strtotime('+10 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_Coupon')) {
|
||||
$coupon_instance = AKSRT_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_Order_Repository')) {
|
||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||
}
|
||||
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
$order = $repo->get_user_order($user_id, $order_number);
|
||||
if (!$order) {
|
||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||
}
|
||||
|
||||
// 未显式指定时,使用订单创建时选定的支付方式。
|
||||
$payment_method = sanitize_key($payment_method ?: (string) ($order['payment_method'] ?? 'balance'));
|
||||
$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')];
|
||||
}
|
||||
|
||||
// 携带订单号与摘要,供网关生成支付单。
|
||||
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
|
||||
|
||||
// Points payment: synchronous deduction via AKSRT_Credits
|
||||
if ($payment_method === 'points') {
|
||||
return $this->pay_with_points($user_id, $order_number, $order);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
if (empty($payment_result['success'])) {
|
||||
return ['success' => false, 'message' => $payment_result['message'] ?? __('支付失败。', 'aksrt')];
|
||||
}
|
||||
|
||||
// 跳转/扫码型网关(支付宝/微信/PayPal/Stripe):返回支付凭据,订单保持待支付,
|
||||
// 由 /payment/{gateway}/notify|return 回调确认到账;此处绝不预先标记已支付。
|
||||
$code_url = $payment_result['payment_data']['code_url'] ?? '';
|
||||
if (!empty($payment_result['payment_url']) || $code_url !== '') {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
|
||||
'qr_content' => (string) $code_url,
|
||||
'order_number' => $order_number,
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// 同步型网关(站内余额):create_payment 已实际扣款,标记已支付(触发 aksrt_order_paid 履约)。
|
||||
$repo->update_status($order_number, AKSRT_Order::STATUS_PAID, [
|
||||
'payment_method' => $payment_method,
|
||||
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
|
||||
'note' => __('用户在前台完成支付。', 'aksrt'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order' => $this->shop->get_order_detail_view($user_id, $order_number),
|
||||
'message' => $payment_result['message'] ?? __('支付成功。', 'aksrt'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function pay_with_points(int $user_id, string $order_number, array $order): array {
|
||||
if (!class_exists('AKSRT_Credits')) {
|
||||
return ['success' => false, 'message' => __('积分系统不可用。', 'aksrt')];
|
||||
}
|
||||
|
||||
$amount = (int) round((float) ($order['amount'] ?? 0));
|
||||
if ($amount <= 0) {
|
||||
return ['success' => false, 'message' => __('订单金额无效。', 'aksrt')];
|
||||
}
|
||||
|
||||
$balance = AKSRT_Credits::get_balance($user_id);
|
||||
if ($balance < $amount) {
|
||||
return ['success' => false, 'message' => sprintf(__('积分不足,需要 %d 积分,当前余额 %d。', 'aksrt'), $amount, $balance)];
|
||||
}
|
||||
|
||||
$deducted = AKSRT_Credits::deduct($amount, $user_id, 'shop_points_pay', sprintf(__('积分购买订单 %s', 'aksrt'), $order_number));
|
||||
if (!$deducted) {
|
||||
return ['success' => false, 'message' => __('积分扣除失败。', 'aksrt')];
|
||||
}
|
||||
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
$repo->update_status($order_number, AKSRT_Order::STATUS_PAID, [
|
||||
'payment_method' => 'points',
|
||||
'payment_transaction_id' => 'points_' . $order_number,
|
||||
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
|
||||
]);
|
||||
|
||||
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 {
|
||||
if ($coupon_code === '') {
|
||||
return ['success' => true, 'discount' => 0.0, 'coupon' => []];
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_Coupon')) {
|
||||
return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')];
|
||||
}
|
||||
|
||||
$result = AKSRT_Coupon::get_instance()->validate($coupon_code, $order_amount);
|
||||
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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Shop Product Service
|
||||
*
|
||||
* Variant and product-meta helpers extracted from AKSRT_Shop.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Shop_Product_Service {
|
||||
|
||||
private AKSRT_Shop $shop;
|
||||
|
||||
public function __construct(AKSRT_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 {
|
||||
return [
|
||||
'balance' => [
|
||||
'value' => 'balance',
|
||||
'label' => __('站内余额', 'aksrt'),
|
||||
'description' => __('即时完成支付并写回订单状态。', 'aksrt'),
|
||||
],
|
||||
'points' => [
|
||||
'value' => 'points',
|
||||
'label' => __('积分兑换', 'aksrt'),
|
||||
'description' => __('使用积分购买积分商品。', 'aksrt'),
|
||||
],
|
||||
'manual_review' => [
|
||||
'value' => 'manual_review',
|
||||
'label' => __('人工确认', 'aksrt'),
|
||||
'description' => __('保留待支付订单,后续由管理员确认。', 'aksrt'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Shop_Part_1 {
|
||||
|
||||
public function create_after_sale_request(int $user_id, array $payload): array {
|
||||
if ($user_id <= 0 || !class_exists('AKSRT_After_Sale')) {
|
||||
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_After_Sale::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_After_Sale::get_instance()->create([
|
||||
'order_number' => $order_number,
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'reason' => $reason,
|
||||
'amount' => (float) ($order['amount'] ?? 0),
|
||||
'note' => sanitize_textarea_field((string) ($payload['note'] ?? '')),
|
||||
]);
|
||||
|
||||
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),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
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'] ?? 1));
|
||||
$per_page = 12;
|
||||
|
||||
if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true)) {
|
||||
return ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0];
|
||||
}
|
||||
|
||||
$term = get_term($term_id, $taxonomy);
|
||||
if (is_wp_error($term) || !$term) {
|
||||
return ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0];
|
||||
}
|
||||
|
||||
$query = new WP_Query([
|
||||
'post_type' => 'aksrt_product',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $paged,
|
||||
'post_status' => 'publish',
|
||||
'tax_query' => [[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => $term_id,
|
||||
]],
|
||||
]);
|
||||
|
||||
$products = [];
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$products[] = $this->build_product_card_view(get_the_ID());
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
|
||||
return [
|
||||
'products' => $products,
|
||||
'term' => [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'description' => $term->description,
|
||||
'count' => $term->count,
|
||||
],
|
||||
'total' => (int) $query->found_posts,
|
||||
'max_pages' => (int) $query->max_num_pages,
|
||||
];
|
||||
}
|
||||
|
||||
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') ? AKSRT_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_user_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_Order_Repository') ? AKSRT_Order_Repository::get_instance()->get_stats()['total'] : 0,
|
||||
'revenue_total' => class_exists('AKSRT_Order_Repository') ? AKSRT_Order_Repository::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_Shipping') ? AKSRT_Shipping::get_instance()->get_by_order($order_number) : [];
|
||||
$after_sale_records = class_exists('AKSRT_After_Sale') ? AKSRT_After_Sale::get_instance()->get_list([
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'order_number' => $order_number,
|
||||
]) : ['items' => []];
|
||||
|
||||
$status = (string) ($order['status'] ?? AKSRT_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_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_Order::STATUS_PENDING, AKSRT_Order::STATUS_EXPIRED], true) && $payment_value !== 'manual_review',
|
||||
'can_cancel' => in_array($status, [AKSRT_Order::STATUS_PENDING, AKSRT_Order::STATUS_CREATED, AKSRT_Order::STATUS_EXPIRED], true),
|
||||
'can_after_sale' => in_array($status, [AKSRT_Order::STATUS_PAID, AKSRT_Order::STATUS_DELIVERED, AKSRT_Order::STATUS_COMPLETED], true),
|
||||
'can_receipt' => $status === AKSRT_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;
|
||||
}
|
||||
|
||||
private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array {
|
||||
$timeline = [];
|
||||
$date_fields = [
|
||||
'created_at' => __('订单创建', 'aksrt'),
|
||||
'paid_at' => __('完成支付', 'aksrt'),
|
||||
'delivered_at' => __('订单发货', 'aksrt'),
|
||||
'completed_at' => __('交易完成', 'aksrt'),
|
||||
'cancelled_at' => __('订单取消', 'aksrt'),
|
||||
'refunded_at' => __('完成退款', 'aksrt'),
|
||||
];
|
||||
|
||||
foreach ($date_fields as $key => $label) {
|
||||
if (!empty($order[$key])) {
|
||||
$timeline[] = [
|
||||
'time' => (string) $order[$key],
|
||||
'label' => $label,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($shipping_records as $record) {
|
||||
$timeline[] = [
|
||||
'time' => (string) ($record['created_at'] ?? ''),
|
||||
'label' => sprintf(__('物流更新:%s', 'aksrt'), $record['tracking_no'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($after_sale_records as $record) {
|
||||
$timeline[] = [
|
||||
'time' => (string) ($record['created_at'] ?? ''),
|
||||
'label' => sprintf(__('售后申请:%s', 'aksrt'), $record['reason'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
usort($timeline, static function ($a, $b) {
|
||||
return strcmp($a['time'] ?? '', $b['time'] ?? '');
|
||||
});
|
||||
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
private function get_order_countdown(string $expires_at, string $status): int {
|
||||
if ($expires_at === '' || !in_array($status, [AKSRT_Order::STATUS_PENDING, AKSRT_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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Shop_Part_2 {
|
||||
|
||||
|
||||
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_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_Order_Repository')) {
|
||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||
}
|
||||
|
||||
$repo = AKSRT_Order_Repository::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_Order::STATUS_DELIVERED) {
|
||||
return ['success' => false, 'message' => __('当前订单状态不可确认收货。', 'aksrt')];
|
||||
}
|
||||
|
||||
$repo->update_status($order_number, AKSRT_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_Order_Repository') || !class_exists('AKSRT_Settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shop = AKSRT_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_Order_Repository::get_instance();
|
||||
$orders = $repo->get_orders_admin(['status' => AKSRT_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_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_Order_Repository')) {
|
||||
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_Order_Repository::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')) {
|
||||
return $this->get_payment_method_options();
|
||||
}
|
||||
$shop = AKSRT_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')) {
|
||||
return $this->get_shipping_method_options();
|
||||
}
|
||||
$shop = AKSRT_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')) {
|
||||
return ['refund' => [], 'return' => []];
|
||||
}
|
||||
$shop = AKSRT_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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Shop_Part_3 {
|
||||
|
||||
|
||||
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): 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);
|
||||
}
|
||||
if ($type === 'invit_code') {
|
||||
return $this->create_invite_codes($qty, $user_id, $order_number);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number): string {
|
||||
global $wpdb;
|
||||
$table = AKSRT_DB_Migrator::table('card_key');
|
||||
if ($table === '') 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",
|
||||
...$params
|
||||
), ARRAY_A);
|
||||
|
||||
if (empty($keys)) {
|
||||
return __('卡密已售罄', 'aksrt');
|
||||
}
|
||||
|
||||
$codes = [];
|
||||
$ids = [];
|
||||
foreach ($keys as $k) {
|
||||
$codes[] = $k['card_code'];
|
||||
$ids[] = (int) $k['id'];
|
||||
}
|
||||
if (!empty($ids)) {
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE id IN ({$placeholders})",
|
||||
array_merge([$order_number, $user_id, current_time('mysql')], $ids)
|
||||
));
|
||||
}
|
||||
return implode("\n", $codes);
|
||||
}
|
||||
|
||||
private function create_invite_codes(int $qty, int $user_id, string $order_number): string {
|
||||
if (!class_exists('AKSRT_Invite')) {
|
||||
return __('邀请系统不可用', 'aksrt');
|
||||
}
|
||||
$invite = AKSRT_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_DB_Migrator::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 = [];
|
||||
|
||||
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);
|
||||
if ($content !== '') {
|
||||
$auto_delivery_parts[] = $content;
|
||||
$this->update_order_item_delivery($item['id'], $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
|
||||
}
|
||||
|
||||
private function update_order_item_delivery(int $item_id, string $content): void {
|
||||
global $wpdb;
|
||||
$table = $this->order_item_table();
|
||||
if ($table === '') return;
|
||||
$wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']);
|
||||
}
|
||||
|
||||
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_Invite')) {
|
||||
$referrer_id = AKSRT_Invite::get_instance()->get_referrer($user_id);
|
||||
}
|
||||
if ($referrer_id <= 0) return;
|
||||
|
||||
$rebate_table = AKSRT_DB_Migrator::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_Term_Meta')) {
|
||||
$resolved = AKSRT_Shop_Term_Meta::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;
|
||||
|
||||
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') && (int) $gift_value > 0) {
|
||||
AKSRT_Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number));
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user