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:
@@ -0,0 +1,481 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Payment;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait OrderRepositoryCore {
|
||||
|
||||
|
||||
|
||||
private function table(): string {
|
||||
return \AKSRT\Core\DBMigrator::table('order');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* CRUD
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 创建订单(写入表)
|
||||
*/
|
||||
public function create(array $data): string {
|
||||
global $wpdb;
|
||||
|
||||
$order_number = $data['order_number'] ?? $this->generate_order_number();
|
||||
|
||||
$wpdb->insert($this->table(), [
|
||||
'order_number' => $order_number,
|
||||
'user_id' => (int) ($data['user_id'] ?? 0),
|
||||
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
|
||||
'product_id' => (int) ($data['product_id'] ?? 0),
|
||||
'amount' => (float) ($data['amount'] ?? 0),
|
||||
'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')),
|
||||
'status' => \AKSRT\Core\DomainModel::normalize_order_status((string) ($data['status'] ?? \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING)),
|
||||
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
|
||||
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
|
||||
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
||||
'paid_at' => $data['paid_at'] ?? null,
|
||||
'expires_at' => $data['expires_at'] ?? null,
|
||||
'note' => sanitize_textarea_field((string) ($data['note'] ?? '')),
|
||||
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
|
||||
if ($wpdb->insert_id) {
|
||||
return $order_number;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取单个订单
|
||||
*/
|
||||
public function get(string $order_number): ?array {
|
||||
global $wpdb;
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE order_number = %s",
|
||||
$order_number
|
||||
), ARRAY_A);
|
||||
|
||||
if ($row) {
|
||||
return $this->normalize_row($row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
public function get_user_orders(int $user_id, array $args = []): array {
|
||||
global $wpdb;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||
if ($product_type !== '') {
|
||||
$where[] = 'product_type = %s';
|
||||
$params[] = $product_type;
|
||||
}
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
return array_map([$this, 'normalize_row'], (array) $rows);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取单个用户订单。
|
||||
*/
|
||||
public function get_user_order(int $user_id, string $order_number): ?array {
|
||||
$order = $this->get($order_number);
|
||||
if (!$order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user_id > 0 && (int) ($order['user_id'] ?? 0) !== $user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户订单总数。
|
||||
*/
|
||||
public function count_user_orders(int $user_id, array $args = []): int {
|
||||
global $wpdb;
|
||||
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$where = ['user_id = %d'];
|
||||
$params = [$user_id];
|
||||
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||||
if ($product_type !== '') {
|
||||
$where[] = 'product_type = %s';
|
||||
$params[] = $product_type;
|
||||
}
|
||||
|
||||
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
||||
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||||
|
||||
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function update_status(string $order_number, string $new_status, array $extra = []): bool {
|
||||
global $wpdb;
|
||||
|
||||
$valid = \AKSRT\Core\DomainModel::order_status_labels();
|
||||
if (!isset($valid[$new_status])) return false;
|
||||
|
||||
$order = $this->get($order_number);
|
||||
if (!$order) return false;
|
||||
|
||||
$current_status = $order['status'] ?? 'created';
|
||||
|
||||
if (empty($extra['force']) && !\AKSRT\Core\DomainModel::can_transition_order_status($current_status, $new_status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$update_data = ['status' => $new_status];
|
||||
$format = ['%s'];
|
||||
|
||||
$time_fields = [
|
||||
'paid' => 'paid_at',
|
||||
'delivered' => 'delivered_at',
|
||||
'completed' => 'completed_at',
|
||||
'cancelled' => 'cancelled_at',
|
||||
'refunded' => 'refunded_at',
|
||||
];
|
||||
|
||||
foreach ($time_fields as $status_key => $field) {
|
||||
if ($new_status === $status_key) {
|
||||
$update_data[$field] = current_time('mysql');
|
||||
$format[] = '%s';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($extra['payment_method'])) {
|
||||
$update_data['payment_method'] = sanitize_key($extra['payment_method']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($extra['payment_transaction_id'])) {
|
||||
$update_data['payment_transaction_id'] = sanitize_text_field($extra['payment_transaction_id']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($extra['note'])) {
|
||||
// Preserve JSON pay meta (coupon/topic_pay/content_pay/…); stash human text as status_note.
|
||||
$status_note = sanitize_textarea_field((string) $extra['note']);
|
||||
$raw_note = isset($order['note']) && is_string($order['note']) ? $order['note'] : '';
|
||||
$decoded_note = $raw_note !== '' ? json_decode($raw_note, true) : null;
|
||||
if (is_array($decoded_note)) {
|
||||
$decoded_note['status_note'] = $status_note;
|
||||
$merged = wp_json_encode($decoded_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$update_data['note'] = is_string($merged) ? $merged : $status_note;
|
||||
} else {
|
||||
$update_data['note'] = $status_note;
|
||||
}
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
$result = $wpdb->update($this->table(), $update_data, ['order_number' => $order_number], $format, ['%s']);
|
||||
|
||||
if ($result !== false) {
|
||||
// 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。
|
||||
// 仅在「首次」转为 paid 时触发,避免回调多次到达重复履约。
|
||||
if ($new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID && $current_status !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||
$paid_order = array_merge($order, [
|
||||
'status' => $new_status,
|
||||
'payment_method' => $update_data['payment_method'] ?? ($order['payment_method'] ?? ''),
|
||||
'payment_transaction_id' => $update_data['payment_transaction_id'] ?? ($order['payment_transaction_id'] ?? ''),
|
||||
]);
|
||||
if (isset($update_data['note'])) {
|
||||
// Keep pre-update JSON payload for listeners (coupon burn etc.);
|
||||
// only attach status_note if callers mutated note.
|
||||
$pre_meta = [];
|
||||
$pre_raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : '';
|
||||
if ($pre_raw !== '') {
|
||||
$pre_decoded = json_decode($pre_raw, true);
|
||||
if (is_array($pre_decoded)) {
|
||||
$pre_meta = $pre_decoded;
|
||||
}
|
||||
}
|
||||
if ($pre_meta !== []) {
|
||||
$paid_order['meta_payload'] = $pre_meta;
|
||||
// Prefer merged DB note (JSON + status_note) for downstream reads.
|
||||
$paid_order['note'] = $update_data['note'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单到账。
|
||||
*
|
||||
* @param string $order_number 订单号
|
||||
* @param array $paid_order 订单数据(status=paid)
|
||||
*/
|
||||
do_action('aksrt_order_paid', $order_number, $paid_order);
|
||||
}
|
||||
}
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Delete closed (cancelled/expired) orders older than $days days.
|
||||
* Mirrors zibll ZibPay::clear_order(14).
|
||||
*
|
||||
* @return int Number of rows deleted.
|
||||
*/
|
||||
public function clear_closed_orders(int $days = 14): int {
|
||||
global $wpdb;
|
||||
|
||||
$days = max(1, min(3650, $days));
|
||||
$table = $this->table();
|
||||
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
||||
// Prefer cancelled_at when set; fall back to created_at for older rows.
|
||||
$sql = "DELETE FROM {$table}
|
||||
WHERE status IN ('cancelled', 'expired')
|
||||
AND COALESCE(NULLIF(cancelled_at, ''), created_at) < %s";
|
||||
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
|
||||
return $deleted === false ? 0 : (int) $deleted;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Hard-delete a single order by order_number (admin hygiene).
|
||||
*/
|
||||
public function delete_order(string $order_number): bool {
|
||||
global $wpdb;
|
||||
$order_number = sanitize_text_field($order_number);
|
||||
if ($order_number === '') {
|
||||
return false;
|
||||
}
|
||||
$ok = $wpdb->delete($this->table(), ['order_number' => $order_number], ['%s']);
|
||||
return $ok !== false && $ok > 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取订单统计
|
||||
*/
|
||||
public function get_stats(int $user_id = 0): array {
|
||||
global $wpdb;
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if ($user_id > 0) {
|
||||
$where = 'WHERE user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
|
||||
$sql = "SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
|
||||
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as delivered,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
|
||||
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded,
|
||||
SUM(CASE WHEN status IN ('paid','delivered','completed') THEN amount ELSE 0 END) as revenue
|
||||
FROM {$this->table()} {$where}";
|
||||
|
||||
if (!empty($params)) {
|
||||
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
} else {
|
||||
$row = $wpdb->get_row($sql, ARRAY_A);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'pending' => (int) ($row['pending'] ?? 0),
|
||||
'paid' => (int) ($row['paid'] ?? 0),
|
||||
'delivered' => (int) ($row['delivered'] ?? 0),
|
||||
'completed' => (int) ($row['completed'] ?? 0),
|
||||
'cancelled' => (int) ($row['cancelled'] ?? 0),
|
||||
'refunded' => (int) ($row['refunded'] ?? 0),
|
||||
'revenue' => (float) ($row['revenue'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function has_paid_access(int $user_id, string $product_type, int $product_id): bool {
|
||||
if ($user_id <= 0 || $product_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null !== $this->find_latest_success_order($user_id, $product_type, $product_id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function get_valid_vip_order(int $user_id): ?array {
|
||||
return $this->find_latest_success_order($user_id, \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP, 0, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Migration bridge: option → table
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 一次性迁移:把 option 中的订单搬到表中。
|
||||
* 幂等 — 已存在的 order_number 不会重复插入。
|
||||
*/
|
||||
public function migrate_from_option(): int {
|
||||
global $wpdb;
|
||||
|
||||
$orders = get_option('aksrt_orders', []);
|
||||
if (!is_array($orders) || empty($orders)) return 0;
|
||||
|
||||
$migrated = 0;
|
||||
foreach ($orders as $order_number => $data) {
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE order_number = %s",
|
||||
$order_number
|
||||
));
|
||||
if ($existing) continue;
|
||||
|
||||
$wpdb->insert($this->table(), [
|
||||
'order_number' => $order_number,
|
||||
'user_id' => (int) ($data['user_id'] ?? 0),
|
||||
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
|
||||
'product_id' => (int) ($data['product_id'] ?? 0),
|
||||
'amount' => (float) ($data['amount'] ?? 0),
|
||||
'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')),
|
||||
'status' => \AKSRT\Core\DomainModel::normalize_order_status((string) ($data['status'] ?? \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING)),
|
||||
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
|
||||
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
||||
'paid_at' => $data['paid_at'] ?? null,
|
||||
'delivered_at' => $data['delivered_at'] ?? null,
|
||||
'completed_at' => $data['completed_at'] ?? null,
|
||||
'cancelled_at' => $data['cancelled_at'] ?? null,
|
||||
'refunded_at' => $data['refunded_at'] ?? null,
|
||||
'expires_at' => $data['expires_at'] ?? null,
|
||||
'note' => '',
|
||||
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
|
||||
if ($wpdb->insert_id) $migrated++;
|
||||
}
|
||||
|
||||
update_option(\AKSRT\Core\DomainModel::ORDER_MIGRATED_OPTION, time());
|
||||
|
||||
return $migrated;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Internal helpers
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
private function normalize_row(array $row): array {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['user_id'] = (int) $row['user_id'];
|
||||
$row['product_id'] = (int) $row['product_id'];
|
||||
$row['amount'] = (float) $row['amount'];
|
||||
$row['status'] = \AKSRT\Core\DomainModel::normalize_order_status((string) ($row['status'] ?? ''));
|
||||
|
||||
$row['meta_payload'] = [];
|
||||
if (!empty($row['note']) && is_string($row['note'])) {
|
||||
$decoded = json_decode($row['note'], true);
|
||||
if (is_array($decoded)) {
|
||||
$row['meta_payload'] = $decoded;
|
||||
if (isset($decoded['customer_note']) && is_string($decoded['customer_note'])) {
|
||||
$row['note_text'] = $decoded['customer_note'];
|
||||
}
|
||||
} else {
|
||||
$row['note_text'] = $row['note'];
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function find_latest_success_order(int $user_id, string $product_type, int $product_id = 0, bool $ignore_product_id = false): ?array {
|
||||
global $wpdb;
|
||||
|
||||
if ($user_id <= 0 || $product_type === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$status_placeholders = implode(', ', array_fill(0, count(\AKSRT\Core\DomainModel::paid_order_statuses()), '%s'));
|
||||
$params = array_merge(
|
||||
[$user_id, sanitize_key($product_type)],
|
||||
$ignore_product_id ? [] : [$product_id],
|
||||
\AKSRT\Core\DomainModel::paid_order_statuses()
|
||||
);
|
||||
|
||||
$product_sql = $ignore_product_id ? '' : ' AND product_id = %d';
|
||||
$sql = "SELECT * FROM {$this->table()}
|
||||
WHERE user_id = %d
|
||||
AND product_type = %s{$product_sql}
|
||||
AND status IN ({$status_placeholders})
|
||||
ORDER BY COALESCE(paid_at, created_at) DESC, id DESC
|
||||
LIMIT 1";
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
return is_array($row) ? $this->normalize_row($row) : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function generate_order_number(): string {
|
||||
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user