400 lines
17 KiB
PHP
400 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Shop;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
trait Part3 {
|
|
|
|
|
|
public function deduct_option_stock(int $product_id, string $options_key, int $qty): void {
|
|
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
|
|
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
|
|
$matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
|
|
if (is_array($matrix) && isset($matrix[$options_key])) {
|
|
$matrix[$options_key] = max(0, (int) $matrix[$options_key] - $qty);
|
|
update_post_meta($product_id, '_aksrt_stock_opts', $matrix);
|
|
}
|
|
return;
|
|
}
|
|
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
|
|
if ($stock > 0) {
|
|
update_post_meta($product_id, '_aksrt_stock', max(0, $stock - $qty));
|
|
}
|
|
}
|
|
|
|
public function get_option_image(int $product_id, string $options_key): string {
|
|
if ($options_key === '' || $options_key === '_default') {
|
|
return '';
|
|
}
|
|
$groups = $this->get_product_options($product_id);
|
|
$selections = explode('|', $options_key);
|
|
foreach ($groups as $i => $group) {
|
|
$sel = $selections[$i] ?? '';
|
|
foreach ($group['values'] as $val) {
|
|
if ($val['label'] === $sel && !empty($val['image'])) {
|
|
return $val['image'];
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/* ============================================================
|
|
* Auto-delivery (§6.2 自动发货)
|
|
*
|
|
* Storage: post_meta '_aksrt_auto_delivery' = [
|
|
* 'type' => 'fixed'|'invit_code'|'card_pass'|'opts',
|
|
* 'content' => '固定内容文本',
|
|
* 'opts' => [ options_key => '该选项的发货内容' ],
|
|
* ]
|
|
* ============================================================ */
|
|
|
|
public function get_auto_delivery_config(int $product_id): array {
|
|
$raw = get_post_meta($product_id, '_aksrt_auto_delivery', true);
|
|
if (!is_array($raw)) {
|
|
return ['type' => '', 'content' => '', 'opts' => []];
|
|
}
|
|
return [
|
|
'type' => sanitize_key((string) ($raw['type'] ?? '')),
|
|
'content' => (string) ($raw['content'] ?? ''),
|
|
'opts' => is_array($raw['opts'] ?? null) ? $raw['opts'] : [],
|
|
];
|
|
}
|
|
|
|
public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id = 0): string {
|
|
$config = $this->get_auto_delivery_config($product_id);
|
|
$type = $config['type'];
|
|
|
|
if ($type === 'fixed') {
|
|
return (string) $config['content'];
|
|
}
|
|
if ($type === 'opts') {
|
|
$key = ($options_key !== '' && $options_key !== '_default') ? $options_key : '_default';
|
|
return (string) ($config['opts'][$key] ?? $config['content']);
|
|
}
|
|
if ($type === 'card_pass') {
|
|
return $this->consume_card_keys($product_id, $options_key, $qty, $user_id, $order_number, $order_item_id);
|
|
}
|
|
if ($type === 'invit_code') {
|
|
return $this->create_invite_codes($qty, $user_id, $order_number);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private function auto_delivery_succeeded(int $product_id, string $content, int $qty): bool {
|
|
$config = $this->get_auto_delivery_config($product_id);
|
|
if (in_array($config['type'], ['card_pass', 'invit_code'], true)) {
|
|
$lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $content) ?: [])));
|
|
return count($lines) === max(1, $qty)
|
|
&& !in_array($content, [__('卡密已售罄', 'aksrt'), __('邀请系统不可用', 'aksrt')], true);
|
|
}
|
|
return trim($content) !== '';
|
|
}
|
|
|
|
private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id): string {
|
|
global $wpdb;
|
|
$table = \AKSRT\Core\DBMigrator::table('card_key');
|
|
if ($table === '' || $order_item_id <= 0 || $wpdb->query('START TRANSACTION') === false) return '';
|
|
|
|
$where_opt = '';
|
|
$params = [$product_id];
|
|
if ($options_key !== '' && $options_key !== '_default') {
|
|
$where_opt = ' AND options_key = %s';
|
|
$params[] = $options_key;
|
|
} else {
|
|
$where_opt = " AND (options_key = '' OR options_key = '_default')";
|
|
}
|
|
$params[] = $qty;
|
|
|
|
$keys = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT id, card_code FROM {$table} WHERE product_id = %d AND status = 'available'{$where_opt} ORDER BY id ASC LIMIT %d FOR UPDATE",
|
|
...$params
|
|
), ARRAY_A);
|
|
if (count((array) $keys) !== $qty) {
|
|
$wpdb->query('ROLLBACK');
|
|
return __('卡密已售罄', 'aksrt');
|
|
}
|
|
|
|
$codes = [];
|
|
$ids = [];
|
|
foreach ($keys as $key) {
|
|
$codes[] = (string) $key['card_code'];
|
|
$ids[] = (int) $key['id'];
|
|
}
|
|
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
|
$updated = $wpdb->query($wpdb->prepare(
|
|
"UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE status = 'available' AND id IN ({$placeholders})",
|
|
array_merge([$order_number, $user_id, current_time('mysql')], $ids)
|
|
));
|
|
$content = implode("\n", $codes);
|
|
$item_table = $this->order_item_table();
|
|
$persisted = $item_table !== '' ? $wpdb->update(
|
|
$item_table,
|
|
['auto_delivery_content' => $content],
|
|
['id' => $order_item_id, 'order_number' => $order_number],
|
|
['%s'],
|
|
['%d', '%s']
|
|
) : false;
|
|
if ($updated !== $qty || $persisted === false || $wpdb->query('COMMIT') === false) {
|
|
$wpdb->query('ROLLBACK');
|
|
return '';
|
|
}
|
|
return $content;
|
|
}
|
|
|
|
private function create_invite_codes(int $qty, int $user_id, string $order_number): string {
|
|
if (!class_exists('\AKSRT\Credits\Invite')) {
|
|
return __('邀请系统不可用', 'aksrt');
|
|
}
|
|
$invite = \AKSRT\Credits\Invite::get_instance();
|
|
$codes = [];
|
|
for ($i = 0; $i < $qty; $i++) {
|
|
$code = $invite->generate_code($user_id);
|
|
if ($code) {
|
|
$codes[] = $code;
|
|
}
|
|
}
|
|
return implode("\n", $codes);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Order Items (§6.5 多商品订单子表)
|
|
* ============================================================ */
|
|
|
|
private function order_item_table(): string {
|
|
return \AKSRT\Core\DBMigrator::table('order_item');
|
|
}
|
|
|
|
public function save_order_items(string $order_number, array $items): void {
|
|
global $wpdb;
|
|
$table = $this->order_item_table();
|
|
if ($table === '') return;
|
|
|
|
foreach ($items as $item) {
|
|
$wpdb->insert($table, [
|
|
'order_number' => $order_number,
|
|
'product_id' => (int) ($item['product_id'] ?? 0),
|
|
'product_title' => sanitize_text_field((string) ($item['title'] ?? '')),
|
|
'options_key' => sanitize_text_field((string) ($item['options_key'] ?? '')),
|
|
'options_label' => sanitize_text_field((string) ($item['options_label'] ?? '')),
|
|
'quantity' => max(1, (int) ($item['quantity'] ?? 1)),
|
|
'unit_price' => (float) ($item['unit_price'] ?? $item['price'] ?? 0),
|
|
'total_price' => (float) ($item['total_price'] ?? $item['line_total'] ?? 0),
|
|
'discount_amount' => (float) ($item['discount_amount'] ?? 0),
|
|
'shipping_fee' => (float) ($item['shipping_fee'] ?? 0),
|
|
'pay_price' => (float) ($item['pay_price'] ?? $item['line_total'] ?? 0),
|
|
'shipping_type' => sanitize_key((string) ($item['shipping_type'] ?? 'express')),
|
|
'auto_delivery_content'=> ($item['auto_delivery_content'] ?? null),
|
|
'discount_hit' => isset($item['applied_discounts']) ? wp_json_encode($item['applied_discounts']) : null,
|
|
'gift_data' => isset($item['gifts']) ? wp_json_encode($item['gifts']) : null,
|
|
'image' => esc_url_raw((string) ($item['image'] ?? '')),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function get_order_items(string $order_number): array {
|
|
global $wpdb;
|
|
$table = $this->order_item_table();
|
|
if ($table === '') return [];
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM {$table} WHERE order_number = %s ORDER BY id ASC",
|
|
$order_number
|
|
), ARRAY_A);
|
|
|
|
return array_map(static function ($row) {
|
|
return [
|
|
'id' => (int) $row['id'],
|
|
'product_id' => (int) $row['product_id'],
|
|
'product_title' => (string) $row['product_title'],
|
|
'options_key' => (string) $row['options_key'],
|
|
'options_label' => (string) $row['options_label'],
|
|
'quantity' => (int) $row['quantity'],
|
|
'unit_price' => (float) $row['unit_price'],
|
|
'total_price' => (float) $row['total_price'],
|
|
'discount_amount' => (float) $row['discount_amount'],
|
|
'shipping_fee' => (float) $row['shipping_fee'],
|
|
'pay_price' => (float) $row['pay_price'],
|
|
'shipping_type' => (string) $row['shipping_type'],
|
|
'auto_delivery_content' => $row['auto_delivery_content'],
|
|
'discount_hit' => json_decode($row['discount_hit'] ?? '', true) ?: [],
|
|
'gift_data' => json_decode($row['gift_data'] ?? '', true) ?: [],
|
|
'image' => (string) $row['image'],
|
|
];
|
|
}, (array) $rows);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Enhanced Fulfillment (§6.5 履约链路)
|
|
* ============================================================ */
|
|
|
|
public function handle_order_paid_enhanced(string $order_number, array $order): void {
|
|
$items = $this->get_order_items($order_number);
|
|
|
|
if (empty($items)) {
|
|
$this->handle_order_paid($order_number, $order);
|
|
return;
|
|
}
|
|
|
|
$user_id = (int) ($order['user_id'] ?? 0);
|
|
$auto_delivery_parts = [];
|
|
$auto_delivery_successes = 0;
|
|
|
|
foreach ($items as $item) {
|
|
$pid = (int) $item['product_id'];
|
|
$qty = max(1, (int) $item['quantity']);
|
|
$opt_key = (string) $item['options_key'];
|
|
if ($pid <= 0) continue;
|
|
|
|
$this->deduct_option_stock($pid, $opt_key, $qty);
|
|
|
|
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
|
|
update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty);
|
|
|
|
$shipping_type = (string) get_post_meta($pid, '_aksrt_shipping_type', true);
|
|
if ($shipping_type === 'auto') {
|
|
$content = $this->execute_auto_delivery($pid, $opt_key, $qty, $user_id, $order_number, (int) $item['id']);
|
|
if ($content !== '') {
|
|
$auto_delivery_parts[] = $content;
|
|
$config = $this->get_auto_delivery_config($pid);
|
|
$persisted = $config['type'] === 'card_pass'
|
|
? true
|
|
: $this->update_order_item_delivery($item['id'], $content);
|
|
if ($persisted && $this->auto_delivery_succeeded($pid, $content, $qty)) {
|
|
$auto_delivery_successes++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->process_rebate($order_number, $order, $items);
|
|
$this->process_gifts($order_number, $order, $items);
|
|
|
|
if (!empty($auto_delivery_parts)) {
|
|
$this->send_auto_delivery_notification($user_id, $order_number, $auto_delivery_parts);
|
|
}
|
|
|
|
if ($auto_delivery_successes === count($items) && class_exists('\AKSRT\Payment\OrderRepository')) {
|
|
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
|
|
$order_number,
|
|
\AKSRT\Payment\Order::STATUS_COMPLETED,
|
|
['note' => __('自动发货订单已完成。', 'aksrt')]
|
|
);
|
|
}
|
|
|
|
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
|
|
}
|
|
|
|
private function update_order_item_delivery(int $item_id, string $content): bool {
|
|
global $wpdb;
|
|
$table = $this->order_item_table();
|
|
if ($table === '') return false;
|
|
$updated = $wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']);
|
|
if ($updated === false) return false;
|
|
if ($updated === 0) {
|
|
$stored = $wpdb->get_var($wpdb->prepare("SELECT auto_delivery_content FROM {$table} WHERE id = %d", $item_id));
|
|
return is_string($stored) && hash_equals($stored, $content);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private function send_auto_delivery_notification(int $user_id, string $order_number, array $parts): void {
|
|
if (!class_exists('AKSRT_Notification_Service')) return;
|
|
|
|
$content = sprintf(
|
|
__("订单 %s 已自动发货:\n\n%s", 'aksrt'),
|
|
$order_number,
|
|
implode("\n---\n", $parts)
|
|
);
|
|
|
|
AKSRT_Notification_Service::send($user_id, 'auto_delivery', $content, [
|
|
'order_number' => $order_number,
|
|
]);
|
|
|
|
if (function_exists('aksrt_send_event_email')) {
|
|
$user = get_userdata($user_id);
|
|
if ($user && !empty($user->user_email)) {
|
|
aksrt_send_event_email($user->user_email, sprintf(__('订单 %s 自动发货通知', 'aksrt'), $order_number), $content);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function process_rebate(string $order_number, array $order, array $items): void {
|
|
$user_id = (int) ($order['user_id'] ?? 0);
|
|
if ($user_id <= 0) return;
|
|
|
|
$referrer_id = 0;
|
|
if (class_exists('\AKSRT\Credits\Invite')) {
|
|
$referrer_id = \AKSRT\Credits\Invite::get_instance()->get_referrer($user_id);
|
|
}
|
|
if ($referrer_id <= 0) return;
|
|
|
|
$rebate_table = \AKSRT\Core\DBMigrator::table('rebate_log');
|
|
if ($rebate_table === '') return;
|
|
|
|
global $wpdb;
|
|
foreach ($items as $item) {
|
|
$pid = (int) $item['product_id'];
|
|
$pay_price = (float) $item['pay_price'];
|
|
if ($pid <= 0 || $pay_price <= 0) continue;
|
|
|
|
$rebate_rate = (float) get_post_meta($pid, '_aksrt_rebate_rate', true);
|
|
if ($rebate_rate <= 0 && class_exists('AKSRT\Shop\TermMeta')) {
|
|
$resolved = TermMeta::resolve_inheritance($pid, 'rebate');
|
|
$rebate_rate = (float) ($resolved ?: 0);
|
|
}
|
|
if ($rebate_rate <= 0) continue;
|
|
|
|
$amount = round($pay_price * $rebate_rate / 100, 2);
|
|
if ($amount <= 0) continue;
|
|
|
|
$wpdb->insert($rebate_table, [
|
|
'user_id' => $referrer_id,
|
|
'order_number' => $order_number,
|
|
'source_user_id' => $user_id,
|
|
'amount' => $amount,
|
|
'type' => 'order',
|
|
'status' => 'pending',
|
|
'created_at' => current_time('mysql'),
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function process_gifts(string $order_number, array $order, array $items): void {
|
|
$user_id = (int) ($order['user_id'] ?? 0);
|
|
if ($user_id <= 0) return;
|
|
$claim_key = '_aksrt_shop_gifts_' . sanitize_key($order_number);
|
|
if (!add_user_meta($user_id, $claim_key, 'processing', true)) return;
|
|
|
|
foreach ($items as $item) {
|
|
$gifts = $item['gift_data'] ?? [];
|
|
if (empty($gifts)) continue;
|
|
|
|
foreach ($gifts as $gift) {
|
|
$gift_type = (string) ($gift['type'] ?? '');
|
|
$gift_value = $gift['value'] ?? 0;
|
|
|
|
if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) {
|
|
\AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number));
|
|
} elseif (in_array($gift_type, ['level_integral', 'experience'], true) && class_exists('\AKSRT\User\Level') && (int) $gift_value > 0) {
|
|
\AKSRT\User\Level::add($user_id, 'shop_gift', (int) $gift_value, true);
|
|
} elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) {
|
|
$current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
|
|
$base = max($current, time());
|
|
update_user_meta($user_id, 'aksrt_vip_expires', $base + ((int) $gift_value * DAY_IN_SECONDS));
|
|
// 赠送 VIP 天数时确保至少 VIP1;不降级已有 VIP2。
|
|
$level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
|
|
if ($level < 1) {
|
|
update_user_meta($user_id, 'aksrt_vip_level', 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
update_user_meta($user_id, $claim_key, '1');
|
|
}
|
|
}
|