387 lines
14 KiB
PHP
387 lines
14 KiB
PHP
<?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());
|
||
}
|
||
|
||
}
|