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.
407 lines
14 KiB
PHP
407 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT Coupon — 优惠券系统
|
|
*
|
|
* 使用专用表 {prefix}aksrt_coupon 存储优惠券。
|
|
* 支持:创建、列表、验证、核销。
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_Coupon {
|
|
|
|
const TYPE_PERCENT = 'percent';
|
|
const TYPE_FIXED = 'fixed';
|
|
|
|
private static $instance = null;
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) self::$instance = new self();
|
|
return self::$instance;
|
|
}
|
|
|
|
private function table(): string {
|
|
return AKSRT_DB_Migrator::table('coupon');
|
|
}
|
|
|
|
/**
|
|
* 创建优惠券
|
|
*/
|
|
public function create(array $data): int {
|
|
global $wpdb;
|
|
|
|
$code = sanitize_text_field((string) ($data['code'] ?? ''));
|
|
if ($code === '') return 0;
|
|
|
|
// Check uniqueness
|
|
$exists = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT id FROM {$this->table()} WHERE code = %s",
|
|
$code
|
|
));
|
|
if ($exists) return 0;
|
|
|
|
$type = sanitize_key((string) ($data['type'] ?? self::TYPE_FIXED));
|
|
if (!in_array($type, [self::TYPE_PERCENT, self::TYPE_FIXED], true)) {
|
|
$type = self::TYPE_FIXED;
|
|
}
|
|
|
|
$wpdb->insert($this->table(), [
|
|
'code' => $code,
|
|
'type' => $type,
|
|
'value' => (float) ($data['value'] ?? 0),
|
|
'min_amount' => (float) ($data['min_amount'] ?? 0),
|
|
'usage_limit' => (int) ($data['usage_limit'] ?? 0),
|
|
'used_count' => 0,
|
|
'expires_at' => sanitize_text_field((string) ($data['expires_at'] ?? '')),
|
|
'per_user_limit' => (int) ($data['per_user_limit'] ?? 0),
|
|
'product_scope' => wp_json_encode($data['product_scope'] ?? [], JSON_UNESCAPED_UNICODE),
|
|
'max_discount' => (float) ($data['max_discount'] ?? 0),
|
|
'status' => 'active',
|
|
'created_at' => current_time('mysql'),
|
|
], ['%s', '%s', '%f', '%f', '%d', '%d', '%s', '%d', '%s', '%f', '%s', '%s']);
|
|
|
|
return (int) $wpdb->insert_id;
|
|
}
|
|
|
|
/**
|
|
* 获取优惠券列表
|
|
*/
|
|
public function get_list(array $args = []): array {
|
|
global $wpdb;
|
|
|
|
$where = [];
|
|
$params = [];
|
|
|
|
$status = sanitize_key((string) ($args['status'] ?? ''));
|
|
if ($status !== '') {
|
|
$where[] = 'status = %s';
|
|
$params[] = $status;
|
|
}
|
|
|
|
$search = sanitize_text_field((string) ($args['search'] ?? ''));
|
|
if ($search !== '') {
|
|
$where[] = 'code LIKE %s';
|
|
$params[] = '%' . $wpdb->esc_like($search) . '%';
|
|
}
|
|
|
|
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
|
$offset = max(0, (int) ($args['offset'] ?? 0));
|
|
|
|
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
|
|
|
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
|
|
if (!empty($params)) {
|
|
$total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
|
|
} else {
|
|
$total = (int) $wpdb->get_var($count_sql);
|
|
}
|
|
|
|
$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 [
|
|
'items' => array_map([$this, 'normalize_row'], (array) $rows),
|
|
'total' => $total,
|
|
'page' => max(1, (int) ceil($offset / $limit) + 1),
|
|
'total_pages' => max(1, (int) ceil($total / $limit)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 验证优惠券是否可用
|
|
*
|
|
* @return array{valid: bool, coupon?: array, message?: string}
|
|
*/
|
|
public function validate(string $code, float $order_amount = 0, array $args = []): array {
|
|
global $wpdb;
|
|
|
|
$code = sanitize_text_field($code);
|
|
if ($code === '') {
|
|
return ['valid' => false, 'message' => '请输入优惠码'];
|
|
}
|
|
|
|
$coupon = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$this->table()} WHERE code = %s",
|
|
$code
|
|
), ARRAY_A);
|
|
|
|
if (!$coupon) {
|
|
return ['valid' => false, 'message' => '优惠码不存在'];
|
|
}
|
|
|
|
$coupon = $this->normalize_row($coupon);
|
|
|
|
if ($coupon['status'] !== 'active') {
|
|
return ['valid' => false, 'message' => '优惠码已失效'];
|
|
}
|
|
|
|
if (!empty($coupon['expires_at']) && strtotime($coupon['expires_at']) < time()) {
|
|
return ['valid' => false, 'message' => '优惠码已过期'];
|
|
}
|
|
|
|
if ($coupon['usage_limit'] > 0 && $coupon['used_count'] >= $coupon['usage_limit']) {
|
|
return ['valid' => false, 'message' => '优惠码已用完'];
|
|
}
|
|
|
|
if ($coupon['min_amount'] > 0 && $order_amount < $coupon['min_amount']) {
|
|
return ['valid' => false, 'message' => '订单金额不满足最低消费要求'];
|
|
}
|
|
|
|
// Per-user limit check
|
|
$user_id = (int) ($args['user_id'] ?? 0);
|
|
if ($coupon['per_user_limit'] > 0 && $user_id > 0) {
|
|
$user_used = $this->get_user_usage_count($coupon['id'], $user_id);
|
|
if ($user_used >= $coupon['per_user_limit']) {
|
|
return ['valid' => false, 'message' => '您已使用该优惠码达到上限'];
|
|
}
|
|
}
|
|
|
|
// Product / content scope check.
|
|
// Supports shop product cats and content post cats.
|
|
if (!empty($coupon['product_scope']) && !empty($args['product_ids'])) {
|
|
$scope = is_array($coupon['product_scope']) ? $coupon['product_scope'] : json_decode($coupon['product_scope'], true);
|
|
if (is_array($scope) && !empty($scope)) {
|
|
$scope_type = sanitize_key((string) ($scope['type'] ?? ''));
|
|
$scope_ids = array_map('intval', (array) ($scope['ids'] ?? []));
|
|
$product_ids = array_map('intval', (array) $args['product_ids']);
|
|
$taxonomies = array_values(array_filter(array_map('sanitize_key', (array) ($scope['taxonomies'] ?? []))));
|
|
if ($taxonomies === []) {
|
|
// content context prefers category; shop admin may pass aksrt_product_cat.
|
|
$context = sanitize_key((string) ($args['context'] ?? ''));
|
|
$taxonomies = ($context === 'content')
|
|
? ['category']
|
|
: ['aksrt_product_cat', 'category'];
|
|
}
|
|
|
|
if ($scope_type === 'include' && empty(array_intersect($product_ids, $scope_ids))) {
|
|
return ['valid' => false, 'message' => __('该优惠码不适用于当前商品', 'aksrt')];
|
|
}
|
|
if ($scope_type === 'exclude' && !empty(array_intersect($product_ids, $scope_ids))) {
|
|
return ['valid' => false, 'message' => __('该优惠码不适用于当前商品', 'aksrt')];
|
|
}
|
|
if (in_array($scope_type, ['category', 'content_category', 'post_category'], true)) {
|
|
$matched = false;
|
|
foreach ($product_ids as $pid) {
|
|
foreach ($taxonomies as $tax) {
|
|
$cats = wp_get_post_terms($pid, $tax, ['fields' => 'ids']);
|
|
if (!is_wp_error($cats) && !empty(array_intersect(array_map('intval', (array) $cats), $scope_ids))) {
|
|
$matched = true;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
if (!$matched) {
|
|
return ['valid' => false, 'message' => __('该优惠码不适用于当前分类', 'aksrt')];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate discount
|
|
$discount = 0;
|
|
if ($coupon['type'] === self::TYPE_PERCENT) {
|
|
$discount = $order_amount * ($coupon['value'] / 100);
|
|
} else {
|
|
$discount = min($coupon['value'], $order_amount);
|
|
}
|
|
|
|
// Max discount cap
|
|
if ($coupon['max_discount'] > 0 && $discount > $coupon['max_discount']) {
|
|
$discount = $coupon['max_discount'];
|
|
}
|
|
|
|
return [
|
|
'valid' => true,
|
|
'coupon' => $coupon,
|
|
'discount' => round($discount, 2),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 核销优惠券
|
|
*/
|
|
public function use_coupon(string $code): bool {
|
|
global $wpdb;
|
|
|
|
$code = sanitize_text_field($code);
|
|
$result = $wpdb->query($wpdb->prepare(
|
|
"UPDATE {$this->table()} SET used_count = used_count + 1 WHERE code = %s",
|
|
$code
|
|
));
|
|
|
|
return $result !== false;
|
|
}
|
|
|
|
/**
|
|
* 禁用优惠券
|
|
*/
|
|
public function disable(int $id): bool {
|
|
global $wpdb;
|
|
return $wpdb->update($this->table(), ['status' => 'disabled'], ['id' => $id], ['%s'], ['%d']) !== false;
|
|
}
|
|
|
|
public function enable(int $id): bool {
|
|
global $wpdb;
|
|
return $wpdb->update($this->table(), ['status' => 'active'], ['id' => $id], ['%s'], ['%d']) !== false;
|
|
}
|
|
|
|
/**
|
|
* Get one coupon by id.
|
|
*/
|
|
public function get(int $id): ?array {
|
|
global $wpdb;
|
|
if ($id <= 0) {
|
|
return null;
|
|
}
|
|
$row = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$this->table()} WHERE id = %d",
|
|
$id
|
|
), ARRAY_A);
|
|
if (!$row) {
|
|
return null;
|
|
}
|
|
return $this->normalize_row($row);
|
|
}
|
|
|
|
/**
|
|
* Update mutable coupon fields (not code / used_count / created_at).
|
|
*/
|
|
public function update(int $id, array $data): bool {
|
|
global $wpdb;
|
|
if ($id <= 0 || !$this->get($id)) {
|
|
return false;
|
|
}
|
|
|
|
$fields = [];
|
|
$format = [];
|
|
|
|
if (array_key_exists('type', $data)) {
|
|
$type = sanitize_key((string) $data['type']);
|
|
if (!in_array($type, [self::TYPE_PERCENT, self::TYPE_FIXED], true)) {
|
|
$type = self::TYPE_FIXED;
|
|
}
|
|
$fields['type'] = $type;
|
|
$format[] = '%s';
|
|
}
|
|
if (array_key_exists('value', $data)) {
|
|
$fields['value'] = (float) $data['value'];
|
|
$format[] = '%f';
|
|
}
|
|
if (array_key_exists('min_amount', $data)) {
|
|
$fields['min_amount'] = (float) $data['min_amount'];
|
|
$format[] = '%f';
|
|
}
|
|
if (array_key_exists('max_discount', $data)) {
|
|
$fields['max_discount'] = (float) $data['max_discount'];
|
|
$format[] = '%f';
|
|
}
|
|
if (array_key_exists('usage_limit', $data)) {
|
|
$fields['usage_limit'] = (int) $data['usage_limit'];
|
|
$format[] = '%d';
|
|
}
|
|
if (array_key_exists('per_user_limit', $data)) {
|
|
$fields['per_user_limit'] = (int) $data['per_user_limit'];
|
|
$format[] = '%d';
|
|
}
|
|
if (array_key_exists('expires_at', $data)) {
|
|
$fields['expires_at'] = sanitize_text_field((string) $data['expires_at']);
|
|
$format[] = '%s';
|
|
}
|
|
if (array_key_exists('product_scope', $data)) {
|
|
$scope = is_array($data['product_scope']) ? $data['product_scope'] : [];
|
|
$fields['product_scope'] = wp_json_encode($scope, JSON_UNESCAPED_UNICODE);
|
|
$format[] = '%s';
|
|
}
|
|
if (array_key_exists('status', $data)) {
|
|
$status = sanitize_key((string) $data['status']);
|
|
if (in_array($status, ['active', 'disabled'], true)) {
|
|
$fields['status'] = $status;
|
|
$format[] = '%s';
|
|
}
|
|
}
|
|
|
|
if (!$fields) {
|
|
return false;
|
|
}
|
|
|
|
return $wpdb->update($this->table(), $fields, ['id' => $id], $format, ['%d']) !== false;
|
|
}
|
|
|
|
/**
|
|
* Batch-create coupons with auto-generated codes.
|
|
*
|
|
* @return array{created:int,codes:array<int,string>}
|
|
*/
|
|
public function batch_create(array $data, int $count = 1, int $length = 10, string $prefix = ''): array {
|
|
$count = max(1, min(100, $count));
|
|
$length = max(6, min(24, $length));
|
|
$prefix = sanitize_text_field($prefix);
|
|
$codes = [];
|
|
$created = 0;
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$code = $prefix !== ''
|
|
? $prefix . strtoupper(wp_generate_password($length, false, false))
|
|
: strtoupper(wp_generate_password($length, false, false));
|
|
$payload = $data;
|
|
$payload['code'] = $code;
|
|
$id = $this->create($payload);
|
|
if ($id > 0) {
|
|
$created++;
|
|
$codes[] = $code;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'created' => $created,
|
|
'codes' => $codes,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Count how many times a user has used a specific coupon.
|
|
*/
|
|
public function get_user_usage_count(int $coupon_id, int $user_id): int {
|
|
global $wpdb;
|
|
$table = AKSRT_DB_Migrator::table('coupon_usage');
|
|
return (int) $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$table} WHERE coupon_id = %d AND user_id = %d",
|
|
$coupon_id, $user_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Log a coupon usage record.
|
|
*/
|
|
public function log_usage(int $coupon_id, int $user_id, string $order_number, float $discount): void {
|
|
global $wpdb;
|
|
$table = AKSRT_DB_Migrator::table('coupon_usage');
|
|
$wpdb->insert($table, [
|
|
'coupon_id' => $coupon_id,
|
|
'user_id' => $user_id,
|
|
'order_number' => $order_number,
|
|
'discount' => $discount,
|
|
'created_at' => current_time('mysql'),
|
|
], ['%d', '%d', '%s', '%f', '%s']);
|
|
}
|
|
|
|
private function normalize_row(array $row): array {
|
|
$row['id'] = (int) $row['id'];
|
|
$row['value'] = (float) $row['value'];
|
|
$row['min_amount'] = (float) $row['min_amount'];
|
|
$row['usage_limit'] = (int) $row['usage_limit'];
|
|
$row['used_count'] = (int) $row['used_count'];
|
|
$row['per_user_limit'] = (int) ($row['per_user_limit'] ?? 0);
|
|
$row['max_discount'] = (float) ($row['max_discount'] ?? 0);
|
|
if (!empty($row['product_scope']) && is_string($row['product_scope'])) {
|
|
$row['product_scope'] = json_decode($row['product_scope'], true) ?: [];
|
|
}
|
|
return $row;
|
|
}
|
|
}
|