refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories (core, auth, user, credits, content, payment/gateways, forum, shop, sms, ai, messages, extensions, widgets, settings, admin). Update bootstrap require paths, co-locate traits with owners, fix forum/user relative requires, drop duplicate shop controller requires, and wire home-modules load. Path-only; class names and init order unchanged.
This commit is contained in:
1 parent
2a8fe68048
commit
ce1c13b947
115 files changed
+171
-224
No files matched your search
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Product Review — 商品评价/评分系统 (§6.10)
|
||||
*
|
||||
* Multi-dimensional rating (product/service/shipping/average),
|
||||
* review images, auto-positive after timeout, filtering.
|
||||
* Uses WP comments with comment_type = 'aksrt_product_review'.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Product_Review {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
const COMMENT_TYPE = 'aksrt_product_review';
|
||||
|
||||
const RATING_LEVELS = [
|
||||
5 => '超赞',
|
||||
4 => '很棒',
|
||||
3 => '不错',
|
||||
2 => '一般',
|
||||
1 => '较差',
|
||||
0 => '很差',
|
||||
];
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('aksrt_daily_cron', [$this, 'run_auto_positive_cron']);
|
||||
}
|
||||
|
||||
public function submit_review(int $user_id, string $order_number, int $product_id, array $data): array {
|
||||
if ($user_id <= 0 || $order_number === '' || $product_id <= 0) {
|
||||
return ['success' => false, 'message' => __('参数错误', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!$this->can_review($user_id, $order_number, $product_id)) {
|
||||
return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')];
|
||||
}
|
||||
|
||||
if ($this->has_reviewed($user_id, $order_number, $product_id)) {
|
||||
return ['success' => false, 'message' => __('已评价过该商品', 'aksrt')];
|
||||
}
|
||||
|
||||
$rating_product = max(0, min(5, (int) ($data['rating_product'] ?? 5)));
|
||||
$rating_service = max(0, min(5, (int) ($data['rating_service'] ?? 5)));
|
||||
$rating_shipping = max(0, min(5, (int) ($data['rating_shipping'] ?? 5)));
|
||||
$average = round(($rating_product + $rating_service + $rating_shipping) / 3, 1);
|
||||
|
||||
$content = sanitize_textarea_field((string) ($data['content'] ?? ''));
|
||||
$images = [];
|
||||
$max_images = $this->get_max_images();
|
||||
foreach ((array) ($data['images'] ?? []) as $img) {
|
||||
if (count($images) >= $max_images) break;
|
||||
$url = esc_url_raw((string) $img);
|
||||
if ($url !== '') {
|
||||
$images[] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
$comment_id = wp_insert_comment([
|
||||
'comment_post_ID' => $product_id,
|
||||
'user_id' => $user_id,
|
||||
'comment_content' => $content ?: __('好评!', 'aksrt'),
|
||||
'comment_type' => self::COMMENT_TYPE,
|
||||
'comment_approved' => 1,
|
||||
]);
|
||||
|
||||
if (!$comment_id) {
|
||||
return ['success' => false, 'message' => __('评价提交失败', 'aksrt')];
|
||||
}
|
||||
|
||||
update_comment_meta($comment_id, '_aksrt_order_number', $order_number);
|
||||
update_comment_meta($comment_id, '_aksrt_rating_product', $rating_product);
|
||||
update_comment_meta($comment_id, '_aksrt_rating_service', $rating_service);
|
||||
update_comment_meta($comment_id, '_aksrt_rating_shipping', $rating_shipping);
|
||||
update_comment_meta($comment_id, '_aksrt_rating_average', $average);
|
||||
if (!empty($images)) {
|
||||
update_comment_meta($comment_id, '_aksrt_review_images', $images);
|
||||
}
|
||||
|
||||
$this->refresh_product_scores($product_id);
|
||||
|
||||
if (class_exists('AKSRT_Order_Repository')) {
|
||||
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
|
||||
$status = (string) ($order['status'] ?? '');
|
||||
if ($status === AKSRT_Order::STATUS_PENDING_REVIEW) {
|
||||
AKSRT_Order_Repository::get_instance()->update_status(
|
||||
$order_number,
|
||||
AKSRT_Order::STATUS_COMPLETED,
|
||||
['note' => __('用户已评价', 'aksrt')]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'comment_id' => $comment_id];
|
||||
}
|
||||
|
||||
public function can_review(int $user_id, string $order_number, int $product_id): bool {
|
||||
if (!class_exists('AKSRT_Order_Repository')) return false;
|
||||
|
||||
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
|
||||
if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) return false;
|
||||
|
||||
$status = (string) ($order['status'] ?? '');
|
||||
if (!in_array($status, [
|
||||
AKSRT_Order::STATUS_COMPLETED,
|
||||
AKSRT_Order::STATUS_PENDING_REVIEW,
|
||||
AKSRT_Order::STATUS_DELIVERED,
|
||||
], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (class_exists('AKSRT_After_Sale')) {
|
||||
$tickets = AKSRT_After_Sale::get_instance()->get_list([
|
||||
'order_number' => $order_number,
|
||||
'limit' => 1,
|
||||
]);
|
||||
foreach (($tickets['items'] ?? []) as $ticket) {
|
||||
if (in_array(($ticket['status'] ?? ''), ['pending', 'processing'], true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function has_reviewed(int $user_id, string $order_number, int $product_id): bool {
|
||||
$comments = get_comments([
|
||||
'post_id' => $product_id,
|
||||
'user_id' => $user_id,
|
||||
'type' => self::COMMENT_TYPE,
|
||||
'number' => 1,
|
||||
'meta_query' => [
|
||||
['key' => '_aksrt_order_number', 'value' => $order_number],
|
||||
],
|
||||
]);
|
||||
return !empty($comments);
|
||||
}
|
||||
|
||||
public function get_reviews(int $product_id, array $args = []): array {
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 10)));
|
||||
$filter = sanitize_key((string) ($args['filter'] ?? ''));
|
||||
|
||||
$query_args = [
|
||||
'post_id' => $product_id,
|
||||
'type' => self::COMMENT_TYPE,
|
||||
'status' => 'approve',
|
||||
'number' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
'orderby' => 'comment_date',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
|
||||
if ($filter === 'good') {
|
||||
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 4, 'compare' => '>=', 'type' => 'NUMERIC']];
|
||||
} elseif ($filter === 'bad') {
|
||||
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 3, 'compare' => '<', 'type' => 'NUMERIC']];
|
||||
} elseif ($filter === 'has_image') {
|
||||
$query_args['meta_query'] = [['key' => '_aksrt_review_images', 'compare' => 'EXISTS']];
|
||||
}
|
||||
|
||||
$comments = get_comments($query_args);
|
||||
$total = (int) get_comments(array_merge($query_args, ['count' => true, 'number' => 0, 'offset' => 0]));
|
||||
|
||||
$items = [];
|
||||
foreach ($comments as $comment) {
|
||||
$cid = (int) $comment->comment_ID;
|
||||
$items[] = [
|
||||
'id' => $cid,
|
||||
'user_id' => (int) $comment->user_id,
|
||||
'user_name' => (string) $comment->comment_author,
|
||||
'user_avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url((int) $comment->user_id) : get_avatar_url((int) $comment->user_id),
|
||||
'content' => (string) $comment->comment_content,
|
||||
'date' => (string) $comment->comment_date,
|
||||
'rating_product' => (int) get_comment_meta($cid, '_aksrt_rating_product', true),
|
||||
'rating_service' => (int) get_comment_meta($cid, '_aksrt_rating_service', true),
|
||||
'rating_shipping' => (int) get_comment_meta($cid, '_aksrt_rating_shipping', true),
|
||||
'rating_average' => (float) get_comment_meta($cid, '_aksrt_rating_average', true),
|
||||
'images' => (array) (get_comment_meta($cid, '_aksrt_review_images', true) ?: []),
|
||||
'order_number' => (string) get_comment_meta($cid, '_aksrt_order_number', true),
|
||||
];
|
||||
}
|
||||
|
||||
return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
|
||||
}
|
||||
|
||||
public function get_product_scores(int $product_id): array {
|
||||
$cached = get_post_meta($product_id, '_aksrt_review_scores', true);
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
return $this->refresh_product_scores($product_id);
|
||||
}
|
||||
|
||||
public function refresh_product_scores(int $product_id): array {
|
||||
global $wpdb;
|
||||
|
||||
$results = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT COUNT(*) as total,
|
||||
AVG(CAST(m.meta_value AS DECIMAL(3,1))) as average
|
||||
FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
|
||||
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
|
||||
$product_id, self::COMMENT_TYPE
|
||||
), ARRAY_A);
|
||||
|
||||
$total = (int) ($results['total'] ?? 0);
|
||||
$average = $total > 0 ? round((float) ($results['average'] ?? 0), 1) : 0;
|
||||
|
||||
$good_count = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
|
||||
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
|
||||
AND CAST(m.meta_value AS DECIMAL(3,1)) >= 4",
|
||||
$product_id, self::COMMENT_TYPE
|
||||
));
|
||||
|
||||
$bad_count = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
|
||||
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
|
||||
AND CAST(m.meta_value AS DECIMAL(3,1)) < 3",
|
||||
$product_id, self::COMMENT_TYPE
|
||||
));
|
||||
|
||||
$has_image = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->comments} c
|
||||
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_review_images'
|
||||
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
|
||||
$product_id, self::COMMENT_TYPE
|
||||
));
|
||||
|
||||
$scores = [
|
||||
'total' => $total,
|
||||
'average' => $average,
|
||||
'good' => $good_count,
|
||||
'bad' => $bad_count,
|
||||
'has_image' => $has_image,
|
||||
'level' => self::RATING_LEVELS[min(5, max(0, (int) round($average)))] ?? '',
|
||||
];
|
||||
|
||||
update_post_meta($product_id, '_aksrt_review_scores', $scores);
|
||||
return $scores;
|
||||
}
|
||||
|
||||
public function run_auto_positive_cron(): void {
|
||||
if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shop = AKSRT_Settings::get_shop_settings();
|
||||
$max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15));
|
||||
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
|
||||
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
$orders = $repo->get_orders_admin([
|
||||
'status' => AKSRT_Order::STATUS_PENDING_REVIEW,
|
||||
'limit' => 200,
|
||||
]);
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$completed_at = (string) ($order['completed_at'] ?? $order['delivered_at'] ?? '');
|
||||
if ($completed_at === '' || $completed_at > $cutoff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
if ($user_id <= 0 || $order_number === '') continue;
|
||||
|
||||
$items = class_exists('AKSRT_Shop')
|
||||
? AKSRT_Shop::get_instance()->get_order_items($order_number)
|
||||
: [];
|
||||
|
||||
$product_ids = array_column($items, 'product_id');
|
||||
if (empty($product_ids) && !empty($order['product_id'])) {
|
||||
$product_ids = [(int) $order['product_id']];
|
||||
}
|
||||
|
||||
foreach ($product_ids as $pid) {
|
||||
if (!$this->has_reviewed($user_id, $order_number, (int) $pid)) {
|
||||
$this->submit_review($user_id, $order_number, (int) $pid, [
|
||||
'rating_product' => 5,
|
||||
'rating_service' => 5,
|
||||
'rating_shipping' => 5,
|
||||
'content' => __('系统默认好评', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$repo->update_status($order_number, AKSRT_Order::STATUS_COMPLETED, [
|
||||
'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function get_max_images(): int {
|
||||
if (!class_exists('AKSRT_Settings')) return 9;
|
||||
$shop = AKSRT_Settings::get_shop_settings();
|
||||
return max(1, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Shop Term Meta
|
||||
*
|
||||
* 为商品分类(aksrt_product_cat)、商品标签(aksrt_product_tag)和
|
||||
* 优惠活动(shop_discount)注册 term meta 并提供 getter/setter。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Shop_Term_Meta {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
/** @var string Meta key for product_cat/product_tag unified storage */
|
||||
const META_KEY = 'aksrt_shop_term_meta';
|
||||
|
||||
/** @var string Meta key for shop_discount unified storage */
|
||||
const DISCOUNT_META_KEY = 'aksrt_shop_discount_meta';
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*/
|
||||
public function init(): void {
|
||||
add_action('init', [$this, 'register_discount_taxonomy'], 5);
|
||||
add_action('init', [$this, 'register_term_meta_fields'], 10);
|
||||
|
||||
// Native term UI for shop_discount (zibll-style admin ops).
|
||||
add_action('shop_discount_add_form_fields', [$this, 'render_discount_add_fields']);
|
||||
add_action('shop_discount_edit_form_fields', [$this, 'render_discount_edit_fields'], 10, 1);
|
||||
add_action('created_shop_discount', [$this, 'save_discount_term_fields']);
|
||||
add_action('edited_shop_discount', [$this, 'save_discount_term_fields']);
|
||||
|
||||
// Product category inheritance fields.
|
||||
add_action('aksrt_product_cat_add_form_fields', [$this, 'render_cat_add_fields']);
|
||||
add_action('aksrt_product_cat_edit_form_fields', [$this, 'render_cat_edit_fields'], 10, 1);
|
||||
add_action('created_aksrt_product_cat', [$this, 'save_cat_term_fields']);
|
||||
add_action('edited_aksrt_product_cat', [$this, 'save_cat_term_fields']);
|
||||
|
||||
// Product tag display fields.
|
||||
add_action('aksrt_product_tag_add_form_fields', [$this, 'render_tag_add_fields']);
|
||||
add_action('aksrt_product_tag_edit_form_fields', [$this, 'render_tag_edit_fields'], 10, 1);
|
||||
add_action('created_aksrt_product_tag', [$this, 'save_tag_term_fields']);
|
||||
add_action('edited_aksrt_product_tag', [$this, 'save_tag_term_fields']);
|
||||
}
|
||||
|
||||
// ─── Taxonomy Registration ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register the shop_discount taxonomy for discount campaigns.
|
||||
*/
|
||||
public function register_discount_taxonomy(): void {
|
||||
if (taxonomy_exists('shop_discount')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shop_enabled = true;
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$shop_settings = AKSRT_Settings::get_shop_settings();
|
||||
$shop_enabled = !empty($shop_settings['enabled']);
|
||||
}
|
||||
$shop_show_ui = true;
|
||||
$shop_show_menu = is_admin() ? $shop_enabled : false;
|
||||
|
||||
register_taxonomy('shop_discount', 'aksrt_product', [
|
||||
'labels' => [
|
||||
'name' => __('优惠活动', 'aksrt'),
|
||||
'singular_name' => __('优惠活动', 'aksrt'),
|
||||
'search_items' => __('搜索优惠活动', 'aksrt'),
|
||||
'all_items' => __('所有优惠活动', 'aksrt'),
|
||||
'edit_item' => __('编辑优惠活动', 'aksrt'),
|
||||
'add_new_item' => __('新增优惠活动', 'aksrt'),
|
||||
'menu_name' => __('优惠活动', 'aksrt'),
|
||||
],
|
||||
'public' => true,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => $shop_show_ui,
|
||||
'show_in_menu' => $shop_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => ['slug' => 'shop-discount', 'with_front' => false],
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Term Meta Registration ──────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register all term meta fields for REST API exposure.
|
||||
*/
|
||||
public function register_term_meta_fields(): void {
|
||||
$auth = function () {
|
||||
return current_user_can('manage_categories');
|
||||
};
|
||||
|
||||
// ── aksrt_product_cat ──
|
||||
$cat_fields = $this->get_cat_field_definitions();
|
||||
foreach ($cat_fields as $key => $def) {
|
||||
register_term_meta('aksrt_product_cat', $key, [
|
||||
'type' => $def['type'],
|
||||
'description' => $def['description'] ?? $key,
|
||||
'single' => true,
|
||||
'default' => $def['default'] ?? '',
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
|
||||
'auth_callback' => $auth,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── aksrt_product_tag ──
|
||||
$tag_fields = $this->get_tag_field_definitions();
|
||||
foreach ($tag_fields as $key => $def) {
|
||||
register_term_meta('aksrt_product_tag', $key, [
|
||||
'type' => $def['type'],
|
||||
'description' => $def['description'] ?? $key,
|
||||
'single' => true,
|
||||
'default' => $def['default'] ?? '',
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
|
||||
'auth_callback' => $auth,
|
||||
]);
|
||||
}
|
||||
|
||||
// ── shop_discount ──
|
||||
$discount_fields = $this->get_discount_field_definitions();
|
||||
foreach ($discount_fields as $key => $def) {
|
||||
register_term_meta('shop_discount', $key, [
|
||||
'type' => $def['type'],
|
||||
'description' => $def['description'] ?? $key,
|
||||
'single' => true,
|
||||
'default' => $def['default'] ?? '',
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
|
||||
'auth_callback' => $auth,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Field Definitions ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Product category term meta field definitions.
|
||||
*
|
||||
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
|
||||
*/
|
||||
private function get_cat_field_definitions(): array {
|
||||
return [
|
||||
'shop_cat_limit_buy' => [
|
||||
'type' => 'integer', 'description' => '分类级限购 (-1 不限)', 'default' => -1,
|
||||
'sanitize' => function ($v) { return max(-1, min(9999, (int) $v)); },
|
||||
],
|
||||
'shop_cat_shipping_fee' => [
|
||||
'type' => 'string', 'description' => '分类级运费类型', 'default' => '',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['', 'free', 'fixed', 'amount'], true) ? $v : '';
|
||||
},
|
||||
],
|
||||
'shop_cat_guest_buy' => [
|
||||
'type' => 'string', 'description' => '分类级免登录购买', 'default' => '',
|
||||
],
|
||||
'shop_cat_email_fill' => [
|
||||
'type' => 'string', 'description' => '分类级邮箱必留', 'default' => '',
|
||||
],
|
||||
'shop_cat_after_sale' => [
|
||||
'type' => 'string', 'description' => '分类级售后政策', 'default' => '',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['', 'refund', 'refund_return', 'none'], true) ? $v : '';
|
||||
},
|
||||
],
|
||||
'shop_cat_rebate' => [
|
||||
'type' => 'string', 'description' => '分类级返佣覆盖', 'default' => '',
|
||||
],
|
||||
'shop_cat_service' => [
|
||||
'type' => 'string', 'description' => '分类级服务保障', 'default' => '',
|
||||
'sanitize' => 'sanitize_textarea_field',
|
||||
],
|
||||
'shop_cat_single_tab' => [
|
||||
'type' => 'string', 'description' => '分类级自定义 TAB', 'default' => '',
|
||||
'sanitize' => 'sanitize_textarea_field',
|
||||
],
|
||||
'shop_cat_content_after' => [
|
||||
'type' => 'string', 'description' => '分类级详情页底部内容', 'default' => '',
|
||||
'sanitize' => 'wp_kses_post',
|
||||
],
|
||||
'shop_cat_content_layout' => [
|
||||
'type' => 'string', 'description' => '分类级内容布局', 'default' => '',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['', 'adapt', 'full', 'sidebar'], true) ? $v : '';
|
||||
},
|
||||
],
|
||||
'shop_cat_content_show_bg' => [
|
||||
'type' => 'string', 'description' => '分类级详情背景盒子', 'default' => '',
|
||||
],
|
||||
'shop_cat_list_style' => [
|
||||
'type' => 'string', 'description' => '分类页列表样式', 'default' => '',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['', 'default', 'card-lg', 'card-sm'], true) ? $v : '';
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Product tag term meta field definitions.
|
||||
*
|
||||
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
|
||||
*/
|
||||
private function get_tag_field_definitions(): array {
|
||||
return [
|
||||
'shop_tag_priority' => [
|
||||
'type' => 'integer', 'description' => '标签优先级(影响显示顺序)', 'default' => 0,
|
||||
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
|
||||
],
|
||||
'shop_tag_class' => [
|
||||
'type' => 'string', 'description' => '标签颜色 class', 'default' => '',
|
||||
'sanitize' => 'sanitize_html_class',
|
||||
],
|
||||
'shop_tag_is_important' => [
|
||||
'type' => 'string', 'description' => '重点标签(商品价格位着重显示)', 'default' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shop discount term meta field definitions.
|
||||
*
|
||||
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
|
||||
*/
|
||||
private function get_discount_field_definitions(): array {
|
||||
return [
|
||||
'shop_discount_small_badge' => [
|
||||
'type' => 'string', 'description' => '小标签文案', 'default' => '',
|
||||
],
|
||||
'shop_discount_priority' => [
|
||||
'type' => 'integer', 'description' => '优先级(数字越大越优先)', 'default' => 0,
|
||||
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
|
||||
],
|
||||
'shop_discount_is_important' => [
|
||||
'type' => 'string', 'description' => '重点活动', 'default' => '',
|
||||
],
|
||||
'shop_discount_important_class' => [
|
||||
'type' => 'string', 'description' => '重点活动背景色 class', 'default' => '',
|
||||
'sanitize' => 'sanitize_html_class',
|
||||
],
|
||||
'shop_discount_scope' => [
|
||||
'type' => 'string', 'description' => '优惠范围', 'default' => 'item',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['item', 'product', 'author', 'order'], true) ? $v : 'item';
|
||||
},
|
||||
],
|
||||
'shop_discount_type' => [
|
||||
'type' => 'string', 'description' => '优惠类型', 'default' => 'reduction',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['reduction', 'discount', 'gift'], true) ? $v : 'reduction';
|
||||
},
|
||||
],
|
||||
'shop_discount_value' => [
|
||||
'type' => 'string', 'description' => '优惠数值(立减金额/折扣 0.01-9.99)', 'default' => '0',
|
||||
'sanitize' => function ($v) { return sanitize_text_field((string) $v); },
|
||||
],
|
||||
'shop_discount_gift_config' => [
|
||||
'type' => 'string', 'description' => '赠品配置 JSON', 'default' => '',
|
||||
'sanitize' => 'sanitize_textarea_field',
|
||||
],
|
||||
'shop_discount_price_limit' => [
|
||||
'type' => 'string', 'description' => '金额门槛', 'default' => '0',
|
||||
'sanitize' => function ($v) { return max(0, (float) $v); },
|
||||
],
|
||||
'shop_discount_user_limit' => [
|
||||
'type' => 'string', 'description' => '用户身份限制', 'default' => '',
|
||||
'sanitize' => function ($v) {
|
||||
$v = sanitize_key((string) $v);
|
||||
return in_array($v, ['', 'vip', 'vip_2', 'auth'], true) ? $v : '';
|
||||
},
|
||||
],
|
||||
'shop_discount_start' => [
|
||||
'type' => 'string', 'description' => '活动开始时间', 'default' => '',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
],
|
||||
'shop_discount_end' => [
|
||||
'type' => 'string', 'description' => '活动结束时间', 'default' => '',
|
||||
'sanitize' => 'sanitize_text_field',
|
||||
],
|
||||
'shop_discount_countdown' => [
|
||||
'type' => 'string', 'description' => '倒计时显示', 'default' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Getters ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get all shop term meta for a product category.
|
||||
*
|
||||
* @param int $term_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_cat_meta(int $term_id): array {
|
||||
$defaults = [
|
||||
'limit_buy' => -1,
|
||||
'shipping_fee' => '',
|
||||
'guest_buy' => '',
|
||||
'email_fill' => '',
|
||||
'after_sale' => '',
|
||||
'rebate' => '',
|
||||
'service' => '',
|
||||
'single_tab' => '',
|
||||
'content_after' => '',
|
||||
'content_layout' => '',
|
||||
'content_show_bg' => '',
|
||||
'list_style' => '',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($defaults as $key => $default) {
|
||||
$meta_key = "shop_cat_{$key}";
|
||||
$val = get_term_meta($term_id, $meta_key, true);
|
||||
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all shop term meta for a product tag.
|
||||
*
|
||||
* @param int $term_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_tag_meta(int $term_id): array {
|
||||
$priority = get_term_meta($term_id, 'shop_tag_priority', true);
|
||||
$class = get_term_meta($term_id, 'shop_tag_class', true);
|
||||
$is_important = get_term_meta($term_id, 'shop_tag_is_important', true);
|
||||
|
||||
return [
|
||||
'priority' => $priority !== '' ? (int) $priority : 0,
|
||||
'class' => is_string($class) ? $class : '',
|
||||
'is_important' => !empty($is_important),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all shop discount term meta.
|
||||
*
|
||||
* @param int $term_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_discount_meta(int $term_id): array {
|
||||
$defaults = [
|
||||
'small_badge' => '',
|
||||
'priority' => 0,
|
||||
'is_important' => false,
|
||||
'important_class' => '',
|
||||
'scope' => 'item',
|
||||
'type' => 'reduction',
|
||||
'value' => '0',
|
||||
'gift_config' => '',
|
||||
'price_limit' => '0',
|
||||
'user_limit' => '',
|
||||
'start' => '',
|
||||
'end' => '',
|
||||
'countdown' => '',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($defaults as $key => $default) {
|
||||
$meta_key = "shop_discount_{$key}";
|
||||
$val = get_term_meta($term_id, $meta_key, true);
|
||||
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
|
||||
}
|
||||
|
||||
$result['priority'] = (int) $result['priority'];
|
||||
$result['is_important'] = !empty($result['is_important']);
|
||||
$result['price_limit'] = (float) $result['price_limit'];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve parameter inheritance: product → category → theme settings.
|
||||
*
|
||||
* Returns the effective value for a given parameter key by checking:
|
||||
* 1. Product-level override (from post meta)
|
||||
* 2. Category-level override (from term meta)
|
||||
* 3. Theme-level default (from AKSRT_Settings::get_shop_settings())
|
||||
*
|
||||
* @param int $product_id Product post ID
|
||||
* @param string $param_key Parameter key (e.g. 'limit_buy', 'shipping_fee')
|
||||
* @param mixed $product_value Product-level value (null if not set)
|
||||
* @return mixed
|
||||
*/
|
||||
public static function resolve_inheritance(int $product_id, string $param_key, $product_value = null) {
|
||||
// 1. Product-level override
|
||||
if ($product_value !== null && $product_value !== '' && $product_value !== -1) {
|
||||
return $product_value;
|
||||
}
|
||||
|
||||
// 2. Category-level override
|
||||
$terms = get_the_terms($product_id, 'aksrt_product_cat');
|
||||
if (is_array($terms) && !empty($terms)) {
|
||||
$cat_meta_key = "shop_cat_{$param_key}";
|
||||
foreach ($terms as $term) {
|
||||
$val = get_term_meta($term->term_id, $cat_meta_key, true);
|
||||
if ($val !== '' && $val !== false && $val !== -1) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Theme-level default
|
||||
$shop = AKSRT_Settings::get_shop_settings();
|
||||
$defaults = $shop['product_defaults'] ?? [];
|
||||
return $defaults[$param_key] ?? $product_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add form fields for shop_discount taxonomy.
|
||||
*/
|
||||
public function render_discount_add_fields(): void {
|
||||
$fields = $this->get_discount_field_definitions();
|
||||
foreach ($fields as $key => $def) {
|
||||
$label = (string) ($def['description'] ?? $key);
|
||||
$default = $def['default'] ?? '';
|
||||
echo '<div class="form-field">';
|
||||
echo '<label for="' . esc_attr($key) . '">' . esc_html($label) . '</label>';
|
||||
$this->render_discount_field_control($key, $default, false);
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit form fields for shop_discount taxonomy.
|
||||
*/
|
||||
public function render_discount_edit_fields($term): void {
|
||||
if (!($term instanceof WP_Term)) {
|
||||
return;
|
||||
}
|
||||
$fields = $this->get_discount_field_definitions();
|
||||
foreach ($fields as $key => $def) {
|
||||
$label = (string) ($def['description'] ?? $key);
|
||||
$value = get_term_meta($term->term_id, $key, true);
|
||||
if ($value === '' || $value === false) {
|
||||
$value = $def['default'] ?? '';
|
||||
}
|
||||
echo '<tr class="form-field">';
|
||||
echo '<th scope="row"><label for="' . esc_attr($key) . '">' . esc_html($label) . '</label></th>';
|
||||
echo '<td>';
|
||||
$this->render_discount_field_control($key, $value, true);
|
||||
echo '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int|float $value
|
||||
*/
|
||||
private function render_discount_field_control(string $key, $value, bool $edit): void {
|
||||
$id = esc_attr($key);
|
||||
$name = esc_attr($key);
|
||||
$val = (string) $value;
|
||||
|
||||
if ($key === 'shop_discount_type') {
|
||||
$opts = [
|
||||
'reduction' => __('立减', 'aksrt'),
|
||||
'discount' => __('折扣', 'aksrt'),
|
||||
'gift' => __('赠品', 'aksrt'),
|
||||
];
|
||||
echo '<select name="' . $name . '" id="' . $id . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key === 'shop_discount_scope') {
|
||||
$opts = [
|
||||
'item' => __('单品', 'aksrt'),
|
||||
'product' => __('商品', 'aksrt'),
|
||||
'author' => __('商家', 'aksrt'),
|
||||
'order' => __('整单', 'aksrt'),
|
||||
];
|
||||
echo '<select name="' . $name . '" id="' . $id . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key === 'shop_discount_user_limit') {
|
||||
$opts = [
|
||||
'' => __('不限', 'aksrt'),
|
||||
'vip' => __('VIP1', 'aksrt'),
|
||||
'vip_2' => __('VIP2', 'aksrt'),
|
||||
'auth' => __('已认证', 'aksrt'),
|
||||
];
|
||||
echo '<select name="' . $name . '" id="' . $id . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
|
||||
echo '<label><input type="checkbox" name="' . $name . '" id="' . $id . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key === 'shop_discount_gift_config') {
|
||||
echo '<textarea name="' . $name . '" id="' . $id . '" rows="3" class="large-text">' . esc_textarea($val) . '</textarea>';
|
||||
return;
|
||||
}
|
||||
|
||||
$type = in_array($key, ['shop_discount_priority'], true) ? 'number' : 'text';
|
||||
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true)) {
|
||||
$type = 'datetime-local';
|
||||
// Convert mysql-ish values to datetime-local if needed.
|
||||
if ($val !== '' && str_contains($val, ' ')) {
|
||||
$val = str_replace(' ', 'T', substr($val, 0, 16));
|
||||
}
|
||||
}
|
||||
|
||||
printf(
|
||||
'<input type="%s" name="%s" id="%s" value="%s" class="%s" />',
|
||||
esc_attr($type),
|
||||
$name,
|
||||
$id,
|
||||
esc_attr($val),
|
||||
$edit ? 'regular-text' : ''
|
||||
);
|
||||
}
|
||||
|
||||
public function save_discount_term_fields(int $term_id): void {
|
||||
if (!current_user_can('manage_categories')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = $this->get_discount_field_definitions();
|
||||
foreach ($fields as $key => $def) {
|
||||
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
|
||||
$raw = isset($_POST[$key]) ? '1' : '';
|
||||
} else {
|
||||
if (!isset($_POST[$key])) {
|
||||
continue;
|
||||
}
|
||||
$raw = wp_unslash($_POST[$key]);
|
||||
}
|
||||
|
||||
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
|
||||
$value = call_user_func($def['sanitize'], $raw);
|
||||
} else {
|
||||
$value = sanitize_text_field((string) $raw);
|
||||
}
|
||||
|
||||
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true) && is_string($value) && str_contains($value, 'T')) {
|
||||
$value = str_replace('T', ' ', $value);
|
||||
if (strlen($value) === 16) {
|
||||
$value .= ':00';
|
||||
}
|
||||
}
|
||||
|
||||
update_term_meta($term_id, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function render_cat_add_fields(): void {
|
||||
foreach ($this->get_cat_field_definitions() as $key => $def) {
|
||||
echo '<div class="form-field">';
|
||||
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
|
||||
$this->render_cat_field_control($key, $def['default'] ?? '', false);
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
public function render_cat_edit_fields($term): void {
|
||||
if (!($term instanceof WP_Term)) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->get_cat_field_definitions() as $key => $def) {
|
||||
$value = get_term_meta($term->term_id, $key, true);
|
||||
if ($value === '' || $value === false) {
|
||||
$value = $def['default'] ?? '';
|
||||
}
|
||||
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
|
||||
$this->render_cat_field_control($key, $value, true);
|
||||
echo '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function render_cat_field_control(string $key, $value, bool $edit): void {
|
||||
$val = (string) $value;
|
||||
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
|
||||
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
|
||||
return;
|
||||
}
|
||||
if (in_array($key, ['shop_cat_service', 'shop_cat_single_tab', 'shop_cat_content_after'], true)) {
|
||||
echo '<textarea class="large-text" rows="3" name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">' . esc_textarea($val) . '</textarea>';
|
||||
return;
|
||||
}
|
||||
if ($key === 'shop_cat_shipping_fee') {
|
||||
$opts = ['' => __('继承', 'aksrt'), 'free' => __('包邮', 'aksrt'), 'fixed' => __('固定运费', 'aksrt'), 'amount' => __('满额包邮', 'aksrt')];
|
||||
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
if ($key === 'shop_cat_after_sale') {
|
||||
$opts = ['' => __('继承', 'aksrt'), 'refund' => __('仅退款', 'aksrt'), 'refund_return' => __('退货退款', 'aksrt'), 'none' => __('不支持', 'aksrt')];
|
||||
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
if ($key === 'shop_cat_content_layout') {
|
||||
$opts = ['' => __('继承', 'aksrt'), 'adapt' => __('自适应', 'aksrt'), 'full' => __('通栏', 'aksrt'), 'sidebar' => __('侧栏', 'aksrt')];
|
||||
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
if ($key === 'shop_cat_list_style') {
|
||||
$opts = ['' => __('继承', 'aksrt'), 'default' => __('默认', 'aksrt'), 'card-lg' => __('大卡片', 'aksrt'), 'card-sm' => __('小卡片', 'aksrt')];
|
||||
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
|
||||
foreach ($opts as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select>';
|
||||
return;
|
||||
}
|
||||
$type = $key === 'shop_cat_limit_buy' ? 'number' : 'text';
|
||||
printf(
|
||||
'<input type="%s" class="%s" name="%s" id="%s" value="%s" />',
|
||||
esc_attr($type),
|
||||
$edit ? 'regular-text' : '',
|
||||
esc_attr($key),
|
||||
esc_attr($key),
|
||||
esc_attr($val)
|
||||
);
|
||||
}
|
||||
|
||||
public function save_cat_term_fields(int $term_id): void {
|
||||
if (!current_user_can('manage_categories')) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->get_cat_field_definitions() as $key => $def) {
|
||||
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
|
||||
$raw = isset($_POST[$key]) ? '1' : '';
|
||||
} else {
|
||||
if (!isset($_POST[$key])) {
|
||||
continue;
|
||||
}
|
||||
$raw = wp_unslash($_POST[$key]);
|
||||
}
|
||||
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
|
||||
$value = call_user_func($def['sanitize'], $raw);
|
||||
} else {
|
||||
$value = sanitize_text_field((string) $raw);
|
||||
}
|
||||
update_term_meta($term_id, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function render_tag_add_fields(): void {
|
||||
foreach ($this->get_tag_field_definitions() as $key => $def) {
|
||||
echo '<div class="form-field">';
|
||||
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
|
||||
$this->render_tag_field_control($key, $def['default'] ?? '');
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
public function render_tag_edit_fields($term): void {
|
||||
if (!($term instanceof WP_Term)) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->get_tag_field_definitions() as $key => $def) {
|
||||
$value = get_term_meta($term->term_id, $key, true);
|
||||
if ($value === '' || $value === false) {
|
||||
$value = $def['default'] ?? '';
|
||||
}
|
||||
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
|
||||
$this->render_tag_field_control($key, $value);
|
||||
echo '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function render_tag_field_control(string $key, $value): void {
|
||||
$val = (string) $value;
|
||||
if ($key === 'shop_tag_is_important') {
|
||||
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
|
||||
return;
|
||||
}
|
||||
$type = $key === 'shop_tag_priority' ? 'number' : 'text';
|
||||
printf(
|
||||
'<input type="%s" class="regular-text" name="%s" id="%s" value="%s" />',
|
||||
esc_attr($type),
|
||||
esc_attr($key),
|
||||
esc_attr($key),
|
||||
esc_attr($val)
|
||||
);
|
||||
}
|
||||
|
||||
public function save_tag_term_fields(int $term_id): void {
|
||||
if (!current_user_can('manage_categories')) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->get_tag_field_definitions() as $key => $def) {
|
||||
if ($key === 'shop_tag_is_important') {
|
||||
$raw = isset($_POST[$key]) ? '1' : '';
|
||||
} else {
|
||||
if (!isset($_POST[$key])) {
|
||||
continue;
|
||||
}
|
||||
$raw = wp_unslash($_POST[$key]);
|
||||
}
|
||||
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
|
||||
$value = call_user_func($def['sanitize'], $raw);
|
||||
} else {
|
||||
$value = sanitize_text_field((string) $raw);
|
||||
}
|
||||
update_term_meta($term_id, $key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Shop
|
||||
*
|
||||
* Frontend shop orchestration and order-context helpers.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Shop {
|
||||
use AKSRT_Shop_Part_1;
|
||||
use AKSRT_Shop_Part_2;
|
||||
use AKSRT_Shop_Part_3;
|
||||
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private ?AKSRT_Shop_Cart_Service $cart_svc = null;
|
||||
private ?AKSRT_Shop_Checkout_Service $checkout_svc = null;
|
||||
private ?AKSRT_Shop_Address_Service $address_svc = null;
|
||||
private ?AKSRT_Shop_Product_Service $product_svc = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function cart_service(): AKSRT_Shop_Cart_Service {
|
||||
if (!$this->cart_svc) {
|
||||
$this->cart_svc = new AKSRT_Shop_Cart_Service($this);
|
||||
}
|
||||
return $this->cart_svc;
|
||||
}
|
||||
|
||||
private function checkout_service(): AKSRT_Shop_Checkout_Service {
|
||||
if (!$this->checkout_svc) {
|
||||
$this->checkout_svc = new AKSRT_Shop_Checkout_Service($this);
|
||||
}
|
||||
return $this->checkout_svc;
|
||||
}
|
||||
|
||||
private function address_service(): AKSRT_Shop_Address_Service {
|
||||
if (!$this->address_svc) {
|
||||
$this->address_svc = new AKSRT_Shop_Address_Service($this);
|
||||
}
|
||||
return $this->address_svc;
|
||||
}
|
||||
|
||||
private function product_service(): AKSRT_Shop_Product_Service {
|
||||
if ($this->product_svc === null) {
|
||||
$this->product_svc = new AKSRT_Shop_Product_Service($this);
|
||||
}
|
||||
return $this->product_svc;
|
||||
}
|
||||
|
||||
public function get_config(): array {
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
return AKSRT_Settings::get_shop_settings();
|
||||
}
|
||||
|
||||
return ['enabled' => false];
|
||||
}
|
||||
|
||||
public function get_products(array $args = []): array {
|
||||
$limit = max(1, (int) ($args['limit'] ?? 20));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
$search = sanitize_text_field((string) ($args['search'] ?? ''));
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
|
||||
$query_args = [
|
||||
'post_type' => 'aksrt_product',
|
||||
'posts_per_page' => $limit,
|
||||
'offset' => $offset,
|
||||
'post_status' => $status !== '' ? $status : ['publish', 'draft', 'pending'],
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
|
||||
if ($search !== '') {
|
||||
$query_args['s'] = $search;
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
public function get_product_detail_view(int $product_id): array {
|
||||
$product = get_post($product_id);
|
||||
if (!$product || $product->post_type !== 'aksrt_product') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$detail = $this->build_product_card_view($product_id);
|
||||
$detail['content'] = (string) $product->post_content;
|
||||
$detail['excerpt'] = (string) $product->post_excerpt;
|
||||
$detail['permalink'] = get_permalink($product_id) ?: '';
|
||||
$detail['status'] = (string) get_post_status($product_id);
|
||||
$detail['gallery'] = $this->product_service()->get_product_gallery($product_id);
|
||||
$detail['categories'] = wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'all']) ?: [];
|
||||
$detail['tags'] = wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'all']) ?: [];
|
||||
$detail['shipping'] = $this->product_service()->get_product_shipping_profile($product_id);
|
||||
$detail['after_sale'] = $this->product_service()->get_product_after_sale_profile($product_id);
|
||||
$detail['marketing'] = $this->product_service()->get_product_marketing_profile($product_id);
|
||||
$detail['checkout_hint'] = $this->get_checkout_hint($detail);
|
||||
$detail['purchase_enabled'] = ($detail['is_unlimited'] || $detail['stock'] > 0) && $detail['status'] === 'publish';
|
||||
$detail['default_quantity'] = 1;
|
||||
$detail['options'] = $this->get_product_options($product_id);
|
||||
$detail['has_options'] = $this->has_options($product_id);
|
||||
$detail['price_range'] = $this->get_price_range($product_id);
|
||||
$detail['pay_modo'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0';
|
||||
$detail['start_price'] = (float) get_post_meta($product_id, '_aksrt_start_price', true);
|
||||
$detail['shipping_type'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_type', true)) ?: 'express';
|
||||
$detail['auto_delivery'] = $this->get_auto_delivery_config($product_id);
|
||||
$detail['limit_buy'] = $this->get_limit_buy_config($product_id);
|
||||
$detail['services'] = $this->get_product_services($product_id);
|
||||
$detail['params'] = $this->get_product_params($product_id);
|
||||
|
||||
return $detail;
|
||||
}
|
||||
|
||||
public function get_product_edit_view(int $user_id, int $product_id = 0): array {
|
||||
$view = [
|
||||
'allowed' => true,
|
||||
'is_edit' => false,
|
||||
'product' => [
|
||||
'id' => 0,
|
||||
'title' => '',
|
||||
'price' => 0,
|
||||
'stock' => 0,
|
||||
'excerpt' => '',
|
||||
'content' => '',
|
||||
'image' => '',
|
||||
'gallery' => [],
|
||||
'categories' => [],
|
||||
'tags' => [],
|
||||
'shipping_mode' => 'express',
|
||||
'shipping_fee' => 0,
|
||||
'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'),
|
||||
'tags' => $this->product_service()->get_term_options('aksrt_product_tag'),
|
||||
],
|
||||
'shipping_modes' => $this->get_shipping_method_options(),
|
||||
'status_options' => [
|
||||
['value' => 'draft', 'label' => __('草稿', 'aksrt')],
|
||||
['value' => 'publish', 'label' => __('发布', 'aksrt')],
|
||||
['value' => 'pending', 'label' => __('待审核', 'aksrt')],
|
||||
],
|
||||
];
|
||||
|
||||
if ($product_id <= 0) {
|
||||
return $view;
|
||||
}
|
||||
|
||||
$product = get_post($product_id);
|
||||
if (!$product || $product->post_type !== 'aksrt_product') {
|
||||
return $view;
|
||||
}
|
||||
|
||||
$is_owner = (int) $product->post_author === $user_id || current_user_can('manage_options');
|
||||
if (!$is_owner) {
|
||||
$view['allowed'] = false;
|
||||
return $view;
|
||||
}
|
||||
|
||||
$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,
|
||||
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
|
||||
'stock' => (int) get_post_meta($product_id, '_aksrt_stock', true),
|
||||
'excerpt' => (string) $product->post_excerpt,
|
||||
'content' => (string) $product->post_content,
|
||||
'image' => $this->resolve_main_image($product_id),
|
||||
'gallery' => $this->product_service()->get_product_gallery($product_id),
|
||||
'categories' => wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'ids']) ?: [],
|
||||
'tags' => wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
|
||||
'shipping_mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
|
||||
'shipping_fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
|
||||
'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;
|
||||
}
|
||||
|
||||
public function get_cart_page_view(int $user_id): array {
|
||||
$pending_orders = $this->get_order_center_view($user_id, [
|
||||
'page' => 1,
|
||||
'per_page' => 5,
|
||||
'status' => AKSRT_Order::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
return [
|
||||
'payment_methods' => $this->get_payment_method_options(),
|
||||
'shipping_methods' => $this->get_shipping_method_options(),
|
||||
'address_book' => $this->get_user_address_book($user_id),
|
||||
'coupon_enabled' => class_exists('AKSRT_Coupon'),
|
||||
'currency_symbol' => '¥',
|
||||
'pending_orders' => $pending_orders['items'] ?? [],
|
||||
'cart_items' => $this->get_cart($user_id),
|
||||
'cart_count' => $this->get_cart_count($user_id),
|
||||
];
|
||||
}
|
||||
|
||||
public function get_cart(int $user_id): array {
|
||||
return $this->cart_service()->get_cart($user_id);
|
||||
}
|
||||
|
||||
public function get_cart_count(int $user_id): int {
|
||||
return $this->cart_service()->get_cart_count($user_id);
|
||||
}
|
||||
|
||||
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
|
||||
return $this->cart_service()->add_to_cart($user_id, $product_id, $options_string, $count);
|
||||
}
|
||||
|
||||
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
|
||||
return $this->cart_service()->reduce_from_cart($user_id, $product_id, $options_string, $count);
|
||||
}
|
||||
|
||||
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
|
||||
return $this->cart_service()->remove_from_cart($user_id, $product_id, $options_string);
|
||||
}
|
||||
|
||||
public function remove_multi_from_cart(int $user_id, array $items): array {
|
||||
return $this->cart_service()->remove_multi_from_cart($user_id, $items);
|
||||
}
|
||||
|
||||
public function empty_cart(int $user_id): array {
|
||||
return $this->cart_service()->empty_cart($user_id);
|
||||
}
|
||||
|
||||
public function validate_cart_item(int $product_id, string $options_string = '') {
|
||||
return $this->cart_service()->validate_cart_item($product_id, $options_string);
|
||||
}
|
||||
|
||||
public function get_order_center_view(int $user_id, array $args = []): array {
|
||||
if ($user_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
|
||||
return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1];
|
||||
}
|
||||
|
||||
$page = max(1, (int) ($args['page'] ?? 1));
|
||||
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 20)));
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
|
||||
$repo_args = [
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page - 1) * $per_page,
|
||||
];
|
||||
|
||||
if ($status !== '') {
|
||||
$repo_args['status'] = $status;
|
||||
}
|
||||
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
$orders = $repo->get_user_orders($user_id, $repo_args);
|
||||
$total = $repo->count_user_orders($user_id, $repo_args);
|
||||
|
||||
$items = [];
|
||||
foreach ($orders as $order) {
|
||||
$items[] = $this->decorate_order_view($order);
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'total_pages' => max(1, (int) ceil($total / $per_page)),
|
||||
'page' => $page,
|
||||
];
|
||||
}
|
||||
|
||||
public function get_order_detail_view(int $user_id, string $order_number): ?array {
|
||||
if ($user_id <= 0 || $order_number === '' || !class_exists('AKSRT_Order_Repository')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$order = AKSRT_Order_Repository::get_instance()->get_user_order($user_id, $order_number);
|
||||
if (!$order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->decorate_order_view($order, true);
|
||||
}
|
||||
|
||||
public function preview_checkout(int $user_id, array $payload): array {
|
||||
return $this->checkout_service()->preview_checkout($user_id, $payload);
|
||||
}
|
||||
|
||||
public function create_checkout_order(int $user_id, array $payload): array {
|
||||
return $this->checkout_service()->create_checkout_order($user_id, $payload);
|
||||
}
|
||||
|
||||
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
|
||||
return $this->checkout_service()->pay_order($user_id, $order_number, $payment_method);
|
||||
}
|
||||
|
||||
public function get_user_favorites(int $user_id, int $page = 1, int $per_page = 20): array {
|
||||
if ($user_id <= 0) {
|
||||
return ['items' => [], 'total' => 0];
|
||||
}
|
||||
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
|
||||
if (!is_array($fav_ids) || empty($fav_ids)) {
|
||||
return ['items' => [], 'total' => 0];
|
||||
}
|
||||
$total = count($fav_ids);
|
||||
$paged = array_slice(array_reverse($fav_ids), ($page - 1) * $per_page, $per_page);
|
||||
$items = [];
|
||||
foreach ($paged as $pid) {
|
||||
$card = $this->build_product_card_view((int) $pid);
|
||||
if (!empty($card)) {
|
||||
$items[] = $card;
|
||||
}
|
||||
}
|
||||
return ['items' => $items, 'total' => $total];
|
||||
}
|
||||
|
||||
public function toggle_favorite(int $user_id, int $product_id): array {
|
||||
if ($user_id <= 0 || $product_id <= 0) {
|
||||
return ['success' => false];
|
||||
}
|
||||
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
|
||||
if (!is_array($fav_ids)) {
|
||||
$fav_ids = [];
|
||||
}
|
||||
$key = array_search($product_id, $fav_ids, true);
|
||||
if ($key !== false) {
|
||||
unset($fav_ids[$key]);
|
||||
$fav_ids = array_values($fav_ids);
|
||||
$is_fav = false;
|
||||
} else {
|
||||
$fav_ids[] = $product_id;
|
||||
$is_fav = true;
|
||||
}
|
||||
update_user_meta($user_id, 'aksrt_product_favorites', $fav_ids);
|
||||
$count = (int) get_post_meta($product_id, '_aksrt_favorite_count', true);
|
||||
update_post_meta($product_id, '_aksrt_favorite_count', max(0, $count + ($is_fav ? 1 : -1)));
|
||||
if ($is_fav) {
|
||||
do_action('aksrt_product_favorited', $user_id, $product_id);
|
||||
} else {
|
||||
do_action('aksrt_product_unfavorited', $user_id, $product_id);
|
||||
}
|
||||
return ['success' => true, 'is_favorite' => $is_fav, 'count' => count($fav_ids)];
|
||||
}
|
||||
|
||||
public function is_favorite(int $user_id, int $product_id): bool {
|
||||
if ($user_id <= 0) return false;
|
||||
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
|
||||
return is_array($fav_ids) && in_array($product_id, $fav_ids, true);
|
||||
}
|
||||
|
||||
public function re_purchase(int $user_id, string $order_number): array {
|
||||
$items = $this->get_order_items($order_number);
|
||||
if (empty($items)) {
|
||||
return ['success' => false, 'message' => __('订单无商品记录', 'aksrt')];
|
||||
}
|
||||
$added = 0;
|
||||
foreach ($items as $item) {
|
||||
$pid = (int) $item['product_id'];
|
||||
$qty = max(1, (int) $item['quantity']);
|
||||
$opt_key = (string) $item['options_key'];
|
||||
$result = $this->add_to_cart($user_id, $pid, $opt_key, $qty);
|
||||
if (!empty($result['success'])) {
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
return ['success' => $added > 0, 'added' => $added, 'cart_count' => $this->get_cart_count($user_id)];
|
||||
}
|
||||
|
||||
public function cancel_order(int $user_id, string $order_number): array {
|
||||
if ($user_id <= 0 || $order_number === '' || !class_exists('AKSRT_Order_Repository')) {
|
||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||
}
|
||||
|
||||
$repo = AKSRT_Order_Repository::get_instance();
|
||||
$order = $repo->get_user_order($user_id, $order_number);
|
||||
if (!$order) {
|
||||
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
|
||||
}
|
||||
|
||||
$status = (string) ($order['status'] ?? '');
|
||||
if (!in_array($status, [AKSRT_Order::STATUS_PENDING, AKSRT_Order::STATUS_CREATED, AKSRT_Order::STATUS_EXPIRED], true)) {
|
||||
return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')];
|
||||
}
|
||||
|
||||
$repo->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
|
||||
'force' => true,
|
||||
'note' => __('用户在前台取消订单。', 'aksrt'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order' => $this->get_order_detail_view($user_id, $order_number),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user