feat(shop): complete storefront archive, seller edit, revenue
Wire product cat/tag archives to settings-driven sort and AJAX paging, seller product options/video/params editing, revenue withdraw form and order review submit — finishing remaining shop frontend gaps after buy-path.
This commit is contained in:
1 parent
5a8f08db8a
commit
f3db6c89cc
17 files changed
+1024
-248
No files matched your search
@@ -354,6 +354,190 @@ trait AKSRT_Admin_Shop_Controller_Frontend_Flow_Operations {
|
||||
return $this->success(['message' => __('已设为默认地址', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function get_frontend_product_archive(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Shop')) {
|
||||
return $this->error('商城服务不可用', 500);
|
||||
}
|
||||
|
||||
$taxonomy = sanitize_key((string) ($request->get_param('taxonomy') ?? ''));
|
||||
$term_id = (int) ($request->get_param('term_id') ?? 0);
|
||||
$page = max(1, (int) ($request->get_param('page') ?? 1));
|
||||
$orderby = sanitize_key((string) ($request->get_param('orderby') ?? ''));
|
||||
|
||||
$view = AKSRT_Shop::get_instance()->get_product_archive_view([
|
||||
'taxonomy' => $taxonomy,
|
||||
'term_id' => $term_id,
|
||||
'paged' => $page,
|
||||
'orderby' => $orderby,
|
||||
]);
|
||||
|
||||
$html = '';
|
||||
if (!empty($view['products']) && is_array($view['products'])) {
|
||||
ob_start();
|
||||
foreach ($view['products'] as $product) {
|
||||
$this->render_product_card_html($product, $view);
|
||||
}
|
||||
$html = (string) ob_get_clean();
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => array_merge($view, [
|
||||
'html' => $html,
|
||||
'has_more' => (int) ($view['page'] ?? 1) < (int) ($view['max_pages'] ?? 1),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
private function render_product_card_html(array $product, array $view = []): void {
|
||||
$sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : [];
|
||||
$mode = (string) ($sales_cfg['show'] ?? 'show');
|
||||
$min_sales = max(0, (int) ($sales_cfg['show_min'] ?? 0));
|
||||
$sales = (int) ($product['sales'] ?? 0);
|
||||
$can_show_sales = false;
|
||||
if ($mode === 'show') {
|
||||
$can_show_sales = $sales > 0;
|
||||
} elseif ($mode === 'threshold') {
|
||||
$can_show_sales = $sales >= $min_sales;
|
||||
}
|
||||
|
||||
$permalink = (string) ($product['permalink'] ?? '#');
|
||||
$title = (string) ($product['title'] ?? '');
|
||||
$image = (string) ($product['image'] ?? '');
|
||||
$price = (float) ($product['price'] ?? 0);
|
||||
$excerpt = (string) ($product['excerpt'] ?? '');
|
||||
?>
|
||||
<article class="aksrt-product-card">
|
||||
<a href="<?php echo esc_url($permalink); ?>" class="aksrt-product-card__link">
|
||||
<?php if ($image !== '') : ?>
|
||||
<div class="aksrt-product-card__image">
|
||||
<img src="<?php echo esc_url($image); ?>" alt="<?php echo esc_attr($title); ?>" loading="lazy">
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="aksrt-product-card__image aksrt-product-card__image--placeholder">
|
||||
<i class="fas fa-image"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="aksrt-product-card__body">
|
||||
<h3 class="aksrt-product-card__title"><?php echo esc_html($title); ?></h3>
|
||||
<?php if ($excerpt !== '') : ?>
|
||||
<p class="aksrt-product-card__excerpt"><?php echo esc_html(wp_trim_words($excerpt, 15)); ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="aksrt-product-card__footer">
|
||||
<span class="aksrt-product-card__price">¥<?php echo esc_html(number_format($price, 2)); ?></span>
|
||||
<?php if ($can_show_sales) : ?>
|
||||
<span class="aksrt-product-card__sales"><?php printf(esc_html__('已售 %d', 'aksrt'), $sales); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function get_frontend_revenue(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!function_exists('aksrt_get_revenue_page_view')) {
|
||||
return $this->error('收益服务不可用', 500);
|
||||
}
|
||||
$view = aksrt_get_revenue_page_view(get_current_user_id());
|
||||
$withdrawals = [];
|
||||
if (class_exists('AKSRT_Withdrawal')) {
|
||||
$rows = AKSRT_Withdrawal::get_instance()->get_user_withdrawals(get_current_user_id(), ['limit' => 20]);
|
||||
foreach ($rows as $row) {
|
||||
$withdrawals[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'amount' => (float) ($row['amount'] ?? 0),
|
||||
'status' => (string) ($row['status'] ?? ''),
|
||||
'method' => (string) ($row['method'] ?? ''),
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'note' => (string) ($row['note'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
$settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_withdrawal_settings() : [];
|
||||
return $this->success([
|
||||
'data' => array_merge($view, [
|
||||
'withdrawals' => $withdrawals,
|
||||
'settings' => [
|
||||
'min_amount' => (float) ($settings['min_amount'] ?? 50),
|
||||
'service_charge_rate' => (float) ($settings['service_charge_rate'] ?? 0),
|
||||
'text' => (string) ($settings['text'] ?? ''),
|
||||
],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create_frontend_withdraw(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Withdrawal') || !class_exists('AKSRT_Credits')) {
|
||||
return $this->error('提现服务不可用', 500);
|
||||
}
|
||||
|
||||
$params = $this->get_json_params($request);
|
||||
$amount = (float) ($params['amount'] ?? 0);
|
||||
$method = sanitize_key((string) ($params['method'] ?? 'alipay'));
|
||||
$account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? ''));
|
||||
|
||||
$settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_withdrawal_settings() : [];
|
||||
$min = (float) ($settings['min_amount'] ?? 50);
|
||||
if ($amount < $min) {
|
||||
return $this->error(sprintf(__('最低提现金额为 %s', 'aksrt'), $min));
|
||||
}
|
||||
if ($account === '') {
|
||||
return $this->error(__('请填写收款账户信息', 'aksrt'));
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$balance = (float) AKSRT_Credits::balance($user_id);
|
||||
if ($amount > $balance) {
|
||||
return $this->error(__('可提现余额不足', 'aksrt'));
|
||||
}
|
||||
|
||||
$cost = (int) round($amount);
|
||||
if ($cost <= 0) {
|
||||
return $this->error(__('提现金额无效', 'aksrt'));
|
||||
}
|
||||
$deducted = AKSRT_Credits::deduct($user_id, $cost, 'withdraw', ['type' => 'withdraw'], __('用户提现申请', 'aksrt'));
|
||||
if (is_wp_error($deducted)) {
|
||||
return $this->error($deducted->get_error_message());
|
||||
}
|
||||
|
||||
$id = AKSRT_Withdrawal::get_instance()->create($user_id, $amount, $method, $account);
|
||||
if ($id <= 0) {
|
||||
AKSRT_Credits::add($user_id, $cost, 'withdraw_refund', ['type' => 'withdraw'], __('提现失败退回', 'aksrt'));
|
||||
return $this->error(__('提现申请创建失败', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('提现申请已提交', 'aksrt'),
|
||||
'data' => [
|
||||
'id' => $id,
|
||||
'amount' => $amount,
|
||||
'balance' => (float) AKSRT_Credits::balance($user_id),
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function submit_frontend_product_review(WP_REST_Request $request): WP_REST_Response {
|
||||
if (!class_exists('AKSRT_Product_Review')) {
|
||||
return $this->error('评价服务不可用', 500);
|
||||
}
|
||||
$params = $this->get_json_params($request);
|
||||
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
|
||||
$product_id = (int) ($params['product_id'] ?? 0);
|
||||
$result = AKSRT_Product_Review::get_instance()->submit_review(
|
||||
get_current_user_id(),
|
||||
$order_number,
|
||||
$product_id,
|
||||
$params
|
||||
);
|
||||
if (empty($result['success'])) {
|
||||
return $this->error($result['message'] ?? __('评价失败', 'aksrt'));
|
||||
}
|
||||
return $this->success([
|
||||
'message' => __('评价已提交', 'aksrt'),
|
||||
'data' => $result,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function create_frontend_product(WP_REST_Request $request): WP_REST_Response {
|
||||
$params = $this->get_json_params($request);
|
||||
$params['author_id'] = get_current_user_id();
|
||||
|
||||
@@ -333,6 +333,10 @@ trait AKSRT_Admin_Shop_Controller_Product_Operations {
|
||||
update_post_meta($post_id, '_aksrt_product_image', $image);
|
||||
}
|
||||
|
||||
if (isset($params['options']) && is_array($params['options']) && class_exists('AKSRT_Shop')) {
|
||||
AKSRT_Shop::get_instance()->save_product_options((int) $post_id, $params['options']);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('商品已创建', 'aksrt'),
|
||||
'data' => ['id' => $post_id],
|
||||
@@ -484,6 +488,10 @@ trait AKSRT_Admin_Shop_Controller_Product_Operations {
|
||||
update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image']));
|
||||
}
|
||||
|
||||
if (isset($params['options']) && is_array($params['options']) && class_exists('AKSRT_Shop')) {
|
||||
AKSRT_Shop::get_instance()->save_product_options($id, $params['options']);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => __('商品已更新', 'aksrt'),
|
||||
]);
|
||||
|
||||
@@ -352,7 +352,31 @@ class AKSRT_Admin_Shop_Controller extends AKSRT_Admin_Controller_Base {
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)/favorite', [
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/archive', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_frontend_product_archive'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_frontend_revenue'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue/withdraw', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_frontend_withdraw'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9]+)/review', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'submit_frontend_product_review'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)/favorite', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'toggle_frontend_product_favorite'],
|
||||
'permission_callback' => [$this, 'check_logged_in_permission'],
|
||||
|
||||
@@ -1000,6 +1000,10 @@ class AKSRT_Settings {
|
||||
return AKSRT_Settings_Post_Display::get_comment_display_settings();
|
||||
}
|
||||
|
||||
public static function get_withdrawal_settings(): array {
|
||||
return AKSRT_Settings_Payment_Runtime::get_withdrawal_settings();
|
||||
}
|
||||
|
||||
/**
|
||||
* pre_get_posts: apply post_list_per_page on archive/tag/category/search/home pages.
|
||||
*
|
||||
|
||||
@@ -152,6 +152,11 @@ class AKSRT_Shop {
|
||||
'after_sale_policy' => '',
|
||||
'support_refund' => true,
|
||||
'status' => 'draft',
|
||||
'options' => [],
|
||||
'pay_modo' => '0',
|
||||
'start_price' => 0,
|
||||
'product_video' => [],
|
||||
'product_params' => '',
|
||||
],
|
||||
'taxonomy' => [
|
||||
'categories' => $this->product_service()->get_term_options('aksrt_product_cat'),
|
||||
@@ -181,6 +186,10 @@ class AKSRT_Shop {
|
||||
}
|
||||
|
||||
$view['is_edit'] = true;
|
||||
$videos = get_post_meta($product_id, '_aksrt_product_video', true);
|
||||
if (!is_array($videos)) {
|
||||
$videos = [];
|
||||
}
|
||||
$view['product'] = [
|
||||
'id' => $product_id,
|
||||
'title' => (string) $product->post_title,
|
||||
@@ -197,6 +206,11 @@ class AKSRT_Shop {
|
||||
'after_sale_policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
|
||||
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
|
||||
'status' => (string) get_post_status($product_id),
|
||||
'options' => method_exists($this, 'get_product_options') ? $this->get_product_options($product_id) : [],
|
||||
'pay_modo' => sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0',
|
||||
'start_price' => (float) get_post_meta($product_id, '_aksrt_start_price', true),
|
||||
'product_video' => array_values(array_filter(array_map('esc_url_raw', $videos))),
|
||||
'product_params' => (string) get_post_meta($product_id, '_aksrt_product_params', true),
|
||||
];
|
||||
|
||||
return $view;
|
||||
|
||||
@@ -46,48 +46,111 @@ trait AKSRT_Shop_Part_1 {
|
||||
public function get_product_archive_view(array $args = []): array {
|
||||
$taxonomy = sanitize_key((string) ($args['taxonomy'] ?? ''));
|
||||
$term_id = (int) ($args['term_id'] ?? 0);
|
||||
$paged = max(1, (int) ($args['paged'] ?? 1));
|
||||
$per_page = 12;
|
||||
$paged = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1));
|
||||
|
||||
if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true)) {
|
||||
return ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0];
|
||||
$shop_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_shop_settings() : [];
|
||||
$list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : [];
|
||||
$per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12))));
|
||||
$orderby = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date')));
|
||||
if (!in_array($orderby, ['date', 'sales', 'price_asc', 'price_desc', 'comments', 'modified'], true)) {
|
||||
$orderby = 'date';
|
||||
}
|
||||
$paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number')));
|
||||
if (!in_array($paging, ['number', 'ajax'], true)) {
|
||||
$paging = 'number';
|
||||
}
|
||||
$ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']);
|
||||
$ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5))));
|
||||
$style = sanitize_key((string) ($list['style'] ?? 'default'));
|
||||
|
||||
$empty = [
|
||||
'products' => [],
|
||||
'term' => null,
|
||||
'total' => 0,
|
||||
'max_pages' => 0,
|
||||
'page' => $paged,
|
||||
'orderby' => $orderby,
|
||||
'paging' => $paging,
|
||||
'ias_s' => $ias_s,
|
||||
'ias_max' => $ias_max,
|
||||
'per_page' => $per_page,
|
||||
'style' => $style,
|
||||
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
|
||||
];
|
||||
|
||||
if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$term = get_term($term_id, $taxonomy);
|
||||
if (is_wp_error($term) || !$term) {
|
||||
return ['products' => [], 'term' => null, 'total' => 0, 'max_pages' => 0];
|
||||
}
|
||||
|
||||
$query = new WP_Query([
|
||||
$query_args = [
|
||||
'post_type' => 'aksrt_product',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $paged,
|
||||
'post_status' => 'publish',
|
||||
'tax_query' => [[
|
||||
'order' => 'DESC',
|
||||
'orderby' => 'date',
|
||||
];
|
||||
|
||||
$term = null;
|
||||
if ($taxonomy !== '' && $term_id > 0) {
|
||||
$term = get_term($term_id, $taxonomy);
|
||||
if (is_wp_error($term) || !$term) {
|
||||
return $empty;
|
||||
}
|
||||
$query_args['tax_query'] = [[
|
||||
'taxonomy' => $taxonomy,
|
||||
'field' => 'term_id',
|
||||
'terms' => $term_id,
|
||||
]],
|
||||
]);
|
||||
]];
|
||||
}
|
||||
|
||||
if ($orderby === 'sales') {
|
||||
$query_args['meta_key'] = '_aksrt_sales';
|
||||
$query_args['orderby'] = 'meta_value_num';
|
||||
$query_args['order'] = 'DESC';
|
||||
} elseif ($orderby === 'price_asc') {
|
||||
$query_args['meta_key'] = '_aksrt_price';
|
||||
$query_args['orderby'] = 'meta_value_num';
|
||||
$query_args['order'] = 'ASC';
|
||||
} elseif ($orderby === 'price_desc') {
|
||||
$query_args['meta_key'] = '_aksrt_price';
|
||||
$query_args['orderby'] = 'meta_value_num';
|
||||
$query_args['order'] = 'DESC';
|
||||
} elseif ($orderby === 'comments') {
|
||||
$query_args['orderby'] = 'comment_count';
|
||||
$query_args['order'] = 'DESC';
|
||||
} elseif ($orderby === 'modified') {
|
||||
$query_args['orderby'] = 'modified';
|
||||
$query_args['order'] = 'DESC';
|
||||
}
|
||||
|
||||
$query = new WP_Query($query_args);
|
||||
$products = [];
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$products[] = $this->build_product_card_view(get_the_ID());
|
||||
}
|
||||
|
||||
wp_reset_postdata();
|
||||
|
||||
return [
|
||||
'products' => $products,
|
||||
'term' => [
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'description' => $term->description,
|
||||
'count' => $term->count,
|
||||
],
|
||||
'term' => $term ? [
|
||||
'id' => (int) $term->term_id,
|
||||
'name' => (string) $term->name,
|
||||
'description' => (string) $term->description,
|
||||
'count' => (int) $term->count,
|
||||
'taxonomy' => $taxonomy,
|
||||
] : null,
|
||||
'total' => (int) $query->found_posts,
|
||||
'max_pages' => (int) $query->max_num_pages,
|
||||
'page' => $paged,
|
||||
'orderby' => $orderby,
|
||||
'paging' => $paging,
|
||||
'ias_s' => $ias_s,
|
||||
'ias_max' => $ias_max,
|
||||
'per_page' => $per_page,
|
||||
'style' => $style,
|
||||
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user