yoone-wc-subscriptions/includes/models/class-yoone-subscriptions-d...

238 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 数据库模型:订阅实例记录(基础表结构与 CRUD
*
* 使用场景与业务说明:
* - 产品层面的订阅计划(周期、价格、折扣等)存放在 WooCommerce 的 postmeta 中;
* - 本数据表用于保存“用户的实际订阅实例”,包括开始时间、下一次续费时间、数量、每周期应付金额、状态等;
* - 后续可以基于此表实现:自动续费(根据 next_renewal_date 触发)、冻结/暂停、取消、续订记录审计、对账等;
* - 当前阶段仅提供表结构和读写 API不在前端自动创建订阅实例便于之后逐步接入。
*
* 表结构字段含义yoone_subscriptions
* - id主键自增
* - user_id订阅所属的用户WP 用户 ID
* - product_id关联的商品 ID通常为具有订阅属性的产品
* - period订阅周期目前支持 'month'(月)与 'year'(年);
* - qty订阅的数量例如包月数量、份数等默认为 1
* - price_per_cycle每个周期需要支付的金额DECIMAL(18,8)),保留 8 位小数以兼容各种币种与税费;
* - status订阅当前状态示例active活跃、paused暂停、canceled已取消
* - start_date订阅开始时间DATETIME
* - next_renewal_date下一次续费时间DATETIME可空由定时任务或业务逻辑维护
* - created_at / updated_at记录创建与最后更新时间戳
* 索引:
* - KEY user_id便于按用户查询其订阅列表
* - KEY product_id便于按商品维度统计与筛选
* - KEY status便于按状态快速过滤例如批量续费仅处理 active
*/
defined('ABSPATH') || exit;
class Yoone_Subscriptions_DB {
const TABLE = 'yoone_subscriptions'; // 实际表名为 {$wpdb->prefix}yoone_subscriptions
protected static $instance = null;
public static function instance() {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
private function __construct() {}
/**
* 返回完整表名(含前缀)。
*/
public static function table_name() {
global $wpdb;
return $wpdb->prefix . self::TABLE;
}
/**
* 安装/升级表结构。
*
* 业务要点:
* - 使用 dbDelta 执行 CREATE TABLE当表结构有变更时可自动比对并调整
* - 使用 $wpdb->prefix 保证与站点前缀一致,避免与其他站点冲突;
* - 使用 get_charset_collate() 为表设置正确字符集与排序规则;
* - 字段设计保守,满足通用订阅场景,后续可通过迁移增加更多业务字段(如上次扣费结果、失败次数等)。
*/
public static function install() {
global $wpdb;
$table = self::table_name();
$charset_collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
// 说明:下方 SQL 为完整表结构定义,字段含义见文件头部注释。
$sql = "CREATE TABLE {$table} (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
product_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
period VARCHAR(10) NOT NULL DEFAULT 'month',
qty INT(11) UNSIGNED NOT NULL DEFAULT 1,
price_per_cycle DECIMAL(18,8) NOT NULL DEFAULT 0.00000000,
status VARCHAR(20) NOT NULL DEFAULT 'active',
start_date DATETIME NOT NULL,
next_renewal_date DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY product_id (product_id),
KEY status (status)
) {$charset_collate};";
dbDelta($sql);
}
/**
* 创建订阅实例。
*
* 参数与校验规则:
* - user_id / product_id转为非负整数absint缺省为 0
* - period仅允许 'month' 或 'year',否则回落到 'month'
* - qty数量最小为 1
* - price_per_cycle每周期金额使用 floatval
* - status使用 sanitize_key 过滤建议统一枚举active / paused / canceled 等;
* - start_date / next_renewal_date字符串形式的 DATETIME使用 sanitize_text_field
* - created_at / updated_at由系统自动填充当前时间
* 返回值:插入成功返回插入的自增 ID失败返回 false。
*
* 业务提示:
* - 该方法不做业务级冲突检测(例如同用户同产品是否已存在 active 实例),留给上层业务处理;
* - 可在创建后根据 period 自动计算 next_renewal_date例如 +1 month/+1 year
*/
public static function create($data) {
global $wpdb;
$table = self::table_name();
$now = current_time('mysql');
$defaults = array(
'user_id' => 0,
'product_id' => 0,
'period' => 'month',
'qty' => 1,
'price_per_cycle' => 0.0,
'status' => 'active',
'start_date' => $now,
'next_renewal_date' => null,
'created_at' => $now,
'updated_at' => $now,
);
$data = wp_parse_args($data, $defaults);
// 执行插入,构造字段与格式数组以确保类型安全
$inserted = $wpdb->insert(
$table,
array(
'user_id' => absint($data['user_id']),
'product_id' => absint($data['product_id']),
'period' => in_array($data['period'], array('month','year'), true) ? $data['period'] : 'month',
'qty' => max(1, intval($data['qty'])),
'price_per_cycle' => floatval($data['price_per_cycle']),
'status' => sanitize_key($data['status']),
'start_date' => sanitize_text_field($data['start_date']),
'next_renewal_date' => $data['next_renewal_date'] ? sanitize_text_field($data['next_renewal_date']) : null,
'created_at' => $now,
'updated_at' => $now,
),
array('%d','%d','%s','%d','%f','%s','%s','%s','%s','%s')
);
if (false === $inserted) return false;
return $wpdb->insert_id;
}
/**
* 更新订阅实例(部分字段更新)。
*
* 使用说明:
* - 仅允许更新允许字段allowed 列表),其他键会被忽略;
* - 自动刷新 updated_at 为当前时间;
* - 根据不同字段类型设置对应的 SQL 格式(%d/%f/%s保证类型安全
* - 返回布尔true 表示更新成功wpdb->update 返回非 false
*/
public static function update($id, $data) {
global $wpdb;
$table = self::table_name();
$id = absint($id);
if ($id <= 0) return false;
$data['updated_at'] = current_time('mysql');
// 过滤并构造格式:仅采纳白名单字段
$fields = array();
$formats = array();
$allowed = array('user_id','product_id','period','qty','price_per_cycle','status','start_date','next_renewal_date','updated_at');
foreach ($allowed as $key) {
if (! array_key_exists($key, $data)) continue;
$val = $data[$key];
switch ($key) {
case 'user_id':
case 'product_id':
$fields[$key] = absint($val); $formats[] = '%d'; break;
case 'qty':
$fields[$key] = max(1, intval($val)); $formats[] = '%d'; break;
case 'price_per_cycle':
$fields[$key] = floatval($val); $formats[] = '%f'; break;
case 'period':
$fields[$key] = in_array($val, array('month','year'), true) ? $val : 'month'; $formats[] = '%s'; break;
case 'status':
$fields[$key] = sanitize_key($val); $formats[] = '%s'; break;
case 'start_date':
case 'next_renewal_date':
case 'updated_at':
$fields[$key] = sanitize_text_field($val); $formats[] = '%s'; break;
}
}
if (empty($fields)) return false; // 无有效更新字段时直接返回
return $wpdb->update($table, $fields, array('id' => $id), $formats, array('%d')) !== false;
}
/**
* 获取单个订阅实例。
*
* @param int $id 订阅实例主键 ID
* @return array|null 关联数组(字段同表结构),找不到返回 null
*/
public static function get($id) {
global $wpdb;
$table = self::table_name();
$id = absint($id);
return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
}
/**
* 获取某用户的订阅实例列表。
*
* 过滤参数:
* - status按状态过滤active/paused/canceled 等),为空则不限制;
* - product_id按商品过滤0 或空则不限制;
* - limit / offset分页参数默认 limit=50, offset=0
* 返回值:二维数组,每项为一条订阅实例记录(关联数组)。
*/
public static function get_by_user($user_id, $args = array()) {
global $wpdb;
$table = self::table_name();
$user_id = absint($user_id);
$defaults = array('status' => '', 'product_id' => 0, 'limit' => 50, 'offset' => 0);
$args = wp_parse_args($args, $defaults);
$where = array('user_id = %d');
$params = array($user_id);
if (! empty($args['status'])) { $where[] = 'status = %s'; $params[] = sanitize_key($args['status']); }
if (! empty($args['product_id'])) { $where[] = 'product_id = %d'; $params[] = absint($args['product_id']); }
$limit = max(1, absint($args['limit']));
$offset = max(0, absint($args['offset']));
// 组装分页查询语句,按 id 倒序排列,便于查看最新实例
$sql = "SELECT * FROM {$table} WHERE " . implode(' AND ', $where) . " ORDER BY id DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
return $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
}
}