Files
wordpress-theme-aksrt/inc/classes/class-aksrt-coupon.php
T

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;
}
}