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

387 lines
14 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
/**
* AKSRT DB Migrator
*
* 维护 CMS / 积分 / 下载 token 等运行时所需的数据表。
*
* 使用 dbDelta 幂等创建,所有表带主题前缀 aksrt_。
*
* 表清单(v3.5 新增):
* - {prefix}aksrt_credit_log 积分流水
* - {prefix}aksrt_signin_log 每日签到记录
* - {prefix}aksrt_invite 邀请关系
* - {prefix}aksrt_dl_token 下载短期 token
* - {prefix}aksrt_notification 用户通知收件箱
*
* 触发方式:
* 1. switch_theme / after_switch_theme 钩子
* 2. 检测 option 'aksrt_db_schema_version' 与 AKSRT_DB_SCHEMA_VERSION 不一致
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_DB_Migrator {
use AKSRT_DB_Migrator_Part_1;
use AKSRT_DB_Migrator_Part_2;
use AKSRT_DB_Migrator_Part_3;
/** 当前 schema 版本,更改后会触发 dbDelta。 */
const SCHEMA_VERSION = '1.13.0';
const OPT_KEY = 'aksrt_db_schema_version';
/**
* 初始化钩子。
*/
public static function init(): void {
add_action('after_switch_theme', [self::class, 'maybe_migrate']);
add_action('admin_init', [self::class, 'maybe_migrate']);
}
/**
* 若版本不一致则执行迁移。
*/
public static function maybe_migrate(): void {
$current = get_option(self::OPT_KEY, '');
if ($current !== self::SCHEMA_VERSION) {
self::run();
update_option(self::OPT_KEY, self::SCHEMA_VERSION);
}
// 选项级(option)数据迁移:独立版本号控制,与表结构解耦。
self::maybe_migrate_options();
}
const OPT_LAYOUT_MIGRATED = 'aksrt_archive_layout_migrated';
const OPT_RESOURCE_MIGRATED = 'aksrt_resource_cpt_migrated';
const OPT_CMS_FORMAT_MIGRATED = 'aksrt_cms_format_migrated';
const OPT_RESOURCE_PAY_META_CURSOR = 'aksrt_resource_pay_meta_cursor';
const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated';
/**
* 一次性 option 迁移:
* - 旧 aksrt_basic_settings.post_layout / aksrt_settings.default_archive_layout
* → 统一为 aksrt_settings.archive_layout
* - 旧 aksrt_resource CPT 文章 → post + _aksrt_is_resource = 1
* - v4 形态归一:清理 site_profile / opening_mode / archive_layout 等历史字段
* 并删除全部分类的 aksrt_flow term meta
*/
public static function maybe_migrate_options(): void {
self::maybe_migrate_layout();
self::maybe_migrate_resource_cpt();
self::maybe_migrate_cms_format();
self::maybe_migrate_resource_pay_meta();
self::maybe_migrate_orders_to_repository();
self::maybe_migrate_board_taxonomy_to_plate_cpt();
}
private static function maybe_migrate_layout(): void {
if (get_option(self::OPT_LAYOUT_MIGRATED)) return;
$settings = get_option('aksrt_settings', []);
if (!is_array($settings)) $settings = [];
$basic = get_option('aksrt_basic_settings', []);
if (!is_array($basic)) $basic = [];
$allowed = ['card', 'list', 'masonry', 'grid-cms'];
$existing_archive = $settings['archive_layout'] ?? '';
if (!in_array($existing_archive, $allowed, true)) {
// 优先取旧 default_archive_layout,其次旧 post_layout
$candidate = $settings['default_archive_layout']
?? $basic['post_layout']
?? $settings['post_layout']
?? 'card';
if (!in_array($candidate, $allowed, true)) $candidate = 'card';
$settings['archive_layout'] = $candidate;
}
unset($settings['default_archive_layout']);
unset($settings['post_layout']);
update_option('aksrt_settings', $settings);
if (isset($basic['post_layout'])) {
unset($basic['post_layout']);
update_option('aksrt_basic_settings', $basic);
}
update_option(self::OPT_LAYOUT_MIGRATED, time());
}
/**
* 把历史 aksrt_resource CPT 文章迁移到 post:
* - post_type = aksrt_resource → post
* - _aksrt_is_resource = 1
*
* 仅在 admin/cron 上下文且有数据时分批执行;幂等。
*/
private static function maybe_migrate_resource_cpt(): void {
if (get_option(self::OPT_RESOURCE_MIGRATED)) return;
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) return;
global $wpdb;
// 仅查 post_type 字段,避免拉全量行
$ids = $wpdb->get_col("SELECT ID FROM {$wpdb->posts} WHERE post_type = 'aksrt_resource' LIMIT 500");
if (empty($ids)) {
// 无历史数据:直接打标记
update_option(self::OPT_RESOURCE_MIGRATED, time());
return;
}
foreach ($ids as $id) {
$id = (int) $id;
// 转 post_type
$wpdb->update(
$wpdb->posts,
['post_type' => 'post'],
['ID' => $id],
['%s'],
['%d']
);
update_post_meta($id, '_aksrt_is_resource', 1);
clean_post_cache($id);
}
// 若一批迁移满 500,留待下次触发;否则收尾
if (count($ids) < 500) {
update_option(self::OPT_RESOURCE_MIGRATED, time());
// 触发重写规则刷新(因为 /resource/{slug} 旧链接需要 301 重定向)
delete_option('rewrite_rules');
}
}
/**
* v4 形态归一迁移:
* - aksrt_settings 中的 site_profile / opening_mode / archive_layout
* / default_archive_layout / post_layout 全部 unset
* - aksrt_basic_settings 中的 opening_mode / post_layout 全部 unset
* - 全部分类的 aksrt_flow term meta 删除
*
* 幂等。
*/
private static function maybe_migrate_cms_format(): void {
if (get_option(self::OPT_CMS_FORMAT_MIGRATED)) return;
// 1. aksrt_settings
$settings = get_option('aksrt_settings', []);
if (is_array($settings)) {
$changed = false;
foreach (['site_profile', 'opening_mode', 'archive_layout', 'default_archive_layout', 'post_layout'] as $k) {
if (array_key_exists($k, $settings)) {
unset($settings[$k]);
$changed = true;
}
}
if ($changed) {
update_option('aksrt_settings', $settings);
}
}
// 2. aksrt_basic_settings
$basic = get_option('aksrt_basic_settings', []);
if (is_array($basic)) {
$changed = false;
foreach (['opening_mode', 'post_layout'] as $k) {
if (array_key_exists($k, $basic)) {
unset($basic[$k]);
$changed = true;
}
}
if ($changed) {
update_option('aksrt_basic_settings', $basic);
}
}
// 3. 删除全部分类的 aksrt_flow term meta
global $wpdb;
if ($wpdb instanceof wpdb) {
$wpdb->delete($wpdb->termmeta, ['meta_key' => 'aksrt_flow'], ['%s']);
}
update_option(self::OPT_CMS_FORMAT_MIGRATED, time());
}
private static function maybe_migrate_resource_pay_meta(): void {
if (get_option(AKSRT_Domain_Model::RESOURCE_PAY_META_MIGRATED)) return;
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) return;
global $wpdb;
$batch_size = 200;
$cursor = (int) get_option(self::OPT_RESOURCE_PAY_META_CURSOR, 0);
$candidate_keys = [
AKSRT_Domain_Model::PAY_META_KEY,
AKSRT_Domain_Model::PAY_TYPE_KEY,
AKSRT_Domain_Model::PAY_PRICE_KEY,
AKSRT_Domain_Model::RESOURCE_MARKER_KEY,
'_aksrt_res_pay_type',
'_aksrt_res_price',
'_aksrt_res_original_price',
'_aksrt_res_vip1_price',
'_aksrt_res_vip2_price',
'_aksrt_res_version',
'_aksrt_res_size',
'_aksrt_res_license',
'_aksrt_res_compat',
'_aksrt_res_demo_url',
'_aksrt_res_downloads',
'_aksrt_res_require_login',
'_aksrt_res_require_captcha',
'download_count',
];
$placeholders = implode(', ', array_fill(0, count($candidate_keys), '%s'));
$sql = "SELECT DISTINCT pm.post_id
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.post_type IN ('post', 'aksrt_resource')
AND pm.meta_key IN ({$placeholders})
AND pm.post_id > %d
ORDER BY pm.post_id ASC
LIMIT %d";
$params = array_merge($candidate_keys, [$cursor, $batch_size]);
$ids = $wpdb->get_col($wpdb->prepare($sql, $params));
if (empty($ids)) {
delete_option(self::OPT_RESOURCE_PAY_META_CURSOR);
update_option(AKSRT_Domain_Model::RESOURCE_PAY_META_MIGRATED, time());
return;
}
foreach ($ids as $post_id) {
self::migrate_resource_pay_meta_post((int) $post_id);
}
update_option(self::OPT_RESOURCE_PAY_META_CURSOR, max(array_map('intval', $ids)));
if (count($ids) < $batch_size) {
delete_option(self::OPT_RESOURCE_PAY_META_CURSOR);
update_option(AKSRT_Domain_Model::RESOURCE_PAY_META_MIGRATED, time());
}
}
private static function maybe_migrate_orders_to_repository(): void {
if (get_option(AKSRT_Domain_Model::ORDER_MIGRATED_OPTION)) return;
if (!class_exists('AKSRT_Order_Repository')) return;
AKSRT_Order_Repository::get_instance()->migrate_from_option();
update_option(AKSRT_Domain_Model::ORDER_MIGRATED_OPTION, time());
}
/**
* §7.0 板块 taxonomy → plate CPT 一次性迁移。
*
* 将 aksrt_forum_board taxonomy terms 转为 aksrt_forum_plate CPT posts,
* 保留 term meta → post meta 映射、层级关系、版主数据。
* 同时在每个帖子上写入 _aksrt_plate_id 指向新 plate post。
* 旧 taxonomy 数据保留不删(向后兼容)。
*/
private static function maybe_migrate_board_taxonomy_to_plate_cpt(): void {
if (get_option(self::OPT_PLATE_CPT_MIGRATED)) return;
if (!is_admin() && !(defined('DOING_CRON') && DOING_CRON)) return;
if (!post_type_exists('aksrt_forum_plate')) return;
$terms = get_terms([
'taxonomy' => 'aksrt_forum_board',
'hide_empty' => false,
'orderby' => 'meta_value_num',
'meta_key' => '_aksrt_sort_order',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
update_option(self::OPT_PLATE_CPT_MIGRATED, time());
return;
}
$term_to_plate = [];
foreach ($terms as $term) {
$existing = get_posts([
'post_type' => 'aksrt_forum_plate',
'meta_key' => '_aksrt_migrated_from_term',
'meta_value' => (string) $term->term_id,
'numberposts' => 1,
'post_status' => 'any',
'fields' => 'ids',
]);
if (!empty($existing)) {
$term_to_plate[$term->term_id] = (int) $existing[0];
continue;
}
$plate_id = wp_insert_post([
'post_type' => 'aksrt_forum_plate',
'post_title' => $term->name,
'post_content' => $term->description ?: '',
'post_excerpt' => wp_trim_words($term->description ?: '', 30, ''),
'post_status' => 'publish',
'post_parent' => 0,
'menu_order' => (int) get_term_meta($term->term_id, '_aksrt_sort_order', true),
], true);
if (is_wp_error($plate_id)) {
continue;
}
$term_to_plate[$term->term_id] = $plate_id;
update_post_meta($plate_id, '_aksrt_migrated_from_term', $term->term_id);
update_post_meta($plate_id, '_aksrt_sort_order', (int) get_term_meta($term->term_id, '_aksrt_sort_order', true));
update_post_meta($plate_id, '_aksrt_icon', (string) get_term_meta($term->term_id, '_aksrt_icon', true));
$moderators = get_term_meta($term->term_id, '_aksrt_moderators', true);
if (is_array($moderators) && !empty($moderators)) {
update_post_meta($plate_id, '_aksrt_moderators', $moderators);
}
$roles = get_term_meta($term->term_id, '_aksrt_moderator_roles', true);
if (is_array($roles) && !empty($roles)) {
update_post_meta($plate_id, '_aksrt_moderator_roles', $roles);
}
$topic_count = (int) get_term_meta($term->term_id, '_aksrt_topic_count', true);
if ($topic_count <= 0) {
$topic_count = (int) $term->count;
}
update_post_meta($plate_id, '_aksrt_posts_count', $topic_count);
}
// Resolve parent hierarchy
foreach ($terms as $term) {
if ($term->parent > 0 && isset($term_to_plate[$term->term_id], $term_to_plate[$term->parent])) {
wp_update_post([
'ID' => $term_to_plate[$term->term_id],
'post_parent' => $term_to_plate[$term->parent],
]);
}
}
// Assign _aksrt_plate_id on existing topics
global $wpdb;
foreach ($term_to_plate as $term_id => $plate_id) {
$topic_ids = $wpdb->get_col($wpdb->prepare(
"SELECT tr.object_id FROM {$wpdb->term_relationships} tr
INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_id = %d AND tt.taxonomy = 'aksrt_forum_board'",
$term_id
));
foreach ($topic_ids as $topic_id) {
update_post_meta((int) $topic_id, '_aksrt_plate_id', $plate_id);
}
}
delete_option('rewrite_rules');
update_option(self::OPT_PLATE_CPT_MIGRATED, time());
}
}