599 lines
22 KiB
PHP
599 lines
22 KiB
PHP
<?php
|
||
/**
|
||
* @package AKSRT
|
||
*/
|
||
|
||
namespace AKSRT\Payment;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
trait OrderRepositoryAdmin {
|
||
|
||
|
||
|
||
/**
|
||
* Build a stable admin-facing order context payload for workbench pages.
|
||
*/
|
||
public function get_admin_order_context(string $order_number): array {
|
||
$order_number = sanitize_text_field($order_number);
|
||
$contexts = $this->get_admin_order_context_map([$order_number]);
|
||
|
||
return $contexts[$order_number] ?? $this->get_empty_admin_order_context($order_number);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Build admin-facing order context payloads in bulk to avoid per-row order queries.
|
||
*
|
||
* @param array<int, string> $order_numbers
|
||
* @return array<string, array<string, mixed>>
|
||
*/
|
||
public function get_admin_order_context_map(array $order_numbers): array {
|
||
global $wpdb;
|
||
|
||
$order_numbers = array_values(array_unique(array_filter(array_map(
|
||
static function ($order_number): string {
|
||
return sanitize_text_field((string) $order_number);
|
||
},
|
||
$order_numbers
|
||
))));
|
||
|
||
if (empty($order_numbers)) {
|
||
return [];
|
||
}
|
||
|
||
$contexts = [];
|
||
foreach ($order_numbers as $order_number) {
|
||
$contexts[$order_number] = $this->get_empty_admin_order_context($order_number);
|
||
}
|
||
|
||
$placeholders = implode(', ', array_fill(0, count($order_numbers), '%s'));
|
||
$sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders})";
|
||
$rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A);
|
||
|
||
if (empty($rows)) {
|
||
return $contexts;
|
||
}
|
||
|
||
$orders = [];
|
||
foreach ((array) $rows as $row) {
|
||
$order = $this->normalize_row($row);
|
||
$orders[(string) ($order['order_number'] ?? '')] = $order;
|
||
}
|
||
|
||
$resolved_order_numbers = array_keys($orders);
|
||
|
||
foreach ($orders as $order_number => $order) {
|
||
$contexts[$order_number] = $this->build_admin_order_context($order, []);
|
||
}
|
||
|
||
return $contexts;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 更新订单状态(带状态机验证)
|
||
*/
|
||
/**
|
||
* Merge operator admin note into order note meta without destroying JSON payload.
|
||
*/
|
||
public function update_admin_note(string $order_number, string $admin_note): bool {
|
||
global $wpdb;
|
||
|
||
$order = $this->get($order_number);
|
||
if (!$order) {
|
||
return false;
|
||
}
|
||
|
||
$raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : '';
|
||
$decoded = $raw !== '' ? json_decode($raw, true) : null;
|
||
$admin_note = sanitize_textarea_field($admin_note);
|
||
|
||
if (is_array($decoded)) {
|
||
$decoded['admin_note'] = $admin_note;
|
||
$new_note = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
} elseif ($raw !== '') {
|
||
$new_note = wp_json_encode([
|
||
'customer_note' => $raw,
|
||
'admin_note' => $admin_note,
|
||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
} else {
|
||
$new_note = wp_json_encode([
|
||
'admin_note' => $admin_note,
|
||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
}
|
||
|
||
$result = $wpdb->update(
|
||
$this->table(),
|
||
['note' => $new_note],
|
||
['order_number' => $order_number],
|
||
['%s'],
|
||
['%s']
|
||
);
|
||
|
||
return $result !== false;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 管理后台订单列表 — 支持 search(order_number / product_type 模糊匹配)
|
||
*/
|
||
|
||
public function get_orders_admin(array $args = [], string $search = ''): array {
|
||
global $wpdb;
|
||
|
||
$this->expire_pending_orders();
|
||
[$where, $params] = $this->build_admin_order_filters($args, $search);
|
||
|
||
$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);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 管理后台订单计数 — 与 get_orders_admin 共用过滤条件
|
||
*/
|
||
public function get_orders_count(array $args = [], string $search = ''): int {
|
||
global $wpdb;
|
||
|
||
$this->expire_pending_orders();
|
||
[$where, $params] = $this->build_admin_order_filters($args, $search);
|
||
|
||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
||
|
||
if (!empty($params)) {
|
||
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
|
||
}
|
||
return (int) $wpdb->get_var($sql);
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Aggregate paid content (resource) sales by product_id (post ID).
|
||
*
|
||
* @param array{
|
||
* product_type?: string,
|
||
* product_ids?: int[],
|
||
* status_in?: string[],
|
||
* limit?: int,
|
||
* offset?: int,
|
||
* orderby?: string,
|
||
* order?: string
|
||
* } $args
|
||
* @return array{items: list<array{product_id:int,product_type:string,sales_count:int,avg_amount:int|string,revenue:int|string}>, total: int}
|
||
*/
|
||
public function get_content_sales_summary(array $args = []): array {
|
||
global $wpdb;
|
||
|
||
$product_type = sanitize_key((string) ($args['product_type'] ?? \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE));
|
||
if ($product_type === '') {
|
||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE;
|
||
}
|
||
|
||
$status_in = $args['status_in'] ?? \AKSRT\Core\DomainModel::paid_order_statuses();
|
||
if (!is_array($status_in) || empty($status_in)) {
|
||
$status_in = \AKSRT\Core\DomainModel::paid_order_statuses();
|
||
}
|
||
$status_in = array_values(array_filter(array_map('sanitize_key', $status_in)));
|
||
if (empty($status_in)) {
|
||
return ['items' => [], 'total' => 0];
|
||
}
|
||
|
||
$where = ['product_type = %s', 'product_id > 0'];
|
||
$params = [$product_type];
|
||
|
||
$status_placeholders = implode(', ', array_fill(0, count($status_in), '%s'));
|
||
$where[] = "status IN ({$status_placeholders})";
|
||
foreach ($status_in as $st) {
|
||
$params[] = $st;
|
||
}
|
||
|
||
$product_ids = $args['product_ids'] ?? [];
|
||
if (is_array($product_ids) && !empty($product_ids)) {
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) {
|
||
return $id > 0;
|
||
})));
|
||
if (!empty($ids)) {
|
||
$id_ph = implode(', ', array_fill(0, count($ids), '%d'));
|
||
$where[] = "product_id IN ({$id_ph})";
|
||
foreach ($ids as $id) {
|
||
$params[] = $id;
|
||
}
|
||
}
|
||
}
|
||
|
||
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
||
$table = $this->table();
|
||
|
||
$total_sql = "SELECT COUNT(DISTINCT product_id) FROM {$table} {$where_sql}";
|
||
$total = (int) $wpdb->get_var($wpdb->prepare($total_sql, $params));
|
||
|
||
$orderby_map = [
|
||
'sales_count' => 'sales_count',
|
||
'revenue' => 'revenue',
|
||
'avg_amount' => 'avg_amount',
|
||
'product_id' => 'product_id',
|
||
];
|
||
$orderby_key = sanitize_key((string) ($args['orderby'] ?? 'sales_count'));
|
||
$orderby_col = $orderby_map[$orderby_key] ?? 'sales_count';
|
||
$order_dir = strtoupper((string) ($args['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
|
||
|
||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||
|
||
$list_sql = "SELECT product_id,
|
||
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
|
||
ORDER BY {$orderby_col} {$order_dir}
|
||
LIMIT %d OFFSET %d";
|
||
$list_params = $params;
|
||
$list_params[] = $limit;
|
||
$list_params[] = $offset;
|
||
|
||
$rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
|
||
$items = [];
|
||
foreach ((array) $rows as $row) {
|
||
$sales_count = max(1, (int) ($row['sales_count'] ?? 0));
|
||
$is_points = (string) ($row['currency'] ?? '') === 'points';
|
||
if ($is_points) {
|
||
$revenue = max(0, (int) ($row['revenue'] ?? 0));
|
||
$avg_amount = intdiv($revenue + intdiv($sales_count, 2), $sales_count);
|
||
} else {
|
||
try {
|
||
$revenue_minor = \AKSRT\Ledger\AssetAmount::balance_minor((string) ($row['revenue'] ?? '0'));
|
||
} catch (\InvalidArgumentException $e) {
|
||
$revenue_minor = 0;
|
||
}
|
||
$revenue = \AKSRT\Ledger\AssetAmount::decimal_from_minor($revenue_minor);
|
||
$avg_amount = \AKSRT\Ledger\AssetAmount::decimal_from_minor(
|
||
intdiv($revenue_minor + intdiv($sales_count, 2), $sales_count)
|
||
);
|
||
}
|
||
$items[] = [
|
||
'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,
|
||
];
|
||
}
|
||
|
||
return [
|
||
'items' => $items,
|
||
'total' => $total,
|
||
];
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Paid order totals for a set of product IDs (resource content).
|
||
*
|
||
* @param int[] $product_ids
|
||
* @return array<int, array{sales_count:int,avg_amount:int|string,revenue:int|string}>
|
||
*/
|
||
public function get_content_sales_map(array $product_ids, string $product_type = ''): array {
|
||
$ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) {
|
||
return $id > 0;
|
||
})));
|
||
if (empty($ids)) {
|
||
return [];
|
||
}
|
||
|
||
if ($product_type === '') {
|
||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE;
|
||
}
|
||
|
||
$summary = $this->get_content_sales_summary([
|
||
'product_type' => $product_type,
|
||
'product_ids' => $ids,
|
||
'limit' => max(count($ids), 1),
|
||
'offset' => 0,
|
||
'orderby' => 'product_id',
|
||
'order' => 'ASC',
|
||
]);
|
||
|
||
$map = [];
|
||
foreach ($summary['items'] as $item) {
|
||
$pid = (int) ($item['product_id'] ?? 0);
|
||
if ($pid <= 0) {
|
||
continue;
|
||
}
|
||
$map[$pid] = [
|
||
'sales_count' => (int) ($item['sales_count'] ?? 0),
|
||
'avg_amount' => $item['avg_amount'] ?? '0.00',
|
||
'revenue' => $item['revenue'] ?? '0.00',
|
||
];
|
||
}
|
||
return $map;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Shared WHERE builder for admin order list/count.
|
||
*
|
||
* @return array{0: string[], 1: array<int, mixed>}
|
||
*/
|
||
private function build_admin_order_filters(array $args, string $search = ''): array {
|
||
global $wpdb;
|
||
|
||
$where = [];
|
||
$params = [];
|
||
|
||
$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;
|
||
} else {
|
||
$where[] = 'status IN (' . implode(', ', array_fill(0, count($valid_statuses), '%s')) . ')';
|
||
array_push($params, ...$valid_statuses);
|
||
}
|
||
|
||
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
|
||
if ($product_type !== '') {
|
||
$where[] = 'product_type = %s';
|
||
$params[] = $product_type;
|
||
}
|
||
|
||
$product_id = (int) ($args['product_id'] ?? 0);
|
||
if ($product_id > 0) {
|
||
$where[] = 'product_id = %d';
|
||
$params[] = $product_id;
|
||
}
|
||
|
||
$user_id = (int) ($args['user_id'] ?? 0);
|
||
if ($user_id > 0) {
|
||
$where[] = 'user_id = %d';
|
||
$params[] = $user_id;
|
||
}
|
||
|
||
if (array_key_exists('payment_methods', $args)) {
|
||
$payment_methods = array_values(array_unique(array_filter(array_map(
|
||
static fn($method): string => sanitize_key((string) $method),
|
||
(array) $args['payment_methods']
|
||
))));
|
||
if ($payment_methods === []) {
|
||
$where[] = '1 = 0';
|
||
} else {
|
||
$where[] = 'payment_method IN (' . implode(', ', array_fill(0, count($payment_methods), '%s')) . ')';
|
||
array_push($params, ...$payment_methods);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
return [$where, $params];
|
||
}
|
||
|
||
|
||
|
||
private function get_empty_admin_order_context(string $order_number): array {
|
||
return [
|
||
'exists' => false,
|
||
'summary' => [
|
||
'order_number' => $order_number,
|
||
'status' => '',
|
||
'status_label' => '',
|
||
'amount' => '0.00',
|
||
'currency_symbol' => '¥',
|
||
'created_at' => '',
|
||
'paid_at' => '',
|
||
],
|
||
'buyer' => [],
|
||
'address' => [],
|
||
'payment_method' => [],
|
||
'shipping_method' => [],
|
||
'shipping' => [
|
||
'count' => 0,
|
||
'latest' => [],
|
||
],
|
||
'after_sale' => [
|
||
'count' => 0,
|
||
'latest' => [],
|
||
],
|
||
'coupon' => [],
|
||
'pricing' => [
|
||
'subtotal' => '0.00',
|
||
'shipping_fee' => '0.00',
|
||
'discount' => '0.00',
|
||
'amount' => '0.00',
|
||
],
|
||
'items' => [],
|
||
'customer_note' => '',
|
||
'raw_order' => null,
|
||
];
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* @param array<string, mixed> $order
|
||
* @param array<string, mixed> $related
|
||
* @return array<string, mixed>
|
||
*/
|
||
private function build_admin_order_context(array $order, array $related = []): array {
|
||
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
|
||
$user = get_userdata((int) ($order['user_id'] ?? 0));
|
||
$status = (string) ($order['status'] ?? '');
|
||
$address = is_array($meta['address'] ?? null) ? $meta['address'] : [];
|
||
$payment_method = is_array($meta['payment_method'] ?? null) ? $meta['payment_method'] : [];
|
||
$shipping_method = is_array($meta['shipping_method'] ?? null) ? $meta['shipping_method'] : [];
|
||
$coupon = is_array($meta['coupon'] ?? null) ? $meta['coupon'] : [];
|
||
$items = is_array($meta['items'] ?? null) ? $meta['items'] : [];
|
||
$currency = (string) ($order['currency'] ?? 'CNY');
|
||
$is_points = $currency === 'points';
|
||
$shipping_minor = $is_points ? max(0, (int) ($meta['shipping_fee'] ?? 0)) : $this->cash_minor((string) ($meta['shipping_fee'] ?? '0'));
|
||
$discount_minor = $is_points ? max(0, (int) ($meta['discount'] ?? 0)) : $this->cash_minor((string) ($meta['discount'] ?? '0'));
|
||
$amount_minor = $is_points ? max(0, (int) ($order['amount'] ?? 0)) : $this->cash_minor((string) ($order['amount'] ?? '0'));
|
||
$fallback_minor = max(0, $amount_minor + $discount_minor - $shipping_minor);
|
||
$subtotal_minor = $this->calculate_order_subtotal_minor($items, $fallback_minor, $is_points);
|
||
$amount = $is_points ? $amount_minor : \AKSRT\Ledger\AssetAmount::decimal_from_minor($amount_minor);
|
||
$shipping_fee = $is_points ? $shipping_minor : \AKSRT\Ledger\AssetAmount::decimal_from_minor($shipping_minor);
|
||
$discount = $is_points ? $discount_minor : \AKSRT\Ledger\AssetAmount::decimal_from_minor($discount_minor);
|
||
$subtotal = $is_points ? $subtotal_minor : \AKSRT\Ledger\AssetAmount::decimal_from_minor($subtotal_minor);
|
||
|
||
$address['full_text'] = $this->build_address_text($address);
|
||
|
||
return [
|
||
'exists' => true,
|
||
'summary' => [
|
||
'order_number' => (string) ($order['order_number'] ?? ''),
|
||
'status' => $status,
|
||
'status_label' => class_exists('AKSRT\\Payment\\Order') ? (Order::get_valid_statuses()[$status] ?? $status) : $status,
|
||
'amount' => $amount,
|
||
'currency_symbol' => $is_points ? __('积分', 'aksrt') : '¥',
|
||
'created_at' => (string) ($order['created_at'] ?? ''),
|
||
'paid_at' => (string) ($order['paid_at'] ?? ''),
|
||
'product_type' => (string) ($order['product_type'] ?? ''),
|
||
'product_id' => (int) ($order['product_id'] ?? 0),
|
||
],
|
||
'buyer' => [
|
||
'id' => (int) ($order['user_id'] ?? 0),
|
||
'display_name' => $user ? (string) $user->display_name : '',
|
||
'user_login' => $user ? (string) $user->user_login : '',
|
||
'user_email' => $user ? (string) $user->user_email : '',
|
||
],
|
||
'address' => $address,
|
||
'payment_method' => [
|
||
'value' => (string) ($payment_method['value'] ?? $order['payment_method'] ?? ''),
|
||
'label' => (string) ($payment_method['label'] ?? $order['payment_method'] ?? ''),
|
||
],
|
||
'shipping_method' => [
|
||
'value' => (string) ($shipping_method['value'] ?? ''),
|
||
'label' => (string) ($shipping_method['label'] ?? ''),
|
||
'fee' => $shipping_fee,
|
||
],
|
||
'shipping' => [
|
||
'count' => 0,
|
||
'latest' => [],
|
||
],
|
||
'after_sale' => [
|
||
'count' => 0,
|
||
'latest' => [],
|
||
],
|
||
'coupon' => $coupon,
|
||
'pricing' => [
|
||
'subtotal' => $subtotal,
|
||
'shipping_fee' => $shipping_fee,
|
||
'discount' => $discount,
|
||
'amount' => $amount,
|
||
],
|
||
'items' => $items,
|
||
'customer_note' => (string) ($meta['customer_note'] ?? $order['note_text'] ?? ''),
|
||
'raw_order' => $order,
|
||
];
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* @param array<int, array<string, mixed>> $items
|
||
*/
|
||
private function calculate_order_subtotal_minor(array $items, int $fallback, bool $is_points): int {
|
||
if (empty($items)) {
|
||
return max(0, $fallback);
|
||
}
|
||
|
||
$subtotal = 0;
|
||
foreach ($items as $item) {
|
||
if (isset($item['line_total'])) {
|
||
$line_minor = $is_points
|
||
? max(0, (int) $item['line_total'])
|
||
: $this->cash_minor((string) $item['line_total']);
|
||
if ($subtotal > PHP_INT_MAX - $line_minor) {
|
||
return max(0, $fallback);
|
||
}
|
||
$subtotal += $line_minor;
|
||
continue;
|
||
}
|
||
|
||
$price_minor = $is_points
|
||
? max(0, (int) ($item['price'] ?? 0))
|
||
: $this->cash_minor((string) ($item['price'] ?? '0'));
|
||
$quantity = max(1, (int) ($item['quantity'] ?? 1));
|
||
if ($price_minor > intdiv(PHP_INT_MAX, $quantity)
|
||
|| $subtotal > PHP_INT_MAX - ($price_minor * $quantity)
|
||
) {
|
||
return max(0, $fallback);
|
||
}
|
||
$subtotal += $price_minor * $quantity;
|
||
}
|
||
|
||
if ($subtotal <= 0) {
|
||
return max(0, $fallback);
|
||
}
|
||
|
||
return $subtotal;
|
||
}
|
||
|
||
private function cash_minor(string $value): int {
|
||
try {
|
||
return \AKSRT\Ledger\AssetAmount::balance_minor($value);
|
||
} catch (\InvalidArgumentException $e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private function build_address_text(array $address): string {
|
||
$segments = [];
|
||
|
||
foreach (['name', 'phone', 'province', 'city', 'district', 'address'] as $key) {
|
||
$value = trim((string) ($address[$key] ?? ''));
|
||
if ($value !== '') {
|
||
$segments[] = $value;
|
||
}
|
||
}
|
||
|
||
if (!empty($address['full_text']) && is_string($address['full_text'])) {
|
||
return (string) $address['full_text'];
|
||
}
|
||
|
||
return implode(' / ', $segments);
|
||
}
|
||
|
||
}
|