feat(payment): rebuild order details and asset records

This commit is contained in:
2026-10-05 16:42:25 +08:00
parent 476e2f961d
commit 7b55cfb190
57 changed files with 2080 additions and 1294 deletions
+115 -33
View File
@@ -193,8 +193,31 @@ trait OrderRepositoryAdmin {
return ['items' => [], 'total' => 0];
}
$where = ['product_type = %s', 'product_id > 0'];
$params = [$product_type];
$content_types = [
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_READING,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_DOWNLOAD,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_IMAGE,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_VIDEO,
];
$legacy_pay_types = [
\AKSRT\Core\DomainModel::ORDER_PRODUCT_READING => \AKSRT\Core\DomainModel::PAY_TYPE_READING,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_DOWNLOAD => \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_IMAGE => \AKSRT\Core\DomainModel::PAY_TYPE_GALLERY,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_VIDEO => \AKSRT\Core\DomainModel::PAY_TYPE_VIDEO,
];
$where = ['product_id > 0'];
$params = [];
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE) {
$where[] = 'product_type IN (' . implode(', ', array_fill(0, count($content_types), '%s')) . ')';
array_push($params, ...$content_types);
} elseif (isset($legacy_pay_types[$product_type])) {
$where[] = "(product_type = %s OR (product_type = %s AND note LIKE %s))";
array_push($params, $product_type, \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, '%"pay_type":"' . $wpdb->esc_like($legacy_pay_types[$product_type]) . '"%');
} else {
$where[] = 'product_type = %s';
$params[] = $product_type;
}
$status_placeholders = implode(', ', array_fill(0, count($status_in), '%s'));
$where[] = "status IN ({$status_placeholders})";
@@ -236,14 +259,14 @@ trait OrderRepositoryAdmin {
$offset = max(0, (int) ($args['offset'] ?? 0));
$list_sql = "SELECT product_id,
product_type,
MIN(product_type) AS product_type,
currency,
COUNT(*) AS sales_count,
AVG(amount) AS avg_amount,
SUM(amount) AS revenue
FROM {$table}
{$where_sql}
GROUP BY product_id, product_type, currency
GROUP BY product_id, currency
ORDER BY {$orderby_col} {$order_dir}
LIMIT %d OFFSET %d";
$list_params = $params;
@@ -252,6 +275,7 @@ trait OrderRepositoryAdmin {
$rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
$items = [];
$aggregate = [];
foreach ((array) $rows as $row) {
$sales_count = max(1, (int) ($row['sales_count'] ?? 0));
$is_points = (string) ($row['currency'] ?? '') === 'points';
@@ -269,12 +293,37 @@ trait OrderRepositoryAdmin {
intdiv($revenue_minor + intdiv($sales_count, 2), $sales_count)
);
}
$items[] = [
$pid = (int) ($row['product_id'] ?? 0);
if (!isset($aggregate[$pid])) {
$aggregate[$pid] = [
'product_id' => (int) ($row['product_id'] ?? 0),
'product_type' => (string) ($row['product_type'] ?? $product_type),
'sales_count' => (int) ($row['sales_count'] ?? 0),
'avg_amount' => $avg_amount,
'revenue' => $revenue,
'product_type' => $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE ? $product_type : (string) ($row['product_type'] ?? $product_type),
'sales_count' => 0,
'avg_amount' => 0,
'revenue_minor' => 0,
'currency' => (string) ($row['currency'] ?? 'CNY'),
];
}
$aggregate[$pid]['sales_count'] += (int) ($row['sales_count'] ?? 0);
if ((string) ($row['currency'] ?? '') === 'points') {
$aggregate[$pid]['revenue_minor'] += (int) $revenue;
} else {
$aggregate[$pid]['revenue_minor'] += \AKSRT\Ledger\AssetAmount::balance_minor((string) $revenue);
}
}
foreach ($aggregate as $item) {
$count = max(1, (int) $item['sales_count']);
$is_points = $item['currency'] === 'points';
$revenue = $is_points
? (int) $item['revenue_minor']
: \AKSRT\Ledger\AssetAmount::decimal_from_minor((int) $item['revenue_minor']);
$average_minor = intdiv((int) $item['revenue_minor'] + intdiv($count, 2), $count);
$items[] = [
'product_id' => (int) $item['product_id'],
'product_type' => (string) $item['product_type'],
'sales_count' => (int) $item['sales_count'],
'avg_amount' => $is_points ? $average_minor : \AKSRT\Ledger\AssetAmount::decimal_from_minor($average_minor),
'revenue' => $revenue,
];
}
@@ -344,17 +393,39 @@ trait OrderRepositoryAdmin {
$status = sanitize_key((string) ($args['status'] ?? ''));
$valid_statuses = array_keys(\AKSRT\Core\DomainModel::order_status_labels());
if ($status !== '' && $status !== 'all' && in_array($status, $valid_statuses, true)) {
$where[] = 'status = %s';
$params[] = $status;
if ($status === 'paid') {
$where[] = "status IN (%s, 'refunding')";
$params[] = $status;
} else {
$where[] = 'status = %s';
$params[] = $status;
}
} else {
$where[] = 'status IN (' . implode(', ', array_fill(0, count($valid_statuses), '%s')) . ')';
array_push($params, ...$valid_statuses);
$where[] = "status IN (" . implode(', ', array_fill(0, count($valid_statuses), '%s')) . ", 'refunding')";
array_push($params, ...$valid_statuses);
}
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
if ($product_type !== '') {
$where[] = 'product_type = %s';
$params[] = $product_type;
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE) {
$types = \AKSRT\Core\DomainModel::resource_order_product_types();
$where[] = 'product_type IN (' . implode(', ', array_fill(0, count($types), '%s')) . ')';
array_push($params, ...$types);
} else {
$legacy_pay_types = [
\AKSRT\Core\DomainModel::ORDER_PRODUCT_READING => \AKSRT\Core\DomainModel::PAY_TYPE_READING,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_DOWNLOAD => \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_IMAGE => \AKSRT\Core\DomainModel::PAY_TYPE_GALLERY,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_VIDEO => \AKSRT\Core\DomainModel::PAY_TYPE_VIDEO,
];
if (isset($legacy_pay_types[$product_type])) {
$where[] = "(product_type = %s OR (product_type = %s AND note LIKE %s))";
array_push($params, $product_type, \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, '%"pay_type":"' . $wpdb->esc_like($legacy_pay_types[$product_type]) . '"%');
} else {
$where[] = 'product_type = %s';
$params[] = $product_type;
}
}
}
$product_id = (int) ($args['product_id'] ?? 0);
@@ -383,24 +454,35 @@ trait OrderRepositoryAdmin {
}
if ($search !== '') {
$flow_table = \AKSRT\Core\DBMigrator::table('financial_flow');
$flow_match = "EXISTS (SELECT 1 FROM {$flow_table} ff WHERE ff.order_number = " . $this->table() . '.order_number AND ff.flow_number LIKE %s)';
// Numeric search may mean user_id or product_id; also match order or flow number / type.
if (ctype_digit($search)) {
$where[] = "(order_number LIKE %s OR product_type LIKE %s OR user_id = %d OR product_id = %d OR {$flow_match})";
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
$params[] = (int) $search;
$params[] = (int) $search;
$params[] = $like;
} else {
$where[] = "(order_number LIKE %s OR product_type LIKE %s OR payment_method LIKE %s OR {$flow_match})";
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
$params[] = $like;
$params[] = $like;
$user_table = $wpdb->users;
$user_match = "EXISTS (SELECT 1 FROM {$user_table} u WHERE u.ID = " . $this->table() . '.user_id AND (u.user_login LIKE %s OR u.user_email LIKE %s OR u.display_name LIKE %s))';
$post_table = $wpdb->posts;
$product_match = "EXISTS (SELECT 1 FROM {$post_table} p WHERE p.ID = " . $this->table() . '.product_id AND p.post_title LIKE %s)';
$meta_match = "note LIKE %s";
// Search matches the public order number, account, product title, and stored product label.
if (ctype_digit($search)) {
$where[] = "(order_number LIKE %s OR product_type LIKE %s OR user_id = %d OR product_id = %d OR {$user_match} OR {$product_match} OR {$meta_match})";
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
$params[] = (int) $search;
$params[] = (int) $search;
array_push($params, $like, $like, $like, $like, $like);
} else {
$where[] = "(order_number LIKE %s OR product_type LIKE %s OR payment_method LIKE %s OR {$user_match} OR {$product_match} OR {$meta_match})";
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
$params[] = $like;
array_push($params, $like, $like, $like, $like, $like);
}
}
foreach (['date_from' => '>=', 'date_to' => '<='] as $key => $operator) {
$date = sanitize_text_field((string) ($args[$key] ?? ''));
if ($date !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$where[] = 'created_at ' . $operator . ' %s';
$params[] = $operator === '>=' ? $date . ' 00:00:00' : $date . ' 23:59:59';
}
}