Drop dual-read payment runtime fallbacks, align PHP schema to Vue keys, read gateway credentials only from aksrt_settings with enable toggles, expose configured adapters at checkout, fix credits pay signatures, and apply order expiry settings. Wire orphan order/card-pass/withdrawal schema pages; delete unused Payment* wrappers.
367 lines
13 KiB
PHP
367 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Shop Product Service
|
|
*
|
|
* Variant and product-meta helpers extracted from AKSRT_Shop.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Shop_Product_Service {
|
|
|
|
private AKSRT_Shop $shop;
|
|
|
|
public function __construct(AKSRT_Shop $shop) {
|
|
$this->shop = $shop;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Product meta helpers
|
|
* ============================================================ */
|
|
|
|
public function get_term_options(string $taxonomy): array {
|
|
$terms = get_terms([
|
|
'taxonomy' => $taxonomy,
|
|
'hide_empty' => false,
|
|
'orderby' => 'name',
|
|
'order' => 'ASC',
|
|
]);
|
|
|
|
if (is_wp_error($terms) || empty($terms)) {
|
|
return [];
|
|
}
|
|
|
|
return array_map(static function ($term) {
|
|
return [
|
|
'id' => (int) $term->term_id,
|
|
'name' => (string) $term->name,
|
|
];
|
|
}, $terms);
|
|
}
|
|
|
|
public function get_product_gallery(int $product_id): array {
|
|
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
|
|
if (!is_array($gallery)) {
|
|
return [];
|
|
}
|
|
|
|
return array_values(array_filter(array_map('esc_url_raw', $gallery)));
|
|
}
|
|
|
|
public function get_product_shipping_profile(int $product_id): array {
|
|
return [
|
|
'mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
|
|
'fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
|
|
'delivery_note' => (string) get_post_meta($product_id, '_aksrt_delivery_note', true),
|
|
];
|
|
}
|
|
|
|
public function get_product_after_sale_profile(int $product_id): array {
|
|
return [
|
|
'policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
|
|
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
|
|
];
|
|
}
|
|
|
|
public function get_product_marketing_profile(int $product_id): array {
|
|
return [
|
|
'headline' => (string) get_post_meta($product_id, '_aksrt_marketing_headline', true),
|
|
'shipping_tip' => (string) get_post_meta($product_id, '_aksrt_shipping_tip', true),
|
|
'service_tip' => (string) get_post_meta($product_id, '_aksrt_service_tip', true),
|
|
];
|
|
}
|
|
|
|
public function get_payment_method_options(): array {
|
|
$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') && AKSRT_Settings::credits_enabled()) {
|
|
$methods['points'] = [
|
|
'value' => 'points',
|
|
'label' => __('积分兑换', 'aksrt'),
|
|
'description' => __('使用积分购买积分商品。', 'aksrt'),
|
|
];
|
|
}
|
|
|
|
$manual = true;
|
|
if (class_exists('AKSRT_Settings')) {
|
|
$gw = AKSRT_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';
|
|
}
|
|
}
|