refactor(autoload): migrate theme classes to Composer PSR-4

Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
This commit is contained in:
kate522 committed 2026-07-17 13:29:22 +08:00
1 parent 8f349b40fb
commit aaf458af1c
404 files changed
+13602 -7705

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class AddressService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
/**
* 获取用户地址簿(公开接口)
*/
public function get_address_book(int $user_id): array {
return $this->get_user_address_book($user_id);
}
public function get_user_address_book(int $user_id): array {
$saved = get_user_meta($user_id, 'aksrt_shop_addresses', true);
if (is_array($saved) && !empty($saved)) {
return array_values(array_filter(array_map([$this, 'sanitize_address'], $saved)));
}
return [];
}
private function sanitize_address($address): array {
if (!is_array($address)) {
return [];
}
$name = sanitize_text_field((string) ($address['name'] ?? ''));
$phone = sanitize_text_field((string) ($address['phone'] ?? ''));
$province = sanitize_text_field((string) ($address['province'] ?? ''));
$city = sanitize_text_field((string) ($address['city'] ?? ''));
$district = sanitize_text_field((string) ($address['district'] ?? ''));
$street = sanitize_text_field((string) ($address['address'] ?? ''));
return [
'id' => (int) ($address['id'] ?? 0),
'name' => $name,
'phone' => $phone,
'province' => $province,
'city' => $city,
'district' => $district,
'address' => $street,
'is_default' => !empty($address['is_default']),
'full_text' => trim($name . ' / ' . $province . $city . $district . $street),
];
}
/**
* 新增地址
*
* @param int $user_id
* @param array $data 地址数据(name, phone, province, city, district, address, is_default)
* @return array{success:bool, address:array|null, error:string|null}
*/
public function add_address(int $user_id, array $data): array {
$address = $this->sanitize_address($data);
if ($address['name'] === '' || $address['phone'] === '' || $address['address'] === '') {
return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')];
}
$book = $this->get_user_address_book($user_id);
$address['id'] = $this->next_address_id($book);
if (!empty($data['is_default']) || empty($book)) {
foreach ($book as &$item) {
$item['is_default'] = false;
}
$address['is_default'] = true;
}
$book[] = $address;
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'address' => $address, 'error' => null];
}
/**
* 更新地址
*
* @param int $user_id
* @param int $address_id
* @param array $data
* @return array{success:bool, address:array|null, error:string|null}
*/
public function update_address(int $user_id, int $address_id, array $data): array {
$book = $this->get_user_address_book($user_id);
$found = false;
foreach ($book as &$item) {
if ((int) ($item['id'] ?? 0) !== $address_id) {
continue;
}
$found = true;
$merged = array_merge($item, $this->sanitize_address(array_merge($item, $data)));
$merged['id'] = $address_id;
if ($merged['name'] === '' || $merged['phone'] === '' || $merged['address'] === '') {
return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')];
}
if (!empty($data['is_default'])) {
foreach ($book as &$clear) {
$clear['is_default'] = false;
}
$merged['is_default'] = true;
}
$item = $merged;
break;
}
if (!$found) {
return ['success' => false, 'address' => null, 'error' => __('地址不存在。', 'aksrt')];
}
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'address' => $merged, 'error' => null];
}
/**
* 删除地址
*
* @param int $user_id
* @param int $address_id
* @return array{success:bool, error:string|null}
*/
public function delete_address(int $user_id, int $address_id): array {
$book = $this->get_user_address_book($user_id);
$found = false;
$was_default = false;
$filtered = [];
foreach ($book as $item) {
if ((int) ($item['id'] ?? 0) === $address_id) {
$found = true;
$was_default = !empty($item['is_default']);
continue;
}
$filtered[] = $item;
}
if (!$found) {
return ['success' => false, 'error' => __('地址不存在。', 'aksrt')];
}
if ($was_default && !empty($filtered)) {
$filtered[0]['is_default'] = true;
}
update_user_meta($user_id, 'aksrt_shop_addresses', array_values($filtered));
return ['success' => true, 'error' => null];
}
/**
* 设置默认地址
*
* @param int $user_id
* @param int $address_id
* @return array{success:bool, error:string|null}
*/
public function set_default_address(int $user_id, int $address_id): array {
$book = $this->get_user_address_book($user_id);
$found = false;
foreach ($book as &$item) {
$item['is_default'] = ((int) ($item['id'] ?? 0) === $address_id);
if ($item['is_default']) {
$found = true;
}
}
if (!$found) {
return ['success' => false, 'error' => __('地址不存在。', 'aksrt')];
}
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'error' => null];
}
/**
* 生成下一个地址 ID
*/
private function next_address_id(array $book): int {
$max = 0;
foreach ($book as $item) {
$max = max($max, (int) ($item['id'] ?? 0));
}
return $max + 1;
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class CartService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
public function get_cart(int $user_id): array {
if ($user_id <= 0) {
return [];
}
$cart = get_user_meta($user_id, 'shop_cart', true);
return is_array($cart) ? $cart : [];
}
public function get_cart_count(int $user_id): int {
$cart = $this->get_cart($user_id);
$count = 0;
foreach ($cart as $opts) {
if (is_array($opts)) {
foreach ($opts as $qty) {
$count += (int) $qty;
}
}
}
return $count;
}
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
if ($user_id <= 0 || $product_id <= 0 || $count <= 0) {
return ['error' => 'invalid_params'];
}
$validation = $this->validate_cart_item($product_id, $options_string);
if ($validation !== true) {
return ['error' => $validation];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if (!isset($cart[$pid_key]) || !is_array($cart[$pid_key])) {
$cart[$pid_key] = [];
}
$cart[$pid_key][$opt_key] = ((int) ($cart[$pid_key][$opt_key] ?? 0)) + $count;
// Enforce limit_buy if set
$limit = (int) get_post_meta($product_id, '_aksrt_limit_buy', true);
if ($limit > 0 && $cart[$pid_key][$opt_key] > $limit) {
$cart[$pid_key][$opt_key] = $limit;
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
if ($user_id <= 0 || $product_id <= 0 || $count <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if (isset($cart[$pid_key][$opt_key])) {
$cart[$pid_key][$opt_key] -= $count;
if ($cart[$pid_key][$opt_key] <= 0) {
unset($cart[$pid_key][$opt_key]);
}
}
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if ($options_string === '' && isset($cart[$pid_key])) {
// Remove all options for this product
unset($cart[$pid_key]);
} elseif (isset($cart[$pid_key][$opt_key])) {
unset($cart[$pid_key][$opt_key]);
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function remove_multi_from_cart(int $user_id, array $items): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
foreach ($items as $item) {
$pid_key = (string) ($item['product_id'] ?? 0);
$opt_key = !empty($item['options_string']) ? $item['options_string'] : '_default';
if (isset($cart[$pid_key][$opt_key])) {
unset($cart[$pid_key][$opt_key]);
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
}
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function empty_cart(int $user_id): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
delete_user_meta($user_id, 'shop_cart');
return ['success' => true, 'cart_count' => 0];
}
/**
* Validate a product for cart addition.
* Returns true if valid, or a string error code.
*/
public function validate_cart_item(int $product_id, string $options_string = '') {
$post = get_post($product_id);
if (!$post || $post->post_type !== 'aksrt_product') {
return 'invalid_product';
}
if ($post->post_status !== 'publish') {
return 'product_not_published';
}
// Check stock (unless unlimited)
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
if ($stock > -1 && $stock <= 0) {
return 'out_of_stock';
}
// Check guest_buy — if disabled and user not logged in
if (!is_user_logged_in()) {
$guest = get_post_meta($product_id, '_aksrt_guest_buy', true);
if ($guest === '0' || $guest === '') {
// Fall back to inheritance chain via TermMeta
if (class_exists('TermMeta')) {
$resolved = TermMeta::resolve_inheritance($product_id, 'guest_buy');
if (empty($resolved)) {
return 'login_required';
}
}
}
}
return true;
}
}
+375
View File
@@ -0,0 +1,375 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class CheckoutService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
public function preview_checkout(int $user_id, array $payload): array {
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录后再结算。', 'aksrt')];
}
$items = $this->normalize_checkout_items($payload['items'] ?? []);
if (empty($items)) {
return ['success' => false, 'message' => __('请选择至少一个商品。', 'aksrt')];
}
$product_views = [];
$subtotal = 0.0;
foreach ($items as $item) {
$product = $this->shop->get_product_detail_view((int) $item['product_id']);
if (empty($product) || empty($product['purchase_enabled'])) {
return ['success' => false, 'message' => __('存在不可购买商品,请刷新后重试。', 'aksrt')];
}
if ((int) $product['stock'] < (int) $item['quantity']) {
return ['success' => false, 'message' => sprintf(__('商品 %s 库存不足。', 'aksrt'), $product['title'])];
}
$line_total = round(((float) $product['price']) * (int) $item['quantity'], 2);
// R69: Apply discount engine (shop_discount taxonomy campaigns)
$discount_result = ['discount_amount' => 0, 'gifts' => [], 'applied' => []];
if (class_exists('\AKSRT\Payment\Discount')) {
$discount_result = \AKSRT\Payment\Discount::get_instance()->calculate(
(float) $product['price'],
(int) $item['quantity'],
(int) $product['id'],
$user_id
);
$line_total = $discount_result['final_price'];
}
$subtotal += $line_total;
$product_views[] = [
'product_id' => (int) $product['id'],
'title' => $product['title'],
'price' => (float) $product['price'],
'quantity' => (int) $item['quantity'],
'line_total' => $line_total,
'image' => $product['image'],
'shipping' => $product['shipping'],
'discount_amount' => (float) ($discount_result['discount_amount'] ?? 0),
'gifts' => (array) ($discount_result['gifts'] ?? []),
'applied_discounts' => (array) ($discount_result['applied'] ?? []),
];
}
$shipping_method = sanitize_key((string) ($payload['shipping_method'] ?? 'express'));
$shipping_profile = $this->resolve_shipping_quote($product_views, $shipping_method);
$coupon_code = sanitize_text_field((string) ($payload['coupon_code'] ?? ''));
$product_ids = array_values(array_unique(array_map(static function ($row) {
return (int) ($row['product_id'] ?? $row['id'] ?? 0);
}, $product_views)));
$coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee'], $product_ids);
if (!$coupon_result['success']) {
return $coupon_result;
}
$total = max(0, round($subtotal + $shipping_profile['fee'] - $coupon_result['discount'], 2));
$address = $this->resolve_checkout_address($user_id, (int) ($payload['address_id'] ?? 0));
$payment_method = $this->resolve_payment_method((string) ($payload['payment_method'] ?? 'balance'));
return [
'success' => true,
'data' => [
'items' => $product_views,
'subtotal' => round($subtotal, 2),
'shipping_fee' => $shipping_profile['fee'],
'shipping_method' => $shipping_profile['method'],
'discount' => $coupon_result['discount'],
'coupon' => $coupon_result['coupon'],
'address' => $address,
'payment_method' => $payment_method,
'total' => $total,
],
];
}
public function create_checkout_order(int $user_id, array $payload): array {
$preview = $this->preview_checkout($user_id, $payload);
if (empty($preview['success'])) {
return $preview;
}
$data = $preview['data'];
$primary_item = $data['items'][0] ?? null;
if (!$primary_item || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')];
}
$payment_method = sanitize_key((string) ($data['payment_method']['value'] ?? 'balance'));
$meta_payload = [
'customer_note' => sanitize_textarea_field((string) ($payload['customer_note'] ?? '')),
'items' => $data['items'],
'coupon' => $data['coupon'],
'shipping_fee' => $data['shipping_fee'],
'shipping_method' => $data['shipping_method'],
'address' => $data['address'],
'payment_method' => $data['payment_method'],
'discount' => $data['discount'],
];
$expiry_minutes = 30;
if (class_exists('\AKSRT\Settings\Settings')) {
$order_settings = \AKSRT\Settings\Settings::get_order_settings();
$expiry_minutes = max(5, min(1440, (int) ($order_settings['expiry'] ?? 30)));
}
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
'user_id' => $user_id,
'product_type' => 'shop_product',
'product_id' => (int) $primary_item['product_id'],
'amount' => (float) $data['total'],
'currency' => 'CNY',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
'expires_at' => date('Y-m-d H:i:s', strtotime('+' . $expiry_minutes . ' minutes')),
'note' => wp_json_encode($meta_payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
if ($order_number === '') {
return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')];
}
$this->shop->save_order_items($order_number, $data['items']);
if (!empty($data['coupon']['code']) && class_exists('\AKSRT\Payment\Coupon')) {
$coupon_instance = \AKSRT\Payment\Coupon::get_instance();
$coupon_instance->use_coupon($data['coupon']['code']);
// R69: Log coupon usage for per-user tracking
$coupon_row = $coupon_instance->validate($data['coupon']['code']);
$coupon_id = (int) ($coupon_row['coupon']['id'] ?? 0);
if ($coupon_id > 0) {
$coupon_instance->log_usage(
$coupon_id,
$user_id,
$order_number,
(float) ($data['discount'] ?? 0)
);
}
}
$detail = $this->shop->get_order_detail_view($user_id, $order_number);
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'order' => $detail,
],
];
}
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
// 未显式指定时,使用订单创建时选定的支付方式。
$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\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\Payment\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\Credits')) {
return ['success' => false, 'message' => __('积分系统不可用。', 'aksrt')];
}
$amount = (int) round((float) ($order['amount'] ?? 0));
if ($amount <= 0) {
return ['success' => false, 'message' => __('订单金额无效。', 'aksrt')];
}
$balance = \AKSRT\Credits\Credits::balance($user_id);
if ($balance < $amount) {
return ['success' => false, 'message' => sprintf(__('积分不足,需要 %d 积分,当前余额 %d。', 'aksrt'), $amount, $balance)];
}
$deducted = \AKSRT\Credits\Credits::deduct(
$user_id,
$amount,
'shop_points_pay',
['order' => $order_number],
sprintf(__('积分购买订单 %s', 'aksrt'), $order_number)
);
if (is_wp_error($deducted)) {
return ['success' => false, 'message' => $deducted->get_error_message() ?: __('积分扣除失败。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$repo->update_status($order_number, \AKSRT\Payment\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 $product_ids = []): array {
if ($coupon_code === '') {
return ['success' => true, 'discount' => 0.0, 'coupon' => []];
}
if (!class_exists('\AKSRT\Payment\Coupon')) {
return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')];
}
$result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $order_amount, [
'user_id' => get_current_user_id(),
'product_ids' => array_values(array_filter(array_map('intval', $product_ids))),
'context' => 'shop',
]);
if (empty($result['valid'])) {
return ['success' => false, 'message' => $result['message'] ?? __('优惠券不可用。', 'aksrt')];
}
return [
'success' => true,
'discount' => (float) ($result['discount'] ?? 0),
'coupon' => [
'code' => (string) ($result['coupon']['code'] ?? $coupon_code),
'type' => (string) ($result['coupon']['type'] ?? ''),
'value' => (float) ($result['coupon']['value'] ?? 0),
],
];
}
private function resolve_shipping_quote(array $items, string $shipping_method): array {
$methods = $this->shop->get_shipping_method_options();
$method = $methods[$shipping_method] ?? $methods['express'];
$base_fee = (float) ($method['fee'] ?? 0);
foreach ($items as $item) {
$base_fee += (float) ($item['shipping']['fee'] ?? 0);
}
return [
'method' => $method,
'fee' => round($base_fee, 2),
];
}
private function resolve_checkout_address(int $user_id, int $address_id): array {
$addresses = $this->shop->get_address_book($user_id);
foreach ($addresses as $address) {
if ((int) ($address['id'] ?? 0) === $address_id) {
return $address;
}
}
return $addresses[0] ?? [];
}
private function resolve_payment_method(string $value): array {
$methods = $this->shop->get_payment_method_options();
return $methods[$value] ?? reset($methods);
}
public function get_checkout_hint(array $product): array {
return [
'delivery' => $product['shipping']['delivery_note'] ?: __('付款后自动进入订单中心,可继续查看物流和售后进度。', 'aksrt'),
'service' => $product['after_sale']['policy'] ?: __('支持在订单中心发起售后申请。', 'aksrt'),
];
}
}
+455
View File
@@ -0,0 +1,455 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part1 {
public function create_after_sale_request(int $user_id, array $payload): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$order_number = sanitize_text_field((string) ($payload['order_number'] ?? ''));
$reason = sanitize_textarea_field((string) ($payload['reason'] ?? ''));
$type = sanitize_key((string) ($payload['type'] ?? \AKSRT\Payment\AfterSale::TYPE_REFUND));
if ($order_number === '' || $reason === '') {
return ['success' => false, 'message' => __('请填写售后原因。', 'aksrt')];
}
$order = $this->get_order_detail_view($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$ticket_id = \AKSRT\Payment\AfterSale::get_instance()->create([
'order_number' => $order_number,
'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'] ?? $args['page'] ?? 1));
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : [];
$per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12))));
$orderby = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date')));
if (!in_array($orderby, ['date', 'sales', 'price_asc', 'price_desc', 'comments', 'modified'], true)) {
$orderby = 'date';
}
$paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number')));
if (!in_array($paging, ['number', 'ajax'], true)) {
$paging = 'number';
}
$ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']);
$ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5))));
$style = sanitize_key((string) ($list['style'] ?? 'default'));
$empty = [
'products' => [],
'term' => null,
'total' => 0,
'max_pages' => 0,
'page' => $paged,
'orderby' => $orderby,
'paging' => $paging,
'ias_s' => $ias_s,
'ias_max' => $ias_max,
'per_page' => $per_page,
'style' => $style,
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
];
if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) {
return $empty;
}
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $per_page,
'paged' => $paged,
'post_status' => 'publish',
'order' => 'DESC',
'orderby' => 'date',
];
$term = null;
if ($taxonomy !== '' && $term_id > 0) {
$term = get_term($term_id, $taxonomy);
if (is_wp_error($term) || !$term) {
return $empty;
}
$query_args['tax_query'] = [[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $term_id,
]];
}
if ($orderby === 'sales') {
$query_args['meta_key'] = '_aksrt_sales';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'price_asc') {
$query_args['meta_key'] = '_aksrt_price';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'ASC';
} elseif ($orderby === 'price_desc') {
$query_args['meta_key'] = '_aksrt_price';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'comments') {
$query_args['orderby'] = 'comment_count';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'modified') {
$query_args['orderby'] = 'modified';
$query_args['order'] = 'DESC';
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
return [
'products' => $products,
'term' => $term ? [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'count' => (int) $term->count,
'taxonomy' => $taxonomy,
] : null,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
'page' => $paged,
'orderby' => $orderby,
'paging' => $paging,
'ias_s' => $ias_s,
'ias_max' => $ias_max,
'per_page' => $per_page,
'style' => $style,
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
];
}
public function get_product_categories(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_product_cat',
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
]);
if (is_wp_error($terms)) {
return [];
}
return array_map(static function ($t) {
return [
'id' => $t->term_id,
'name' => $t->name,
'count' => $t->count,
'url' => get_term_link($t),
];
}, $terms);
}
/* ============================================================
* Merchant / Store Mode (§6.10)
* ============================================================ */
public function get_store_profile(int $author_id): array {
if ($author_id <= 0) return [];
$user = get_userdata($author_id);
if (!$user) return [];
$product_count = $this->count_author_products($author_id);
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$author_show = !isset($shop_settings['general']['shop_author_show']) || !empty($shop_settings['general']['shop_author_show']);
if (!$author_show) return [];
$contact = $this->get_store_contact($author_id);
return [
'author_id' => $author_id,
'name' => (string) $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id) : get_avatar_url($author_id),
'description' => (string) get_the_author_meta('description', $author_id),
'url' => get_author_posts_url($author_id),
'product_count' => $product_count,
'contact' => $contact,
'addresses' => $this->get_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\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['total'] : 0,
'revenue_total' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['revenue'] : 0,
];
}
public function resolve_main_image(int $product_id): string {
return $this->product_service()->resolve_main_image($product_id);
}
public function build_product_card_view(int $product_id): array {
return $this->product_service()->build_product_card_view($product_id);
}
private function decorate_order_view(array $order, bool $with_detail = false): array {
$meta = $order['meta_payload'] ?? [];
$order_number = (string) ($order['order_number'] ?? '');
$order_items = $this->get_order_items($order_number);
$has_structured_items = !empty($order_items);
$product = $has_structured_items
? $this->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0))
: $this->build_product_card_view((int) ($order['product_id'] ?? 0));
$shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : [];
$after_sale_records = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list([
'limit' => 20,
'offset' => 0,
'order_number' => $order_number,
]) : ['items' => []];
$status = (string) ($order['status'] ?? \AKSRT\Payment\Order::STATUS_PENDING);
$subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount']);
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
$discount = (float) ($meta['discount'] ?? 0);
$expires_at = (string) ($order['expires_at'] ?? '');
$payment_value = (string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance');
$view = [
'id' => (int) ($order['id'] ?? 0),
'order_number' => (string) ($order['order_number'] ?? ''),
'amount' => (float) ($order['amount'] ?? 0),
'status' => $status,
'status_label' => \AKSRT\Payment\Order::get_valid_statuses()[$status] ?? $status,
'product_name' => $product['title'] ?? __('商品订单', 'aksrt'),
'product' => $product,
'created_at' => (string) ($order['created_at'] ?? ''),
'paid_at' => (string) ($order['paid_at'] ?? ''),
'expires_at' => $expires_at,
'currency_symbol' => '¥',
'subtotal' => $subtotal,
'shipping_fee' => $shipping_fee,
'discount' => $discount,
'address' => $meta['address'] ?? [],
'payment_method' => $meta['payment_method'] ?? ['value' => $order['payment_method'] ?? '', 'label' => __('站内余额', 'aksrt')],
'coupon' => $meta['coupon'] ?? [],
'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []),
'shipping_method' => $meta['shipping_method'] ?? [],
'customer_note' => $meta['customer_note'] ?? '',
'pay_url' => '#',
'actions' => [
'can_pay' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true) && $payment_value !== 'manual_review',
'can_cancel' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true),
'can_after_sale' => in_array($status, [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true),
'can_receipt' => $status === \AKSRT\Payment\Order::STATUS_DELIVERED,
],
'time_left' => $this->get_order_countdown($expires_at, $status),
];
if ($with_detail) {
$view['shipping_records'] = $shipping_records;
$view['after_sale_records'] = $after_sale_records['items'] ?? [];
$view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_records['items'] ?? []);
}
return $view;
}
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\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) {
return 0;
}
$left = strtotime($expires_at) - time();
return max(0, $left);
}
public function get_limit_buy_config(int $product_id): array {
return $this->product_service()->get_limit_buy_config($product_id);
}
public function get_product_services(int $product_id): array {
return $this->product_service()->get_product_services($product_id);
}
public function get_product_params(int $product_id): array {
return $this->product_service()->get_product_params($product_id);
}
public function get_address_book(int $user_id): array {
return $this->address_service()->get_address_book($user_id);
}
public function add_address(int $user_id, array $data): array {
return $this->address_service()->add_address($user_id, $data);
}
public function update_address(int $user_id, int $address_id, array $data): array {
return $this->address_service()->update_address($user_id, $address_id, $data);
}
public function delete_address(int $user_id, int $address_id): array {
return $this->address_service()->delete_address($user_id, $address_id);
}
public function set_default_address(int $user_id, int $address_id): array {
return $this->address_service()->set_default_address($user_id, $address_id);
}
public function get_payment_method_options(): array {
return $this->product_service()->get_payment_method_options();
}
public function get_shipping_method_options(): array {
return $this->product_service()->get_shipping_method_options();
}
/* ============================================================
* Product Variants / SKU
* ============================================================ */
public function get_variants(int $product_id): array {
return $this->product_service()->get_variants($product_id);
}
public function get_variant(int $variant_id): ?array {
return $this->product_service()->get_variant($variant_id);
}
}
+436
View File
@@ -0,0 +1,436 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part2 {
public function save_variants(int $product_id, array $variants): array {
return $this->product_service()->save_variants($product_id, $variants);
}
public function delete_variant(int $variant_id): bool {
return $this->product_service()->delete_variant($variant_id);
}
public function has_variants(int $product_id): bool {
return $this->product_service()->has_variants($product_id);
}
/**
* Get the price range for a product (considering variants).
*/
public function get_price_range(int $product_id): array {
$base_price = (float) get_post_meta($product_id, '_aksrt_price', true);
$variants = $this->get_variants($product_id);
if (empty($variants)) {
return ['min' => $base_price, 'max' => $base_price, 'has_variants' => false];
}
$prices = array_column($variants, 'price');
$prices[] = $base_price;
$prices = array_filter($prices, fn($p) => $p > 0);
if (empty($prices)) {
return ['min' => 0, 'max' => 0, 'has_variants' => true];
}
return [
'min' => min($prices),
'max' => max($prices),
'has_variants' => true,
];
}
// ── R68: Fulfillment hook (§6.5) ──
/**
* aksrt_order_paid listener: deduct stock + increment sales.
* Registered via \AKSRT\Core\Setup::init().
*/
public function handle_order_paid(string $order_number, array $order): void {
if ($this->order_item_table() !== '') {
$items = $this->get_order_items($order_number);
if (!empty($items)) {
$this->handle_order_paid_enhanced($order_number, $order);
return;
}
}
$note_raw = (string) ($order['note'] ?? '');
$meta = json_decode($note_raw, true);
if (!is_array($meta)) {
$meta = [];
}
$items = $meta['items'] ?? [];
if (empty($items) && !empty($order['product_id'])) {
$items = [
[
'product_id' => (int) $order['product_id'],
'quantity' => 1,
],
];
}
foreach ($items as $item) {
$pid = (int) ($item['product_id'] ?? 0);
$qty = max(1, (int) ($item['quantity'] ?? 1));
if ($pid <= 0) {
continue;
}
// Deduct stock (skip if unlimited, i.e. stock <= -1)
$stock = (int) get_post_meta($pid, '_aksrt_stock', true);
if ($stock > 0) {
$new_stock = max(0, $stock - $qty);
update_post_meta($pid, '_aksrt_stock', $new_stock);
}
// Increment sales_volume
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty);
}
/**
* Fires after shop fulfillment (stock + sales).
*
* @param string $order_number
* @param array $order
* @param array $items Normalized item list
*/
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
}
// ── R68: Confirm receipt (§6.5) ──
/**
* User confirms receipt: delivered -> completed.
*/
public function confirm_receipt(int $user_id, string $order_number): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if ($status !== \AKSRT\Payment\Order::STATUS_DELIVERED) {
return ['success' => false, 'message' => __('当前订单状态不可确认收货。', 'aksrt')];
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => __('用户确认收货。', 'aksrt'),
]);
return [
'success' => true,
'data' => [
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
/**
* Auto-confirm cron: complete orders delivered > receipt_max_day ago.
*/
public function run_auto_confirm_cron(): void {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = (int) ($shop['timing']['receipt_max_day'] ?? 15);
if ($max_day <= 0) {
$max_day = 15;
}
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin(['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'limit' => 500]);
foreach ($orders as $order) {
$delivered_at = (string) ($order['delivered_at'] ?? '');
if ($delivered_at === '' || $delivered_at > $cutoff) {
continue;
}
$repo->update_status((string) $order['order_number'], \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => sprintf(__('系统自动确认收货(超过 %d 天)。', 'aksrt'), $max_day),
]);
}
}
// ── R68: Order CSV export (§6.5) ──
/**
* Export orders as CSV rows. Admin-only.
*/
public function export_orders_csv(array $args = []): array {
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
return ['csv' => '', 'count' => 0];
}
$status = sanitize_key((string) ($args['status'] ?? ''));
$date_from = sanitize_text_field((string) ($args['date_from'] ?? ''));
$date_to = sanitize_text_field((string) ($args['date_to'] ?? ''));
$limit = max(1, min(5000, (int) ($args['limit'] ?? 1000)));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin(['status' => $status, 'limit' => $limit]);
if ($date_from !== '') {
$orders = array_filter($orders, function ($o) use ($date_from) {
return ($o['created_at'] ?? '') >= $date_from . ' 00:00:00';
});
}
if ($date_to !== '') {
$orders = array_filter($orders, function ($o) use ($date_to) {
return ($o['created_at'] ?? '') <= $date_to . ' 23:59:59';
});
}
$header = ['order_number', 'user_id', 'product_type', 'product_id', 'amount', 'currency', 'status', 'payment_method', 'created_at', 'paid_at'];
$rows = [implode(',', $header)];
foreach ($orders as $o) {
$rows[] = implode(',', [
(string) ($o['order_number'] ?? ''),
(int) ($o['user_id'] ?? 0),
(string) ($o['product_type'] ?? ''),
(int) ($o['product_id'] ?? 0),
(float) ($o['amount'] ?? 0),
(string) ($o['currency'] ?? 'CNY'),
(string) ($o['status'] ?? ''),
(string) ($o['payment_method'] ?? ''),
(string) ($o['created_at'] ?? ''),
(string) ($o['paid_at'] ?? ''),
]);
}
$bom = "\xEF\xBB\xBF";
$csv = $bom . implode("\r\n", $rows);
return ['csv' => $csv, 'count' => count($orders)];
}
// ── R68: Configurable shipping/payment (§6.11) ──
/**
* Get payment method options from settings, falling back to hardcoded defaults.
*/
public function get_payment_method_options_configurable(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->get_payment_method_options();
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$enabled = (string) ($shop['product_defaults']['payment_methods'] ?? '');
if ($enabled === '') {
return $this->get_payment_method_options();
}
$all = $this->get_payment_method_options();
$keys = array_map('trim', explode(',', $enabled));
$result = [];
foreach ($keys as $k) {
$k = sanitize_key($k);
if (isset($all[$k])) {
$result[$k] = $all[$k];
}
}
return $result ?: $this->get_payment_method_options();
}
/**
* Get shipping method options from settings, falling back to hardcoded defaults.
*/
public function get_shipping_method_options_configurable(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->get_shipping_method_options();
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$enabled = (string) ($shop['product_defaults']['shipping_methods'] ?? '');
$fee_override = (float) ($shop['product_defaults']['shipping_fee_fixed'] ?? 0);
if ($enabled === '' && $fee_override <= 0) {
return $this->get_shipping_method_options();
}
$all = $this->get_shipping_method_options();
if ($enabled !== '') {
$keys = array_map('trim', explode(',', $enabled));
$filtered = [];
foreach ($keys as $k) {
$k = sanitize_key($k);
if (isset($all[$k])) {
$filtered[$k] = $all[$k];
}
}
$all = $filtered ?: $all;
}
if ($fee_override > 0) {
foreach ($all as $k => &$v) {
if ($k !== 'download') {
$v['fee'] = $fee_override;
}
}
unset($v);
}
return $all;
}
/**
* Get after-sale reason options from settings.
*/
public function get_after_sale_reason_options(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return ['refund' => [], 'return' => []];
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$refund_raw = (string) ($shop['timing']['after_sale_reason_refund'] ?? '');
$return_raw = (string) ($shop['timing']['after_sale_reason_return'] ?? '');
return [
'refund' => $this->parse_reason_lines($refund_raw),
'return' => $this->parse_reason_lines($return_raw),
];
}
/**
* Parse newline-separated reason text into indexed array.
*/
private function parse_reason_lines(string $text): array {
if ($text === '') {
return [];
}
$lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $text)));
$result = [];
foreach (array_values($lines) as $i => $line) {
$result[] = [
'id' => $i + 1,
'label' => $line,
];
}
return $result;
}
/* ============================================================
* Product Options (§6.2 选项组机制)
*
* Storage: post_meta '_aksrt_product_options' = [
* [ 'name' => '颜色', 'view_mode' => 'list', 'values' => [
* [ 'label' => '红色', 'price_change' => 0, 'image' => '' ],
* [ 'label' => '蓝色', 'price_change' => 10, 'image' => 'url' ],
* ]],
* ]
*
* options_key = "红色|M" (pipe-delimited selected labels per group)
* ============================================================ */
public function get_product_options(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_product_options', true);
if (!is_array($raw) || empty($raw)) {
return [];
}
return array_values(array_filter(array_map([$this, 'sanitize_option_group'], $raw)));
}
public function save_product_options(int $product_id, array $groups): void {
$sanitized = array_values(array_filter(array_map([$this, 'sanitize_option_group'], $groups)));
update_post_meta($product_id, '_aksrt_product_options', $sanitized);
update_post_meta($product_id, '_aksrt_has_options', count($sanitized) > 0 ? '1' : '0');
}
public function has_options(int $product_id): bool {
return get_post_meta($product_id, '_aksrt_has_options', true) === '1';
}
private function sanitize_option_group($group): array {
if (!is_array($group)) return [];
$name = sanitize_text_field((string) ($group['name'] ?? ''));
if ($name === '') return [];
$view_mode_raw = $group['view_mode'] ?? '';
$view_mode = in_array($view_mode_raw, ['list', 'img'], true) ? $view_mode_raw : 'list';
$values = [];
foreach ((array) ($group['values'] ?? []) as $v) {
$label = sanitize_text_field((string) ($v['label'] ?? ''));
if ($label === '') continue;
$values[] = [
'label' => $label,
'price_change' => round((float) ($v['price_change'] ?? 0), 2),
'image' => esc_url_raw((string) ($v['image'] ?? '')),
];
}
if (empty($values)) return [];
return ['name' => $name, 'view_mode' => $view_mode, 'values' => $values];
}
public function build_options_key(array $selections): string {
return implode('|', array_map('sanitize_text_field', $selections));
}
public function resolve_option_price(int $product_id, string $options_key): float {
$base = (float) get_post_meta($product_id, '_aksrt_price', true);
$start = (float) get_post_meta($product_id, '_aksrt_start_price', true);
if ($start > 0) {
$base = $start;
}
if ($options_key === '' || $options_key === '_default') {
return $base;
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
foreach ($groups as $i => $group) {
$selected_label = $selections[$i] ?? '';
foreach ($group['values'] as $val) {
if ($val['label'] === $selected_label) {
$base += $val['price_change'];
break;
}
}
}
return max(0, round($base, 2));
}
public function resolve_option_labels(int $product_id, string $options_key): string {
if ($options_key === '' || $options_key === '_default') {
return '';
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
$parts = [];
foreach ($groups as $i => $group) {
$sel = $selections[$i] ?? '';
if ($sel !== '') {
$parts[] = $group['name'] . ':' . $sel;
}
}
return implode(', ', $parts);
}
public function resolve_option_stock(int $product_id, string $options_key): int {
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
$stock_matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
if (is_array($stock_matrix) && isset($stock_matrix[$options_key])) {
return (int) $stock_matrix[$options_key];
}
return 0;
}
return (int) get_post_meta($product_id, '_aksrt_stock', true);
}
}
+352
View File
@@ -0,0 +1,352 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part3 {
public function deduct_option_stock(int $product_id, string $options_key, int $qty): void {
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
$matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
if (is_array($matrix) && isset($matrix[$options_key])) {
$matrix[$options_key] = max(0, (int) $matrix[$options_key] - $qty);
update_post_meta($product_id, '_aksrt_stock_opts', $matrix);
}
return;
}
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
if ($stock > 0) {
update_post_meta($product_id, '_aksrt_stock', max(0, $stock - $qty));
}
}
public function get_option_image(int $product_id, string $options_key): string {
if ($options_key === '' || $options_key === '_default') {
return '';
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
foreach ($groups as $i => $group) {
$sel = $selections[$i] ?? '';
foreach ($group['values'] as $val) {
if ($val['label'] === $sel && !empty($val['image'])) {
return $val['image'];
}
}
}
return '';
}
/* ============================================================
* Auto-delivery (§6.2 自动发货)
*
* Storage: post_meta '_aksrt_auto_delivery' = [
* 'type' => 'fixed'|'invit_code'|'card_pass'|'opts',
* 'content' => '固定内容文本',
* 'opts' => [ options_key => '该选项的发货内容' ],
* ]
* ============================================================ */
public function get_auto_delivery_config(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_auto_delivery', true);
if (!is_array($raw)) {
return ['type' => '', 'content' => '', 'opts' => []];
}
return [
'type' => sanitize_key((string) ($raw['type'] ?? '')),
'content' => (string) ($raw['content'] ?? ''),
'opts' => is_array($raw['opts'] ?? null) ? $raw['opts'] : [],
];
}
public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number): 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\Core\DBMigrator::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\Credits\Invite')) {
return __('邀请系统不可用', 'aksrt');
}
$invite = \AKSRT\Credits\Invite::get_instance();
$codes = [];
for ($i = 0; $i < $qty; $i++) {
$code = $invite->generate_code($user_id);
if ($code) {
$codes[] = $code;
}
}
return implode("\n", $codes);
}
/* ============================================================
* Order Items (§6.5 多商品订单子表)
* ============================================================ */
private function order_item_table(): string {
return \AKSRT\Core\DBMigrator::table('order_item');
}
public function save_order_items(string $order_number, array $items): void {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return;
foreach ($items as $item) {
$wpdb->insert($table, [
'order_number' => $order_number,
'product_id' => (int) ($item['product_id'] ?? 0),
'product_title' => sanitize_text_field((string) ($item['title'] ?? '')),
'options_key' => sanitize_text_field((string) ($item['options_key'] ?? '')),
'options_label' => sanitize_text_field((string) ($item['options_label'] ?? '')),
'quantity' => max(1, (int) ($item['quantity'] ?? 1)),
'unit_price' => (float) ($item['unit_price'] ?? $item['price'] ?? 0),
'total_price' => (float) ($item['total_price'] ?? $item['line_total'] ?? 0),
'discount_amount' => (float) ($item['discount_amount'] ?? 0),
'shipping_fee' => (float) ($item['shipping_fee'] ?? 0),
'pay_price' => (float) ($item['pay_price'] ?? $item['line_total'] ?? 0),
'shipping_type' => sanitize_key((string) ($item['shipping_type'] ?? 'express')),
'auto_delivery_content'=> ($item['auto_delivery_content'] ?? null),
'discount_hit' => isset($item['applied_discounts']) ? wp_json_encode($item['applied_discounts']) : null,
'gift_data' => isset($item['gifts']) ? wp_json_encode($item['gifts']) : null,
'image' => esc_url_raw((string) ($item['image'] ?? '')),
]);
}
}
public function get_order_items(string $order_number): array {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return [];
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table} WHERE order_number = %s ORDER BY id ASC",
$order_number
), ARRAY_A);
return array_map(static function ($row) {
return [
'id' => (int) $row['id'],
'product_id' => (int) $row['product_id'],
'product_title' => (string) $row['product_title'],
'options_key' => (string) $row['options_key'],
'options_label' => (string) $row['options_label'],
'quantity' => (int) $row['quantity'],
'unit_price' => (float) $row['unit_price'],
'total_price' => (float) $row['total_price'],
'discount_amount' => (float) $row['discount_amount'],
'shipping_fee' => (float) $row['shipping_fee'],
'pay_price' => (float) $row['pay_price'],
'shipping_type' => (string) $row['shipping_type'],
'auto_delivery_content' => $row['auto_delivery_content'],
'discount_hit' => json_decode($row['discount_hit'] ?? '', true) ?: [],
'gift_data' => json_decode($row['gift_data'] ?? '', true) ?: [],
'image' => (string) $row['image'],
];
}, (array) $rows);
}
/* ============================================================
* Enhanced Fulfillment (§6.5 履约链路)
* ============================================================ */
public function handle_order_paid_enhanced(string $order_number, array $order): void {
$items = $this->get_order_items($order_number);
if (empty($items)) {
$this->handle_order_paid($order_number, $order);
return;
}
$user_id = (int) ($order['user_id'] ?? 0);
$auto_delivery_parts = [];
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\Credits\Invite')) {
$referrer_id = \AKSRT\Credits\Invite::get_instance()->get_referrer($user_id);
}
if ($referrer_id <= 0) return;
$rebate_table = \AKSRT\Core\DBMigrator::table('rebate_log');
if ($rebate_table === '') return;
global $wpdb;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$pay_price = (float) $item['pay_price'];
if ($pid <= 0 || $pay_price <= 0) continue;
$rebate_rate = (float) get_post_meta($pid, '_aksrt_rebate_rate', true);
if ($rebate_rate <= 0 && class_exists('TermMeta')) {
$resolved = TermMeta::resolve_inheritance($pid, 'rebate');
$rebate_rate = (float) ($resolved ?: 0);
}
if ($rebate_rate <= 0) continue;
$amount = round($pay_price * $rebate_rate / 100, 2);
if ($amount <= 0) continue;
$wpdb->insert($rebate_table, [
'user_id' => $referrer_id,
'order_number' => $order_number,
'source_user_id' => $user_id,
'amount' => $amount,
'type' => 'order',
'status' => 'pending',
'created_at' => current_time('mysql'),
]);
}
}
private function process_gifts(string $order_number, array $order, array $items): void {
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) return;
foreach ($items as $item) {
$gifts = $item['gift_data'] ?? [];
if (empty($gifts)) continue;
foreach ($gifts as $gift) {
$gift_type = (string) ($gift['type'] ?? '');
$gift_value = $gift['value'] ?? 0;
if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) {
\AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number));
} elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) {
$current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$base = max($current, time());
update_user_meta($user_id, 'aksrt_vip_expires', $base + ((int) $gift_value * DAY_IN_SECONDS));
// 赠送 VIP 天数时确保至少 VIP1;不降级已有 VIP2。
$level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
if ($level < 1) {
update_user_meta($user_id, 'aksrt_vip_level', 1);
}
}
}
}
}
}
+341
View File
@@ -0,0 +1,341 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductMetaBox {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 10, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_commerce',
__('商品商业设置', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'high'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_product_meta_save', 'aksrt_product_meta_nonce');
$price = (float) get_post_meta($post->ID, '_aksrt_price', true);
$stock = (int) get_post_meta($post->ID, '_aksrt_stock', true);
$image = (string) get_post_meta($post->ID, '_aksrt_product_image', true);
$gallery = get_post_meta($post->ID, '_aksrt_product_gallery', true);
$gallery = is_array($gallery) ? $gallery : [];
$shipping_mode = (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true);
$shipping_fee = (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true);
$shipping_fee_type = (string) (get_post_meta($post->ID, '_aksrt_shipping_fee_type', true) ?: 'fixed');
$shipping_fee_free = (float) get_post_meta($post->ID, '_aksrt_shipping_fee_free_amount', true);
$shipping_delivery = (string) get_post_meta($post->ID, '_aksrt_shipping_delivery_desc', true);
$shipping_tip = (string) get_post_meta($post->ID, '_aksrt_shipping_tip', true);
$after_sale_policy = (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true);
$support_refund = get_post_meta($post->ID, '_aksrt_support_refund', true);
$support_refund = $support_refund === '' ? '1' : (string) $support_refund;
$service = (string) get_post_meta($post->ID, '_aksrt_service', true);
$service_tip = (string) get_post_meta($post->ID, '_aksrt_service_tip', true);
$limit_buy = get_post_meta($post->ID, '_aksrt_limit_buy', true);
$limit_buy = $limit_buy === '' ? -1 : (int) $limit_buy;
$limit_buy_note = (string) get_post_meta($post->ID, '_aksrt_limit_buy_note', true);
$stock_type = (string) (get_post_meta($post->ID, '_aksrt_stock_type', true) ?: 'all');
$pay_modo = (string) (get_post_meta($post->ID, '_aksrt_pay_modo', true) ?: '0');
$start_price = (float) get_post_meta($post->ID, '_aksrt_start_price', true);
$product_rebate = (string) get_post_meta($post->ID, '_aksrt_product_rebate', true);
$product_params = (string) get_post_meta($post->ID, '_aksrt_product_params', true);
$user_required = (string) get_post_meta($post->ID, '_aksrt_user_required', true);
$delivery_note = (string) get_post_meta($post->ID, '_aksrt_delivery_note', true);
$marketing = (string) get_post_meta($post->ID, '_aksrt_marketing_headline', true);
$shipping_modes = [
'' => __('默认(继承设置)', 'aksrt'),
'express' => __('快递物流', 'aksrt'),
'auto' => __('自动发货', 'aksrt'),
'manual' => __('手动发货', 'aksrt'),
'virtual' => __('虚拟商品', 'aksrt'),
];
$stock_types = [
'all' => __('统一库存', 'aksrt'),
'opts' => __('按选项/变体', 'aksrt'),
];
$fee_types = [
'free' => __('包邮', 'aksrt'),
'fixed' => __('固定运费', 'aksrt'),
'amount_free' => __('满额包邮', 'aksrt'),
];
?>
<style>
.aksrt-pmb-product .row { display:grid; grid-template-columns: 140px 1fr; gap: 10px 14px; margin: 0 0 12px; align-items:start; }
.aksrt-pmb-product .row label { font-weight:600; padding-top:6px; }
.aksrt-pmb-product input[type="text"],
.aksrt-pmb-product input[type="number"],
.aksrt-pmb-product input[type="url"],
.aksrt-pmb-product select,
.aksrt-pmb-product textarea { width:100%; max-width:560px; }
.aksrt-pmb-product .hint { color:#646970; font-size:12px; margin:4px 0 0; }
.aksrt-pmb-product .grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; max-width:720px; }
.aksrt-pmb-product h4 { margin:18px 0 10px; padding-top:12px; border-top:1px solid #dcdcde; }
</style>
<div class="aksrt-pmb-product">
<h4><?php esc_html_e('价格与库存', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_price"><?php esc_html_e('价格(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_price" name="aksrt_price" min="0" step="0.01" value="<?php echo esc_attr((string) $price); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock"><?php esc_html_e('库存', 'aksrt'); ?></label>
<input type="number" id="aksrt_stock" name="aksrt_stock" min="-1" step="1" value="<?php echo esc_attr((string) $stock); ?>">
<p class="hint"><?php esc_html_e('-1 表示无限库存。', 'aksrt'); ?></p>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_pay_modo"><?php esc_html_e('支付方式', 'aksrt'); ?></label>
<select id="aksrt_pay_modo" name="aksrt_pay_modo">
<option value="0" <?php selected($pay_modo, '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected($pay_modo, 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_start_price"><?php esc_html_e('划线价 / 起售价', 'aksrt'); ?></label>
<input type="number" id="aksrt_start_price" name="aksrt_start_price" min="0" step="0.01" value="<?php echo esc_attr((string) $start_price); ?>">
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock_type"><?php esc_html_e('库存类型', 'aksrt'); ?></label>
<select id="aksrt_stock_type" name="aksrt_stock_type">
<?php foreach ($stock_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($stock_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_limit_buy"><?php esc_html_e('限购数量', 'aksrt'); ?></label>
<input type="number" id="aksrt_limit_buy" name="aksrt_limit_buy" min="-1" step="1" value="<?php echo esc_attr((string) $limit_buy); ?>">
<p class="hint"><?php esc_html_e('-1 不限购。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_limit_buy_note"><?php esc_html_e('限购说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_limit_buy_note" name="aksrt_limit_buy_note" value="<?php echo esc_attr($limit_buy_note); ?>">
</div>
<div class="row">
<label for="aksrt_product_rebate"><?php esc_html_e('推广返佣', 'aksrt'); ?></label>
<div>
<input type="text" id="aksrt_product_rebate" name="aksrt_product_rebate" value="<?php echo esc_attr($product_rebate); ?>" placeholder="<?php esc_attr_e('如 10 或 10%', 'aksrt'); ?>">
<p class="hint"><?php esc_html_e('可填固定金额或百分比,具体解析由返佣服务处理。', 'aksrt'); ?></p>
</div>
</div>
<h4><?php esc_html_e('素材', 'aksrt'); ?></h4>
<div class="row">
<label for="aksrt_product_image"><?php esc_html_e('主图 URL', 'aksrt'); ?></label>
<div>
<input type="url" id="aksrt_product_image" name="aksrt_product_image" value="<?php echo esc_attr($image); ?>" placeholder="https://">
<p class="hint"><?php esc_html_e('也可使用特色图片;此处优先作为商品主图。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_product_gallery"><?php esc_html_e('图集 URL', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_gallery" name="aksrt_product_gallery" rows="4" placeholder="<?php esc_attr_e('每行一个图片 URL', 'aksrt'); ?>"><?php echo esc_textarea(implode("\n", $gallery)); ?></textarea>
</div>
</div>
<div class="row">
<label for="aksrt_product_params"><?php esc_html_e('商品参数', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_params" name="aksrt_product_params" rows="4" placeholder="<?php esc_attr_e('每行一条参数,如 品牌:AKSRT', 'aksrt'); ?>"><?php echo esc_textarea($product_params); ?></textarea>
</div>
</div>
<h4><?php esc_html_e('发货与售后', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_mode"><?php esc_html_e('发货方式', 'aksrt'); ?></label>
<select id="aksrt_shipping_mode" name="aksrt_shipping_mode">
<?php foreach ($shipping_modes as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_mode, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_type"><?php esc_html_e('运费类型', 'aksrt'); ?></label>
<select id="aksrt_shipping_fee_type" name="aksrt_shipping_fee_type">
<?php foreach ($fee_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_fee_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee"><?php esc_html_e('运费金额(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee" name="aksrt_shipping_fee" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_free_amount"><?php esc_html_e('满额包邮门槛', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee_free_amount" name="aksrt_shipping_fee_free_amount" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee_free); ?>">
</div>
</div>
<div class="row">
<label for="aksrt_shipping_delivery_desc"><?php esc_html_e('发货时效说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_delivery_desc" name="aksrt_shipping_delivery_desc" value="<?php echo esc_attr($shipping_delivery); ?>" placeholder="<?php esc_attr_e('如 付款后 24 小时内发货', 'aksrt'); ?>">
</div>
<div class="row">
<label for="aksrt_shipping_tip"><?php esc_html_e('物流提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_tip" name="aksrt_shipping_tip" value="<?php echo esc_attr($shipping_tip); ?>">
</div>
<div class="row">
<label for="aksrt_after_sale_policy"><?php esc_html_e('售后说明', 'aksrt'); ?></label>
<textarea id="aksrt_after_sale_policy" name="aksrt_after_sale_policy" rows="3"><?php echo esc_textarea($after_sale_policy); ?></textarea>
</div>
<div class="row">
<label for="aksrt_support_refund"><?php esc_html_e('支持退款', 'aksrt'); ?></label>
<div>
<label>
<input type="checkbox" id="aksrt_support_refund" name="aksrt_support_refund" value="1" <?php checked($support_refund, '1'); ?>>
<?php esc_html_e('允许申请退款/售后', 'aksrt'); ?>
</label>
</div>
</div>
<div class="row">
<label for="aksrt_service"><?php esc_html_e('服务承诺', 'aksrt'); ?></label>
<textarea id="aksrt_service" name="aksrt_service" rows="3" placeholder="<?php esc_attr_e('每行一条服务承诺', 'aksrt'); ?>"><?php echo esc_textarea($service); ?></textarea>
</div>
<div class="row">
<label for="aksrt_service_tip"><?php esc_html_e('服务提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_service_tip" name="aksrt_service_tip" value="<?php echo esc_attr($service_tip); ?>">
</div>
<div class="row">
<label for="aksrt_user_required"><?php esc_html_e('下单需填信息', 'aksrt'); ?></label>
<textarea id="aksrt_user_required" name="aksrt_user_required" rows="3" placeholder="<?php esc_attr_e('每行一项,如 QQ / 游戏角色名', 'aksrt'); ?>"><?php echo esc_textarea($user_required); ?></textarea>
</div>
<div class="row">
<label for="aksrt_delivery_note"><?php esc_html_e('发货备注', 'aksrt'); ?></label>
<input type="text" id="aksrt_delivery_note" name="aksrt_delivery_note" value="<?php echo esc_attr($delivery_note); ?>">
</div>
<div class="row">
<label for="aksrt_marketing_headline"><?php esc_html_e('营销标题', 'aksrt'); ?></label>
<input type="text" id="aksrt_marketing_headline" name="aksrt_marketing_headline" value="<?php echo esc_attr($marketing); ?>">
</div>
<p class="hint">
<?php esc_html_e('SKU 变体请使用下方「商品变体」面板;列表列已显示价格/库存/销量。', 'aksrt'); ?>
</p>
</div>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_meta_nonce'])), 'aksrt_product_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product') {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$price = isset($_POST['aksrt_price']) ? max(0, (float) wp_unslash($_POST['aksrt_price'])) : 0;
$stock = isset($_POST['aksrt_stock']) ? (int) wp_unslash($_POST['aksrt_stock']) : 0;
if ($stock < -1) {
$stock = -1;
}
$image = isset($_POST['aksrt_product_image']) ? esc_url_raw((string) wp_unslash($_POST['aksrt_product_image'])) : '';
$gallery_raw = isset($_POST['aksrt_product_gallery']) ? (string) wp_unslash($_POST['aksrt_product_gallery']) : '';
$gallery = array_values(array_filter(array_map(
static function ($line) {
return esc_url_raw(trim((string) $line));
},
preg_split('/\r\n|\r|\n/', $gallery_raw) ?: []
)));
$shipping_mode = isset($_POST['aksrt_shipping_mode']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_mode'])) : '';
$shipping_fee = isset($_POST['aksrt_shipping_fee']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee'])) : 0;
$shipping_fee_type = isset($_POST['aksrt_shipping_fee_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_fee_type'])) : 'fixed';
if (!in_array($shipping_fee_type, ['free', 'fixed', 'amount_free'], true)) {
$shipping_fee_type = 'fixed';
}
$shipping_fee_free = isset($_POST['aksrt_shipping_fee_free_amount']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee_free_amount'])) : 0;
$shipping_delivery = isset($_POST['aksrt_shipping_delivery_desc']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_delivery_desc'])) : '';
$shipping_tip = isset($_POST['aksrt_shipping_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_tip'])) : '';
$policy = isset($_POST['aksrt_after_sale_policy']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_after_sale_policy'])) : '';
$support_refund = !empty($_POST['aksrt_support_refund']) ? '1' : '0';
$service = isset($_POST['aksrt_service']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_service'])) : '';
$service_tip = isset($_POST['aksrt_service_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_service_tip'])) : '';
$limit_buy = isset($_POST['aksrt_limit_buy']) ? max(-1, min(9999, (int) wp_unslash($_POST['aksrt_limit_buy']))) : -1;
$limit_buy_note = isset($_POST['aksrt_limit_buy_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_limit_buy_note'])) : '';
$stock_type = isset($_POST['aksrt_stock_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_stock_type'])) : 'all';
if (!in_array($stock_type, ['all', 'opts'], true)) {
$stock_type = 'all';
}
$pay_modo = isset($_POST['aksrt_pay_modo']) ? sanitize_key((string) wp_unslash($_POST['aksrt_pay_modo'])) : '0';
if (!in_array($pay_modo, ['0', 'points'], true)) {
$pay_modo = '0';
}
$start_price = isset($_POST['aksrt_start_price']) ? max(0, (float) wp_unslash($_POST['aksrt_start_price'])) : 0;
$product_rebate = isset($_POST['aksrt_product_rebate']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_product_rebate'])) : '';
$product_params = isset($_POST['aksrt_product_params']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_product_params'])) : '';
$user_required = isset($_POST['aksrt_user_required']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_user_required'])) : '';
$delivery_note = isset($_POST['aksrt_delivery_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_delivery_note'])) : '';
$marketing = isset($_POST['aksrt_marketing_headline']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_marketing_headline'])) : '';
update_post_meta($post_id, '_aksrt_price', $price);
update_post_meta($post_id, '_aksrt_stock', $stock);
update_post_meta($post_id, '_aksrt_product_image', $image);
update_post_meta($post_id, '_aksrt_product_gallery', $gallery);
update_post_meta($post_id, '_aksrt_shipping_mode', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_type', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_fee', $shipping_fee);
update_post_meta($post_id, '_aksrt_shipping_fee_type', $shipping_fee_type);
update_post_meta($post_id, '_aksrt_shipping_fee_free_amount', $shipping_fee_free);
update_post_meta($post_id, '_aksrt_shipping_delivery_desc', $shipping_delivery);
update_post_meta($post_id, '_aksrt_shipping_tip', $shipping_tip);
update_post_meta($post_id, '_aksrt_after_sale_policy', $policy);
update_post_meta($post_id, '_aksrt_support_refund', $support_refund);
update_post_meta($post_id, '_aksrt_service', $service);
update_post_meta($post_id, '_aksrt_service_tip', $service_tip);
update_post_meta($post_id, '_aksrt_limit_buy', $limit_buy);
update_post_meta($post_id, '_aksrt_limit_buy_note', $limit_buy_note);
update_post_meta($post_id, '_aksrt_stock_type', $stock_type);
update_post_meta($post_id, '_aksrt_pay_modo', $pay_modo);
update_post_meta($post_id, '_aksrt_start_price', $start_price);
update_post_meta($post_id, '_aksrt_product_rebate', $product_rebate);
update_post_meta($post_id, '_aksrt_product_params', $product_params);
update_post_meta($post_id, '_aksrt_user_required', $user_required);
update_post_meta($post_id, '_aksrt_delivery_note', $delivery_note);
update_post_meta($post_id, '_aksrt_marketing_headline', $marketing);
// Keep sales counter present for list columns.
if (get_post_meta($post_id, '_aksrt_sales', true) === '') {
update_post_meta($post_id, '_aksrt_sales', 0);
}
}
}
+309
View File
@@ -0,0 +1,309 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductReview {
private static $instance = null;
const COMMENT_TYPE = 'aksrt_product_review';
const RATING_LEVELS = [
5 => '超赞',
4 => '很棒',
3 => '不错',
2 => '一般',
1 => '较差',
0 => '很差',
];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('aksrt_daily_cron', [$this, 'run_auto_positive_cron']);
}
public function submit_review(int $user_id, string $order_number, int $product_id, array $data): array {
if ($user_id <= 0 || $order_number === '' || $product_id <= 0) {
return ['success' => false, 'message' => __('参数错误', 'aksrt')];
}
if (!$this->can_review($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')];
}
if ($this->has_reviewed($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('已评价过该商品', 'aksrt')];
}
$rating_product = max(0, min(5, (int) ($data['rating_product'] ?? 5)));
$rating_service = max(0, min(5, (int) ($data['rating_service'] ?? 5)));
$rating_shipping = max(0, min(5, (int) ($data['rating_shipping'] ?? 5)));
$average = round(($rating_product + $rating_service + $rating_shipping) / 3, 1);
$content = sanitize_textarea_field((string) ($data['content'] ?? ''));
$images = [];
$max_images = $this->get_max_images();
foreach ((array) ($data['images'] ?? []) as $img) {
if (count($images) >= $max_images) break;
$url = esc_url_raw((string) $img);
if ($url !== '') {
$images[] = $url;
}
}
$comment_id = wp_insert_comment([
'comment_post_ID' => $product_id,
'user_id' => $user_id,
'comment_content' => $content ?: __('好评!', 'aksrt'),
'comment_type' => self::COMMENT_TYPE,
'comment_approved' => 1,
]);
if (!$comment_id) {
return ['success' => false, 'message' => __('评价提交失败', 'aksrt')];
}
update_comment_meta($comment_id, '_aksrt_order_number', $order_number);
update_comment_meta($comment_id, '_aksrt_rating_product', $rating_product);
update_comment_meta($comment_id, '_aksrt_rating_service', $rating_service);
update_comment_meta($comment_id, '_aksrt_rating_shipping', $rating_shipping);
update_comment_meta($comment_id, '_aksrt_rating_average', $average);
if (!empty($images)) {
update_comment_meta($comment_id, '_aksrt_review_images', $images);
}
$this->refresh_product_scores($product_id);
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
$status = (string) ($order['status'] ?? '');
if ($status === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('用户已评价', 'aksrt')]
);
}
}
return ['success' => true, 'comment_id' => $comment_id];
}
public function can_review(int $user_id, string $order_number, int $product_id): bool {
if (!class_exists('\AKSRT\Payment\OrderRepository')) return false;
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) return false;
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [
\AKSRT\Payment\Order::STATUS_COMPLETED,
\AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
\AKSRT\Payment\Order::STATUS_DELIVERED,
], true)) {
return false;
}
if (class_exists('\AKSRT\Payment\AfterSale')) {
$tickets = \AKSRT\Payment\AfterSale::get_instance()->get_list([
'order_number' => $order_number,
'limit' => 1,
]);
foreach (($tickets['items'] ?? []) as $ticket) {
if (in_array(($ticket['status'] ?? ''), ['pending', 'processing'], true)) {
return false;
}
}
}
return true;
}
public function has_reviewed(int $user_id, string $order_number, int $product_id): bool {
$comments = get_comments([
'post_id' => $product_id,
'user_id' => $user_id,
'type' => self::COMMENT_TYPE,
'number' => 1,
'meta_query' => [
['key' => '_aksrt_order_number', 'value' => $order_number],
],
]);
return !empty($comments);
}
public function get_reviews(int $product_id, array $args = []): array {
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 10)));
$filter = sanitize_key((string) ($args['filter'] ?? ''));
$query_args = [
'post_id' => $product_id,
'type' => self::COMMENT_TYPE,
'status' => 'approve',
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
'orderby' => 'comment_date',
'order' => 'DESC',
];
if ($filter === 'good') {
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 4, 'compare' => '>=', 'type' => 'NUMERIC']];
} elseif ($filter === 'bad') {
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 3, 'compare' => '<', 'type' => 'NUMERIC']];
} elseif ($filter === 'has_image') {
$query_args['meta_query'] = [['key' => '_aksrt_review_images', 'compare' => 'EXISTS']];
}
$comments = get_comments($query_args);
$total = (int) get_comments(array_merge($query_args, ['count' => true, 'number' => 0, 'offset' => 0]));
$items = [];
foreach ($comments as $comment) {
$cid = (int) $comment->comment_ID;
$items[] = [
'id' => $cid,
'user_id' => (int) $comment->user_id,
'user_name' => (string) $comment->comment_author,
'user_avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url((int) $comment->user_id) : get_avatar_url((int) $comment->user_id),
'content' => (string) $comment->comment_content,
'date' => (string) $comment->comment_date,
'rating_product' => (int) get_comment_meta($cid, '_aksrt_rating_product', true),
'rating_service' => (int) get_comment_meta($cid, '_aksrt_rating_service', true),
'rating_shipping' => (int) get_comment_meta($cid, '_aksrt_rating_shipping', true),
'rating_average' => (float) get_comment_meta($cid, '_aksrt_rating_average', true),
'images' => (array) (get_comment_meta($cid, '_aksrt_review_images', true) ?: []),
'order_number' => (string) get_comment_meta($cid, '_aksrt_order_number', true),
];
}
return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
}
public function get_product_scores(int $product_id): array {
$cached = get_post_meta($product_id, '_aksrt_review_scores', true);
if (is_array($cached)) {
return $cached;
}
return $this->refresh_product_scores($product_id);
}
public function refresh_product_scores(int $product_id): array {
global $wpdb;
$results = $wpdb->get_row($wpdb->prepare(
"SELECT COUNT(*) as total,
AVG(CAST(m.meta_value AS DECIMAL(3,1))) as average
FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
$product_id, self::COMMENT_TYPE
), ARRAY_A);
$total = (int) ($results['total'] ?? 0);
$average = $total > 0 ? round((float) ($results['average'] ?? 0), 1) : 0;
$good_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
AND CAST(m.meta_value AS DECIMAL(3,1)) >= 4",
$product_id, self::COMMENT_TYPE
));
$bad_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
AND CAST(m.meta_value AS DECIMAL(3,1)) < 3",
$product_id, self::COMMENT_TYPE
));
$has_image = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_review_images'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
$product_id, self::COMMENT_TYPE
));
$scores = [
'total' => $total,
'average' => $average,
'good' => $good_count,
'bad' => $bad_count,
'has_image' => $has_image,
'level' => self::RATING_LEVELS[min(5, max(0, (int) round($average)))] ?? '',
];
update_post_meta($product_id, '_aksrt_review_scores', $scores);
return $scores;
}
public function run_auto_positive_cron(): void {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15));
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin([
'status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
'limit' => 200,
]);
foreach ($orders as $order) {
$completed_at = (string) ($order['completed_at'] ?? $order['delivered_at'] ?? '');
if ($completed_at === '' || $completed_at > $cutoff) {
continue;
}
$user_id = (int) ($order['user_id'] ?? 0);
$order_number = (string) ($order['order_number'] ?? '');
if ($user_id <= 0 || $order_number === '') continue;
$items = class_exists('Shop')
? Shop::get_instance()->get_order_items($order_number)
: [];
$product_ids = array_column($items, 'product_id');
if (empty($product_ids) && !empty($order['product_id'])) {
$product_ids = [(int) $order['product_id']];
}
foreach ($product_ids as $pid) {
if (!$this->has_reviewed($user_id, $order_number, (int) $pid)) {
$this->submit_review($user_id, $order_number, (int) $pid, [
'rating_product' => 5,
'rating_service' => 5,
'rating_shipping' => 5,
'content' => __('系统默认好评', 'aksrt'),
]);
}
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day),
]);
}
}
private function get_max_images(): int {
if (!class_exists('\AKSRT\Settings\Settings')) return 9;
$shop = \AKSRT\Settings\Settings::get_shop_settings();
return max(1, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9)));
}
}
+364
View File
@@ -0,0 +1,364 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
/* ============================================================
* Product meta helpers
* ============================================================ */
public function get_term_options(string $taxonomy): array {
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
return array_map(static function ($term) {
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
];
}, $terms);
}
public function get_product_gallery(int $product_id): array {
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
if (!is_array($gallery)) {
return [];
}
return array_values(array_filter(array_map('esc_url_raw', $gallery)));
}
public function get_product_shipping_profile(int $product_id): array {
return [
'mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
'fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
'delivery_note' => (string) get_post_meta($product_id, '_aksrt_delivery_note', true),
];
}
public function get_product_after_sale_profile(int $product_id): array {
return [
'policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
];
}
public function get_product_marketing_profile(int $product_id): array {
return [
'headline' => (string) get_post_meta($product_id, '_aksrt_marketing_headline', true),
'shipping_tip' => (string) get_post_meta($product_id, '_aksrt_shipping_tip', true),
'service_tip' => (string) get_post_meta($product_id, '_aksrt_service_tip', true),
];
}
public function get_payment_method_options(): array {
$methods = [];
if (class_exists('\AKSRT\Payment\Gateway')) {
foreach (\AKSRT\Payment\Gateway::get_instance()->get_all() as $id => $gateway) {
if (!method_exists($gateway, 'is_configured') || !$gateway->is_configured()) {
continue;
}
$methods[$id] = [
'value' => $id,
'label' => method_exists($gateway, 'get_name') ? (string) $gateway->get_name() : $id,
'description' => $this->gateway_method_description((string) $id),
];
}
}
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled()) {
$methods['points'] = [
'value' => 'points',
'label' => __('积分兑换', 'aksrt'),
'description' => __('使用积分购买积分商品。', 'aksrt'),
];
}
$manual = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$gw = \AKSRT\Settings\Settings::get_payment_gateway_settings();
$manual = !empty($gw['manual_review']) || ($gw['mode'] ?? '') === 'manual_review';
}
if ($manual) {
$methods['manual_review'] = [
'value' => 'manual_review',
'label' => __('人工确认', 'aksrt'),
'description' => __('保留待支付订单,后续由管理员确认。', 'aksrt'),
];
}
if (empty($methods)) {
$methods['manual_review'] = [
'value' => 'manual_review',
'label' => __('人工确认', 'aksrt'),
'description' => __('保留待支付订单,后续由管理员确认。', 'aksrt'),
];
}
return $methods;
}
private function gateway_method_description(string $id): string {
switch ($id) {
case 'balance':
return __('即时完成支付并写回订单状态。', 'aksrt');
case 'alipay':
return __('跳转支付宝完成支付。', 'aksrt');
case 'wechatpay':
return __('微信扫码或跳转完成支付。', 'aksrt');
case 'paypal':
return __('跳转 PayPal 完成支付。', 'aksrt');
case 'stripe':
return __('跳转 Stripe Checkout 完成支付。', 'aksrt');
default:
return '';
}
}
public function get_shipping_method_options(): array {
return [
'express' => [
'value' => 'express',
'label' => __('快递配送', 'aksrt'),
'description' => __('标准快递,支持物流跟踪。', 'aksrt'),
'fee' => 12.0,
],
'ems' => [
'value' => 'ems',
'label' => __('EMS', 'aksrt'),
'description' => __('偏远地区可选。', 'aksrt'),
'fee' => 18.0,
],
'download' => [
'value' => 'download',
'label' => __('自动发货', 'aksrt'),
'description' => __('适用于数字商品。', 'aksrt'),
'fee' => 0.0,
],
];
}
public function get_limit_buy_config(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_limit_buy_config', true);
if (!is_array($raw)) {
$legacy = (int) get_post_meta($product_id, '_aksrt_limit_buy', true);
return ['default' => $legacy > 0 ? $legacy : -1, 'desc' => ''];
}
return [
'default' => (int) ($raw['default'] ?? -1),
'vip' => (int) ($raw['vip'] ?? -1),
'vip2' => (int) ($raw['vip2'] ?? -1),
'auth' => (int) ($raw['auth'] ?? -1),
'desc' => sanitize_text_field((string) ($raw['desc'] ?? '')),
];
}
public function get_product_services(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_services', true);
if (!is_array($raw)) return [];
return array_values(array_filter(array_map(static function ($s) {
$name = sanitize_text_field((string) ($s['name'] ?? ''));
if ($name === '') return null;
return [
'name' => $name,
'desc' => sanitize_text_field((string) ($s['desc'] ?? '')),
'icon' => esc_url_raw((string) ($s['icon'] ?? '')),
];
}, $raw)));
}
public function get_product_params(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_product_params', true);
if (!is_array($raw)) return [];
return array_values(array_filter(array_map(static function ($p) {
$name = sanitize_text_field((string) ($p['name'] ?? ''));
if ($name === '') return null;
return [
'name' => $name,
'value' => sanitize_text_field((string) ($p['value'] ?? '')),
];
}, $raw)));
}
/**
* Resolve product main image with 3-tier fallback:
* 1. _aksrt_product_image (custom main image)
* 2. WordPress featured image (post thumbnail)
* 3. First gallery image from _aksrt_product_gallery
*/
public function resolve_main_image(int $product_id): string {
$custom = (string) get_post_meta($product_id, '_aksrt_product_image', true);
if ($custom !== '') {
return esc_url_raw($custom);
}
$thumb = get_the_post_thumbnail_url($product_id, 'large');
if ($thumb) {
return (string) $thumb;
}
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
if (is_array($gallery) && !empty($gallery)) {
$first = reset($gallery);
if ($first) {
return esc_url_raw((string) $first);
}
}
return '';
}
public function build_product_card_view(int $product_id): array {
$post = get_post($product_id);
if (!$post || $post->post_type !== 'aksrt_product') {
return [];
}
$stock_raw = (int) get_post_meta($product_id, '_aksrt_stock', true);
$is_unlimited = $stock_raw <= -1;
return [
'id' => $product_id,
'title' => (string) get_the_title($product_id),
'excerpt' => (string) get_the_excerpt($product_id),
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
'image' => $this->resolve_main_image($product_id),
'stock' => $stock_raw,
'is_unlimited' => $is_unlimited,
'sales' => (int) get_post_meta($product_id, '_aksrt_sales', true),
'status' => (string) get_post_status($product_id),
'created' => get_the_date('Y-m-d H:i', $product_id),
'permalink' => get_permalink($product_id) ?: '',
];
}
/* ============================================================
* Product Variants / SKU
* ============================================================ */
/**
* Get variants table name.
*/
public function variants_table(): string {
global $wpdb;
return $wpdb->prefix . 'aksrt_product_variant';
}
/**
* Get all variants for a product.
*/
public function get_variants(int $product_id): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->variants_table()} WHERE product_id = %d ORDER BY sort_order ASC, id ASC",
$product_id
), ARRAY_A);
if (!$rows) return [];
return array_map(function ($row) {
$row['id'] = (int) $row['id'];
$row['product_id'] = (int) $row['product_id'];
$row['price'] = (float) $row['price'];
$row['stock'] = (int) $row['stock'];
$row['sort_order'] = (int) $row['sort_order'];
$row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: [];
return $row;
}, $rows);
}
/**
* Get a single variant by ID.
*/
public function get_variant(int $variant_id): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->variants_table()} WHERE id = %d",
$variant_id
), ARRAY_A);
if (!$row) return null;
$row['id'] = (int) $row['id'];
$row['product_id'] = (int) $row['product_id'];
$row['price'] = (float) $row['price'];
$row['stock'] = (int) $row['stock'];
$row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: [];
return $row;
}
/**
* Save variants for a product (replace all).
*/
public function save_variants(int $product_id, array $variants): array {
global $wpdb;
$table = $this->variants_table();
// Delete existing variants
$wpdb->delete($table, ['product_id' => $product_id]);
$saved = [];
foreach ($variants as $index => $v) {
$data = [
'product_id' => $product_id,
'sku' => sanitize_text_field((string) ($v['sku'] ?? '')),
'attributes' => wp_json_encode($v['attributes'] ?? []),
'price' => max(0, (float) ($v['price'] ?? 0)),
'stock' => max(0, (int) ($v['stock'] ?? 0)),
'image' => esc_url_raw((string) ($v['image'] ?? '')),
'sort_order' => $index,
'created_at' => current_time('mysql', true),
];
$wpdb->insert($table, $data);
$saved[] = array_merge($data, ['id' => (int) $wpdb->insert_id]);
}
// Update product meta flag
update_post_meta($product_id, '_aksrt_has_variants', count($saved) > 0 ? '1' : '0');
return $saved;
}
/**
* Delete a single variant.
*/
public function delete_variant(int $variant_id): bool {
global $wpdb;
$variant = $this->get_variant($variant_id);
if (!$variant) return false;
$result = $wpdb->delete($this->variants_table(), ['id' => $variant_id]);
if ($result) {
// Update product meta flag
$remaining = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->variants_table()} WHERE product_id = %d",
$variant['product_id']
));
update_post_meta($variant['product_id'], '_aksrt_has_variants', $remaining > 0 ? '1' : '0');
}
return (bool) $result;
}
/**
* Check if a product has variants.
*/
public function has_variants(int $product_id): bool {
return get_post_meta($product_id, '_aksrt_has_variants', true) === '1';
}
}
+188
View File
@@ -0,0 +1,188 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductVariantsMetaBox {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 30, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_variants',
__('SKU 变体', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'default'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_product_variants_save', 'aksrt_product_variants_nonce');
$variants = [];
if (class_exists('Shop')) {
$variants = Shop::get_instance()->get_variants((int) $post->ID);
}
if (!$variants) {
$variants = [[
'sku' => '',
'attributes' => [],
'price' => 0,
'stock' => 0,
'image' => '',
]];
}
?>
<style>
.aksrt-variants-table { width:100%; border-collapse:collapse; }
.aksrt-variants-table th, .aksrt-variants-table td { border:1px solid #dcdcde; padding:6px; vertical-align:top; }
.aksrt-variants-table input { width:100%; }
.aksrt-variants-table .col-sku { width:14%; }
.aksrt-variants-table .col-attrs { width:28%; }
.aksrt-variants-table .col-price, .aksrt-variants-table .col-stock { width:12%; }
.aksrt-variants-table .col-image { width:28%; }
.aksrt-variants-hint { color:#646970; font-size:12px; margin:8px 0; }
</style>
<p class="aksrt-variants-hint">
<?php esc_html_e('属性格式:名称:值,多项用分号分隔,例如 颜色:红;尺寸:L。保存时会整体覆盖该商品变体列表。', 'aksrt'); ?>
</p>
<table class="aksrt-variants-table" id="aksrt-variants-table">
<thead>
<tr>
<th class="col-sku"><?php esc_html_e('SKU', 'aksrt'); ?></th>
<th class="col-attrs"><?php esc_html_e('属性', 'aksrt'); ?></th>
<th class="col-price"><?php esc_html_e('价格', 'aksrt'); ?></th>
<th class="col-stock"><?php esc_html_e('库存', 'aksrt'); ?></th>
<th class="col-image"><?php esc_html_e('图片 URL', 'aksrt'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $i => $row) :
$attrs = $row['attributes'] ?? [];
$attrs_text = '';
if (is_array($attrs)) {
$parts = [];
foreach ($attrs as $k => $v) {
if (is_int($k) && is_array($v)) {
$parts[] = ($v['name'] ?? $v['key'] ?? '') . ':' . ($v['value'] ?? '');
} else {
$parts[] = $k . ':' . $v;
}
}
$attrs_text = implode(';', array_filter($parts));
} elseif (is_string($attrs)) {
$attrs_text = $attrs;
}
?>
<tr>
<td><input type="text" name="aksrt_variant_sku[]" value="<?php echo esc_attr((string) ($row['sku'] ?? '')); ?>"></td>
<td><input type="text" name="aksrt_variant_attrs[]" value="<?php echo esc_attr($attrs_text); ?>" placeholder="颜色:红;尺寸:L"></td>
<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="<?php echo esc_attr((string) ($row['price'] ?? 0)); ?>"></td>
<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="<?php echo esc_attr((string) ($row['stock'] ?? 0)); ?>"></td>
<td><input type="url" name="aksrt_variant_image[]" value="<?php echo esc_attr((string) ($row['image'] ?? '')); ?>"></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" class="button" id="aksrt-add-variant-row"><?php esc_html_e('添加变体行', 'aksrt'); ?></button>
</p>
<script>
(function(){
var btn = document.getElementById('aksrt-add-variant-row');
var tbody = document.querySelector('#aksrt-variants-table tbody');
if (!btn || !tbody) return;
btn.addEventListener('click', function(){
var tr = document.createElement('tr');
tr.innerHTML = '<td><input type="text" name="aksrt_variant_sku[]" value=""></td>' +
'<td><input type="text" name="aksrt_variant_attrs[]" value="" placeholder="颜色:红;尺寸:L"></td>' +
'<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="0"></td>' +
'<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="0"></td>' +
'<td><input type="url" name="aksrt_variant_image[]" value=""></td>';
tbody.appendChild(tr);
});
})();
</script>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_variants_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_variants_nonce'])), 'aksrt_product_variants_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product' || !current_user_can('edit_post', $post_id)) {
return;
}
if (!class_exists('Shop')) {
return;
}
$skus = isset($_POST['aksrt_variant_sku']) && is_array($_POST['aksrt_variant_sku']) ? wp_unslash($_POST['aksrt_variant_sku']) : [];
$attrs = isset($_POST['aksrt_variant_attrs']) && is_array($_POST['aksrt_variant_attrs']) ? wp_unslash($_POST['aksrt_variant_attrs']) : [];
$prices = isset($_POST['aksrt_variant_price']) && is_array($_POST['aksrt_variant_price']) ? wp_unslash($_POST['aksrt_variant_price']) : [];
$stocks = isset($_POST['aksrt_variant_stock']) && is_array($_POST['aksrt_variant_stock']) ? wp_unslash($_POST['aksrt_variant_stock']) : [];
$images = isset($_POST['aksrt_variant_image']) && is_array($_POST['aksrt_variant_image']) ? wp_unslash($_POST['aksrt_variant_image']) : [];
$variants = [];
$count = max(count($skus), count($attrs), count($prices), count($stocks), count($images));
for ($i = 0; $i < $count; $i++) {
$sku = sanitize_text_field((string) ($skus[$i] ?? ''));
$attr_raw = sanitize_text_field((string) ($attrs[$i] ?? ''));
$price = max(0, (float) ($prices[$i] ?? 0));
$stock = max(0, (int) ($stocks[$i] ?? 0));
$image = esc_url_raw((string) ($images[$i] ?? ''));
if ($sku === '' && $attr_raw === '' && $price <= 0 && $stock <= 0 && $image === '') {
continue;
}
$attributes = [];
if ($attr_raw !== '') {
foreach (preg_split('/\s*;\s*/', $attr_raw) ?: [] as $pair) {
if ($pair === '' || !str_contains($pair, ':')) {
continue;
}
[$k, $v] = array_map('trim', explode(':', $pair, 2));
if ($k !== '') {
$attributes[$k] = $v;
}
}
}
$variants[] = [
'sku' => $sku,
'attributes' => $attributes,
'price' => $price,
'stock' => $stock,
'image' => $image,
];
}
Shop::get_instance()->save_variants($post_id, $variants);
}
}
+429
View File
@@ -0,0 +1,429 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class Shop {
use Part1;
use Part2;
use Part3;
private static $instance = null;
private ?CartService $cart_svc = null;
private ?CheckoutService $checkout_svc = null;
private ?AddressService $address_svc = null;
private ?ProductService $product_svc = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function cart_service(): CartService {
if (!$this->cart_svc) {
$this->cart_svc = new CartService($this);
}
return $this->cart_svc;
}
private function checkout_service(): CheckoutService {
if (!$this->checkout_svc) {
$this->checkout_svc = new CheckoutService($this);
}
return $this->checkout_svc;
}
private function address_service(): AddressService {
if (!$this->address_svc) {
$this->address_svc = new AddressService($this);
}
return $this->address_svc;
}
private function product_service(): ProductService {
if ($this->product_svc === null) {
$this->product_svc = new ProductService($this);
}
return $this->product_svc;
}
public function get_config(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_shop_settings();
}
return ['enabled' => false];
}
public function get_products(array $args = []): array {
$limit = max(1, (int) ($args['limit'] ?? 20));
$offset = max(0, (int) ($args['offset'] ?? 0));
$search = sanitize_text_field((string) ($args['search'] ?? ''));
$status = sanitize_key((string) ($args['status'] ?? ''));
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $limit,
'offset' => $offset,
'post_status' => $status !== '' ? $status : ['publish', 'draft', 'pending'],
'orderby' => 'date',
'order' => 'DESC',
];
if ($search !== '') {
$query_args['s'] = $search;
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
return $products;
}
public function get_product_detail_view(int $product_id): array {
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return [];
}
$detail = $this->build_product_card_view($product_id);
$detail['content'] = (string) $product->post_content;
$detail['excerpt'] = (string) $product->post_excerpt;
$detail['permalink'] = get_permalink($product_id) ?: '';
$detail['status'] = (string) get_post_status($product_id);
$detail['gallery'] = $this->product_service()->get_product_gallery($product_id);
$detail['categories'] = wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'all']) ?: [];
$detail['tags'] = wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'all']) ?: [];
$detail['shipping'] = $this->product_service()->get_product_shipping_profile($product_id);
$detail['after_sale'] = $this->product_service()->get_product_after_sale_profile($product_id);
$detail['marketing'] = $this->product_service()->get_product_marketing_profile($product_id);
$detail['checkout_hint'] = $this->get_checkout_hint($detail);
$detail['purchase_enabled'] = ($detail['is_unlimited'] || $detail['stock'] > 0) && $detail['status'] === 'publish';
$detail['default_quantity'] = 1;
$detail['options'] = $this->get_product_options($product_id);
$detail['has_options'] = $this->has_options($product_id);
$detail['price_range'] = $this->get_price_range($product_id);
$detail['pay_modo'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0';
$detail['start_price'] = (float) get_post_meta($product_id, '_aksrt_start_price', true);
$detail['shipping_type'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_type', true)) ?: 'express';
$detail['auto_delivery'] = $this->get_auto_delivery_config($product_id);
$detail['limit_buy'] = $this->get_limit_buy_config($product_id);
$detail['services'] = $this->get_product_services($product_id);
$detail['params'] = $this->get_product_params($product_id);
return $detail;
}
public function get_product_edit_view(int $user_id, int $product_id = 0): array {
$view = [
'allowed' => true,
'is_edit' => false,
'product' => [
'id' => 0,
'title' => '',
'price' => 0,
'stock' => 0,
'excerpt' => '',
'content' => '',
'image' => '',
'gallery' => [],
'categories' => [],
'tags' => [],
'shipping_mode' => 'express',
'shipping_fee' => 0,
'after_sale_policy' => '',
'support_refund' => true,
'status' => 'draft',
'options' => [],
'pay_modo' => '0',
'start_price' => 0,
'product_video' => [],
'product_params' => '',
],
'taxonomy' => [
'categories' => $this->product_service()->get_term_options('aksrt_product_cat'),
'tags' => $this->product_service()->get_term_options('aksrt_product_tag'),
],
'shipping_modes' => $this->get_shipping_method_options(),
'status_options' => [
['value' => 'draft', 'label' => __('草稿', 'aksrt')],
['value' => 'publish', 'label' => __('发布', 'aksrt')],
['value' => 'pending', 'label' => __('待审核', 'aksrt')],
],
];
if ($product_id <= 0) {
return $view;
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $view;
}
$is_owner = (int) $product->post_author === $user_id || current_user_can('manage_options');
if (!$is_owner) {
$view['allowed'] = false;
return $view;
}
$view['is_edit'] = true;
$videos = get_post_meta($product_id, '_aksrt_product_video', true);
if (!is_array($videos)) {
$videos = [];
}
$view['product'] = [
'id' => $product_id,
'title' => (string) $product->post_title,
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
'stock' => (int) get_post_meta($product_id, '_aksrt_stock', true),
'excerpt' => (string) $product->post_excerpt,
'content' => (string) $product->post_content,
'image' => $this->resolve_main_image($product_id),
'gallery' => $this->product_service()->get_product_gallery($product_id),
'categories' => wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'ids']) ?: [],
'tags' => wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
'shipping_mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
'shipping_fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
'after_sale_policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
'status' => (string) get_post_status($product_id),
'options' => method_exists($this, 'get_product_options') ? $this->get_product_options($product_id) : [],
'pay_modo' => sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0',
'start_price' => (float) get_post_meta($product_id, '_aksrt_start_price', true),
'product_video' => array_values(array_filter(array_map('esc_url_raw', $videos))),
'product_params' => (string) get_post_meta($product_id, '_aksrt_product_params', true),
];
return $view;
}
public function get_cart_page_view(int $user_id): array {
$pending_orders = $this->get_order_center_view($user_id, [
'page' => 1,
'per_page' => 5,
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
]);
return [
'payment_methods' => $this->get_payment_method_options(),
'shipping_methods' => $this->get_shipping_method_options(),
'address_book' => $this->get_user_address_book($user_id),
'coupon_enabled' => class_exists('\AKSRT\Payment\Coupon'),
'currency_symbol' => '¥',
'pending_orders' => $pending_orders['items'] ?? [],
'cart_items' => $this->get_cart($user_id),
'cart_count' => $this->get_cart_count($user_id),
];
}
public function get_cart(int $user_id): array {
return $this->cart_service()->get_cart($user_id);
}
public function get_cart_count(int $user_id): int {
return $this->cart_service()->get_cart_count($user_id);
}
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
return $this->cart_service()->add_to_cart($user_id, $product_id, $options_string, $count);
}
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
return $this->cart_service()->reduce_from_cart($user_id, $product_id, $options_string, $count);
}
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
return $this->cart_service()->remove_from_cart($user_id, $product_id, $options_string);
}
public function remove_multi_from_cart(int $user_id, array $items): array {
return $this->cart_service()->remove_multi_from_cart($user_id, $items);
}
public function empty_cart(int $user_id): array {
return $this->cart_service()->empty_cart($user_id);
}
public function validate_cart_item(int $product_id, string $options_string = '') {
return $this->cart_service()->validate_cart_item($product_id, $options_string);
}
public function get_order_center_view(int $user_id, array $args = []): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1];
}
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 20)));
$status = sanitize_key((string) ($args['status'] ?? ''));
$repo_args = [
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
];
if ($status !== '') {
$repo_args['status'] = $status;
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_user_orders($user_id, $repo_args);
$total = $repo->count_user_orders($user_id, $repo_args);
$items = [];
foreach ($orders as $order) {
$items[] = $this->decorate_order_view($order);
}
return [
'items' => $items,
'total' => $total,
'total_pages' => max(1, (int) ceil($total / $per_page)),
'page' => $page,
];
}
public function get_order_detail_view(int $user_id, string $order_number): ?array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return null;
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->get_user_order($user_id, $order_number);
if (!$order) {
return null;
}
return $this->decorate_order_view($order, true);
}
public function preview_checkout(int $user_id, array $payload): array {
return $this->checkout_service()->preview_checkout($user_id, $payload);
}
public function create_checkout_order(int $user_id, array $payload): array {
return $this->checkout_service()->create_checkout_order($user_id, $payload);
}
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
return $this->checkout_service()->pay_order($user_id, $order_number, $payment_method);
}
public function get_user_favorites(int $user_id, int $page = 1, int $per_page = 20): array {
if ($user_id <= 0) {
return ['items' => [], 'total' => 0];
}
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
if (!is_array($fav_ids) || empty($fav_ids)) {
return ['items' => [], 'total' => 0];
}
$total = count($fav_ids);
$paged = array_slice(array_reverse($fav_ids), ($page - 1) * $per_page, $per_page);
$items = [];
foreach ($paged as $pid) {
$card = $this->build_product_card_view((int) $pid);
if (!empty($card)) {
$items[] = $card;
}
}
return ['items' => $items, 'total' => $total];
}
public function toggle_favorite(int $user_id, int $product_id): array {
if ($user_id <= 0 || $product_id <= 0) {
return ['success' => false];
}
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
if (!is_array($fav_ids)) {
$fav_ids = [];
}
$key = array_search($product_id, $fav_ids, true);
if ($key !== false) {
unset($fav_ids[$key]);
$fav_ids = array_values($fav_ids);
$is_fav = false;
} else {
$fav_ids[] = $product_id;
$is_fav = true;
}
update_user_meta($user_id, 'aksrt_product_favorites', $fav_ids);
$count = (int) get_post_meta($product_id, '_aksrt_favorite_count', true);
update_post_meta($product_id, '_aksrt_favorite_count', max(0, $count + ($is_fav ? 1 : -1)));
if ($is_fav) {
do_action('aksrt_product_favorited', $user_id, $product_id);
} else {
do_action('aksrt_product_unfavorited', $user_id, $product_id);
}
return ['success' => true, 'is_favorite' => $is_fav, 'count' => count($fav_ids)];
}
public function is_favorite(int $user_id, int $product_id): bool {
if ($user_id <= 0) return false;
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
return is_array($fav_ids) && in_array($product_id, $fav_ids, true);
}
public function re_purchase(int $user_id, string $order_number): array {
$items = $this->get_order_items($order_number);
if (empty($items)) {
return ['success' => false, 'message' => __('订单无商品记录', 'aksrt')];
}
$added = 0;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$qty = max(1, (int) $item['quantity']);
$opt_key = (string) $item['options_key'];
$result = $this->add_to_cart($user_id, $pid, $opt_key, $qty);
if (!empty($result['success'])) {
$added++;
}
}
return ['success' => $added > 0, 'added' => $added, 'cart_count' => $this->get_cart_count($user_id)];
}
public function cancel_order(int $user_id, string $order_number): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) {
return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')];
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
'force' => true,
'note' => __('用户在前台取消订单。', 'aksrt'),
]);
return [
'success' => true,
'data' => [
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
}
+748
View File
@@ -0,0 +1,748 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class TermMeta {
private static $instance = null;
/** @var string Meta key for product_cat/product_tag unified storage */
const META_KEY = 'aksrt_shop_term_meta';
/** @var string Meta key for shop_discount unified storage */
const DISCOUNT_META_KEY = 'aksrt_shop_discount_meta';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Register hooks.
*/
public function init(): void {
add_action('init', [$this, 'register_discount_taxonomy'], 5);
add_action('init', [$this, 'register_term_meta_fields'], 10);
// Native term UI for shop_discount (zibll-style admin ops).
add_action('shop_discount_add_form_fields', [$this, 'render_discount_add_fields']);
add_action('shop_discount_edit_form_fields', [$this, 'render_discount_edit_fields'], 10, 1);
add_action('created_shop_discount', [$this, 'save_discount_term_fields']);
add_action('edited_shop_discount', [$this, 'save_discount_term_fields']);
// Product category inheritance fields.
add_action('aksrt_product_cat_add_form_fields', [$this, 'render_cat_add_fields']);
add_action('aksrt_product_cat_edit_form_fields', [$this, 'render_cat_edit_fields'], 10, 1);
add_action('created_aksrt_product_cat', [$this, 'save_cat_term_fields']);
add_action('edited_aksrt_product_cat', [$this, 'save_cat_term_fields']);
// Product tag display fields.
add_action('aksrt_product_tag_add_form_fields', [$this, 'render_tag_add_fields']);
add_action('aksrt_product_tag_edit_form_fields', [$this, 'render_tag_edit_fields'], 10, 1);
add_action('created_aksrt_product_tag', [$this, 'save_tag_term_fields']);
add_action('edited_aksrt_product_tag', [$this, 'save_tag_term_fields']);
}
// ─── Taxonomy Registration ───────────────────────────────────
/**
* Register the shop_discount taxonomy for discount campaigns.
*/
public function register_discount_taxonomy(): void {
if (taxonomy_exists('shop_discount')) {
return;
}
$shop_enabled = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$shop_settings = \AKSRT\Settings\Settings::get_shop_settings();
$shop_enabled = !empty($shop_settings['enabled']);
}
$shop_show_ui = true;
$shop_show_menu = is_admin() ? $shop_enabled : false;
register_taxonomy('shop_discount', 'aksrt_product', [
'labels' => [
'name' => __('优惠活动', 'aksrt'),
'singular_name' => __('优惠活动', 'aksrt'),
'search_items' => __('搜索优惠活动', 'aksrt'),
'all_items' => __('所有优惠活动', 'aksrt'),
'edit_item' => __('编辑优惠活动', 'aksrt'),
'add_new_item' => __('新增优惠活动', 'aksrt'),
'menu_name' => __('优惠活动', 'aksrt'),
],
'public' => true,
'hierarchical' => false,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'shop-discount', 'with_front' => false],
]);
}
// ─── Term Meta Registration ──────────────────────────────────
/**
* Register all term meta fields for REST API exposure.
*/
public function register_term_meta_fields(): void {
$auth = function () {
return current_user_can('manage_categories');
};
// ── aksrt_product_cat ──
$cat_fields = $this->get_cat_field_definitions();
foreach ($cat_fields as $key => $def) {
register_term_meta('aksrt_product_cat', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
// ── aksrt_product_tag ──
$tag_fields = $this->get_tag_field_definitions();
foreach ($tag_fields as $key => $def) {
register_term_meta('aksrt_product_tag', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
// ── shop_discount ──
$discount_fields = $this->get_discount_field_definitions();
foreach ($discount_fields as $key => $def) {
register_term_meta('shop_discount', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
}
// ─── Field Definitions ───────────────────────────────────────
/**
* Product category term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_cat_field_definitions(): array {
return [
'shop_cat_limit_buy' => [
'type' => 'integer', 'description' => '分类级限购 (-1 不限)', 'default' => -1,
'sanitize' => function ($v) { return max(-1, min(9999, (int) $v)); },
],
'shop_cat_shipping_fee' => [
'type' => 'string', 'description' => '分类级运费类型', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'free', 'fixed', 'amount'], true) ? $v : '';
},
],
'shop_cat_guest_buy' => [
'type' => 'string', 'description' => '分类级免登录购买', 'default' => '',
],
'shop_cat_email_fill' => [
'type' => 'string', 'description' => '分类级邮箱必留', 'default' => '',
],
'shop_cat_after_sale' => [
'type' => 'string', 'description' => '分类级售后政策', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'refund', 'refund_return', 'none'], true) ? $v : '';
},
],
'shop_cat_rebate' => [
'type' => 'string', 'description' => '分类级返佣覆盖', 'default' => '',
],
'shop_cat_service' => [
'type' => 'string', 'description' => '分类级服务保障', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_cat_single_tab' => [
'type' => 'string', 'description' => '分类级自定义 TAB', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_cat_content_after' => [
'type' => 'string', 'description' => '分类级详情页底部内容', 'default' => '',
'sanitize' => 'wp_kses_post',
],
'shop_cat_content_layout' => [
'type' => 'string', 'description' => '分类级内容布局', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'adapt', 'full', 'sidebar'], true) ? $v : '';
},
],
'shop_cat_content_show_bg' => [
'type' => 'string', 'description' => '分类级详情背景盒子', 'default' => '',
],
'shop_cat_list_style' => [
'type' => 'string', 'description' => '分类页列表样式', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'default', 'card-lg', 'card-sm'], true) ? $v : '';
},
],
];
}
/**
* Product tag term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_tag_field_definitions(): array {
return [
'shop_tag_priority' => [
'type' => 'integer', 'description' => '标签优先级(影响显示顺序)', 'default' => 0,
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
],
'shop_tag_class' => [
'type' => 'string', 'description' => '标签颜色 class', 'default' => '',
'sanitize' => 'sanitize_html_class',
],
'shop_tag_is_important' => [
'type' => 'string', 'description' => '重点标签(商品价格位着重显示)', 'default' => '',
],
];
}
/**
* Shop discount term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_discount_field_definitions(): array {
return [
'shop_discount_small_badge' => [
'type' => 'string', 'description' => '小标签文案', 'default' => '',
],
'shop_discount_priority' => [
'type' => 'integer', 'description' => '优先级(数字越大越优先)', 'default' => 0,
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
],
'shop_discount_is_important' => [
'type' => 'string', 'description' => '重点活动', 'default' => '',
],
'shop_discount_important_class' => [
'type' => 'string', 'description' => '重点活动背景色 class', 'default' => '',
'sanitize' => 'sanitize_html_class',
],
'shop_discount_scope' => [
'type' => 'string', 'description' => '优惠范围', 'default' => 'item',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['item', 'product', 'author', 'order'], true) ? $v : 'item';
},
],
'shop_discount_type' => [
'type' => 'string', 'description' => '优惠类型', 'default' => 'reduction',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['reduction', 'discount', 'gift'], true) ? $v : 'reduction';
},
],
'shop_discount_value' => [
'type' => 'string', 'description' => '优惠数值(立减金额/折扣 0.01-9.99)', 'default' => '0',
'sanitize' => function ($v) { return sanitize_text_field((string) $v); },
],
'shop_discount_gift_config' => [
'type' => 'string', 'description' => '赠品配置 JSON', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_discount_price_limit' => [
'type' => 'string', 'description' => '金额门槛', 'default' => '0',
'sanitize' => function ($v) { return max(0, (float) $v); },
],
'shop_discount_user_limit' => [
'type' => 'string', 'description' => '用户身份限制', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'vip', 'vip_2', 'auth'], true) ? $v : '';
},
],
'shop_discount_start' => [
'type' => 'string', 'description' => '活动开始时间', 'default' => '',
'sanitize' => 'sanitize_text_field',
],
'shop_discount_end' => [
'type' => 'string', 'description' => '活动结束时间', 'default' => '',
'sanitize' => 'sanitize_text_field',
],
'shop_discount_countdown' => [
'type' => 'string', 'description' => '倒计时显示', 'default' => '',
],
];
}
// ─── Getters ─────────────────────────────────────────────────
/**
* Get all shop term meta for a product category.
*
* @param int $term_id
* @return array
*/
public static function get_cat_meta(int $term_id): array {
$defaults = [
'limit_buy' => -1,
'shipping_fee' => '',
'guest_buy' => '',
'email_fill' => '',
'after_sale' => '',
'rebate' => '',
'service' => '',
'single_tab' => '',
'content_after' => '',
'content_layout' => '',
'content_show_bg' => '',
'list_style' => '',
];
$result = [];
foreach ($defaults as $key => $default) {
$meta_key = "shop_cat_{$key}";
$val = get_term_meta($term_id, $meta_key, true);
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
}
return $result;
}
/**
* Get all shop term meta for a product tag.
*
* @param int $term_id
* @return array
*/
public static function get_tag_meta(int $term_id): array {
$priority = get_term_meta($term_id, 'shop_tag_priority', true);
$class = get_term_meta($term_id, 'shop_tag_class', true);
$is_important = get_term_meta($term_id, 'shop_tag_is_important', true);
return [
'priority' => $priority !== '' ? (int) $priority : 0,
'class' => is_string($class) ? $class : '',
'is_important' => !empty($is_important),
];
}
/**
* Get all shop discount term meta.
*
* @param int $term_id
* @return array
*/
public static function get_discount_meta(int $term_id): array {
$defaults = [
'small_badge' => '',
'priority' => 0,
'is_important' => false,
'important_class' => '',
'scope' => 'item',
'type' => 'reduction',
'value' => '0',
'gift_config' => '',
'price_limit' => '0',
'user_limit' => '',
'start' => '',
'end' => '',
'countdown' => '',
];
$result = [];
foreach ($defaults as $key => $default) {
$meta_key = "shop_discount_{$key}";
$val = get_term_meta($term_id, $meta_key, true);
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
}
$result['priority'] = (int) $result['priority'];
$result['is_important'] = !empty($result['is_important']);
$result['price_limit'] = (float) $result['price_limit'];
return $result;
}
/**
* Resolve parameter inheritance: product → category → theme settings.
*
* Returns the effective value for a given parameter key by checking:
* 1. Product-level override (from post meta)
* 2. Category-level override (from term meta)
* 3. Theme-level default (from \AKSRT\Settings\Settings::get_shop_settings())
*
* @param int $product_id Product post ID
* @param string $param_key Parameter key (e.g. 'limit_buy', 'shipping_fee')
* @param mixed $product_value Product-level value (null if not set)
* @return mixed
*/
public static function resolve_inheritance(int $product_id, string $param_key, $product_value = null) {
// 1. Product-level override
if ($product_value !== null && $product_value !== '' && $product_value !== -1) {
return $product_value;
}
// 2. Category-level override
$terms = get_the_terms($product_id, 'aksrt_product_cat');
if (is_array($terms) && !empty($terms)) {
$cat_meta_key = "shop_cat_{$param_key}";
foreach ($terms as $term) {
$val = get_term_meta($term->term_id, $cat_meta_key, true);
if ($val !== '' && $val !== false && $val !== -1) {
return $val;
}
}
}
// 3. Theme-level default
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$defaults = $shop['product_defaults'] ?? [];
return $defaults[$param_key] ?? $product_value;
}
/**
* Add form fields for shop_discount taxonomy.
*/
public function render_discount_add_fields(): void {
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$default = $def['default'] ?? '';
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html($label) . '</label>';
$this->render_discount_field_control($key, $default, false);
echo '</div>';
}
}
/**
* Edit form fields for shop_discount taxonomy.
*/
public function render_discount_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field">';
echo '<th scope="row"><label for="' . esc_attr($key) . '">' . esc_html($label) . '</label></th>';
echo '<td>';
$this->render_discount_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param string|int|float $value
*/
private function render_discount_field_control(string $key, $value, bool $edit): void {
$id = esc_attr($key);
$name = esc_attr($key);
$val = (string) $value;
if ($key === 'shop_discount_type') {
$opts = [
'reduction' => __('立减', 'aksrt'),
'discount' => __('折扣', 'aksrt'),
'gift' => __('赠品', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_scope') {
$opts = [
'item' => __('单品', 'aksrt'),
'product' => __('商品', 'aksrt'),
'author' => __('商家', 'aksrt'),
'order' => __('整单', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_user_limit') {
$opts = [
'' => __('不限', 'aksrt'),
'vip' => __('VIP1', 'aksrt'),
'vip_2' => __('VIP2', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
echo '<label><input type="checkbox" name="' . $name . '" id="' . $id . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if ($key === 'shop_discount_gift_config') {
echo '<textarea name="' . $name . '" id="' . $id . '" rows="3" class="large-text">' . esc_textarea($val) . '</textarea>';
return;
}
$type = in_array($key, ['shop_discount_priority'], true) ? 'number' : 'text';
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true)) {
$type = 'datetime-local';
// Convert mysql-ish values to datetime-local if needed.
if ($val !== '' && str_contains($val, ' ')) {
$val = str_replace(' ', 'T', substr($val, 0, 16));
}
}
printf(
'<input type="%s" name="%s" id="%s" value="%s" class="%s" />',
esc_attr($type),
$name,
$id,
esc_attr($val),
$edit ? 'regular-text' : ''
);
}
public function save_discount_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true) && is_string($value) && str_contains($value, 'T')) {
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
}
update_term_meta($term_id, $key, $value);
}
}
public function render_cat_add_fields(): void {
foreach ($this->get_cat_field_definitions() as $key => $def) {
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
$this->render_cat_field_control($key, $def['default'] ?? '', false);
echo '</div>';
}
}
public function render_cat_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
foreach ($this->get_cat_field_definitions() as $key => $def) {
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
$this->render_cat_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param mixed $value
*/
private function render_cat_field_control(string $key, $value, bool $edit): void {
$val = (string) $value;
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if (in_array($key, ['shop_cat_service', 'shop_cat_single_tab', 'shop_cat_content_after'], true)) {
echo '<textarea class="large-text" rows="3" name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">' . esc_textarea($val) . '</textarea>';
return;
}
if ($key === 'shop_cat_shipping_fee') {
$opts = ['' => __('继承', 'aksrt'), 'free' => __('包邮', 'aksrt'), 'fixed' => __('固定运费', 'aksrt'), 'amount' => __('满额包邮', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_after_sale') {
$opts = ['' => __('继承', 'aksrt'), 'refund' => __('仅退款', 'aksrt'), 'refund_return' => __('退货退款', 'aksrt'), 'none' => __('不支持', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_content_layout') {
$opts = ['' => __('继承', 'aksrt'), 'adapt' => __('自适应', 'aksrt'), 'full' => __('通栏', 'aksrt'), 'sidebar' => __('侧栏', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_list_style') {
$opts = ['' => __('继承', 'aksrt'), 'default' => __('默认', 'aksrt'), 'card-lg' => __('大卡片', 'aksrt'), 'card-sm' => __('小卡片', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
$type = $key === 'shop_cat_limit_buy' ? 'number' : 'text';
printf(
'<input type="%s" class="%s" name="%s" id="%s" value="%s" />',
esc_attr($type),
$edit ? 'regular-text' : '',
esc_attr($key),
esc_attr($key),
esc_attr($val)
);
}
public function save_cat_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
foreach ($this->get_cat_field_definitions() as $key => $def) {
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
update_term_meta($term_id, $key, $value);
}
}
public function render_tag_add_fields(): void {
foreach ($this->get_tag_field_definitions() as $key => $def) {
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
$this->render_tag_field_control($key, $def['default'] ?? '');
echo '</div>';
}
}
public function render_tag_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
foreach ($this->get_tag_field_definitions() as $key => $def) {
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
$this->render_tag_field_control($key, $value);
echo '</td></tr>';
}
}
/**
* @param mixed $value
*/
private function render_tag_field_control(string $key, $value): void {
$val = (string) $value;
if ($key === 'shop_tag_is_important') {
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
$type = $key === 'shop_tag_priority' ? 'number' : 'text';
printf(
'<input type="%s" class="regular-text" name="%s" id="%s" value="%s" />',
esc_attr($type),
esc_attr($key),
esc_attr($key),
esc_attr($val)
);
}
public function save_tag_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
foreach ($this->get_tag_field_definitions() as $key => $def) {
if ($key === 'shop_tag_is_important') {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
update_term_meta($term_id, $key, $value);
}
}
}