Merge pull request 'refactor(db): drop option/data migration from DB migrator' (#59) from worktree-migrator-hygiene into main

Reviewed-on: CYL/wordpress-theme-aksrt#59
This commit is contained in:
2026-07-11 18:49:41 +08:00
4 changed files with 19 additions and 552 deletions
-1
View File
@@ -200,7 +200,6 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-email-rollback.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-credits-admin.php';
// 加载数据表迁移器(CMS / 积分 / 下载 token)
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-1.php';
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-3.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-db-migrator.php';
+17 -351
View File
@@ -2,20 +2,12 @@
/**
* AKSRT DB Migrator
*
* 维护 CMS / 积分 / 下载 token 等运行时所需的数据表。
* Maintains runtime tables (credits / download tokens / shop / forum support, etc.).
* Schema only via dbDelta — no option/data migration, no legacy option dual-write.
*
* 使用 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 不一致
* Trigger:
* 1. after_switch_theme
* 2. admin_init when aksrt_db_schema_version !== SCHEMA_VERSION
*
* @package AKSRT
*/
@@ -23,18 +15,22 @@
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。 */
/** Current schema version; changing it re-runs dbDelta. */
const SCHEMA_VERSION = '1.13.0';
const OPT_KEY = 'aksrt_db_schema_version';
/**
* 初始化钩子。
* Historical flag still readable by older code paths.
* Plate CPT is now native when registered; prefer post_type_exists().
*/
const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated';
/**
* Register hooks.
*/
public static function init(): void {
add_action('after_switch_theme', [self::class, 'maybe_migrate']);
@@ -42,345 +38,15 @@ class AKSRT_DB_Migrator {
}
/**
* 若版本不一致则执行迁移。
* Run table schema upgrade when version mismatches.
*/
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());
if ($current === self::SCHEMA_VERSION) {
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');
}
self::run();
update_option(self::OPT_KEY, self::SCHEMA_VERSION);
}
/**
* 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());
}
}
+2 -2
View File
@@ -132,8 +132,8 @@ class AKSRT_Forum {
}
public function is_plate_cpt_active(): bool {
return post_type_exists('aksrt_forum_plate')
&& (bool) get_option(AKSRT_DB_Migrator::OPT_PLATE_CPT_MIGRATED);
// Plate CPT is the native board model; no option migration gate.
return post_type_exists('aksrt_forum_plate');
}
public function get_config(): array {
@@ -1,198 +0,0 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_DB_Migrator_Part_1 {
private static function migrate_resource_pay_meta_post(int $post_id): void {
$post = get_post($post_id);
if (!($post instanceof WP_Post)) {
return;
}
$all_meta = get_post_meta($post_id);
if (!is_array($all_meta)) {
$all_meta = [];
}
$canonical_meta = self::meta_value($all_meta, AKSRT_Domain_Model::PAY_META_KEY);
if (!is_array($canonical_meta)) {
$canonical_meta = [];
}
$legacy_downloads = self::meta_value($all_meta, '_aksrt_res_downloads');
if (!is_array($legacy_downloads)) {
$legacy_downloads = [];
}
$raw_pay_type = self::first_non_empty_string([
$canonical_meta['pay_type'] ?? '',
self::meta_value($all_meta, AKSRT_Domain_Model::PAY_TYPE_KEY),
self::meta_value($all_meta, '_aksrt_res_pay_type'),
]);
$has_resource_marker = self::first_boolean([
self::meta_value($all_meta, AKSRT_Domain_Model::RESOURCE_MARKER_KEY),
$post->post_type === 'aksrt_resource',
!empty($legacy_downloads),
]);
if ($raw_pay_type === '' && $has_resource_marker) {
$raw_pay_type = AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD;
}
$pay_meta = AKSRT_Domain_Model::sanitize_pay_meta([
'pay_type' => $raw_pay_type,
'price' => self::first_numeric([
$canonical_meta['price'] ?? null,
self::meta_value($all_meta, AKSRT_Domain_Model::PAY_PRICE_KEY),
self::meta_value($all_meta, '_aksrt_res_price'),
]),
'original_price' => self::first_numeric([
$canonical_meta['original_price'] ?? null,
self::meta_value($all_meta, '_aksrt_res_original_price'),
]),
'vip1_price' => self::first_numeric([
$canonical_meta['vip1_price'] ?? null,
self::meta_value($all_meta, '_aksrt_res_vip1_price'),
]),
'vip2_price' => self::first_numeric([
$canonical_meta['vip2_price'] ?? null,
self::meta_value($all_meta, '_aksrt_res_vip2_price'),
]),
'version' => self::first_non_empty_string([
$canonical_meta['version'] ?? '',
self::meta_value($all_meta, '_aksrt_res_version'),
]),
'size' => self::first_non_empty_string([
$canonical_meta['size'] ?? '',
self::meta_value($all_meta, '_aksrt_res_size'),
]),
'license' => self::first_non_empty_string([
$canonical_meta['license'] ?? '',
self::meta_value($all_meta, '_aksrt_res_license'),
]),
'compat' => self::first_non_empty_string([
$canonical_meta['compat'] ?? '',
self::meta_value($all_meta, '_aksrt_res_compat'),
]),
'demo_url' => self::first_non_empty_string([
$canonical_meta['demo_url'] ?? '',
self::meta_value($all_meta, '_aksrt_res_demo_url'),
]),
'video_url' => self::first_non_empty_string([
$canonical_meta['video_url'] ?? '',
]),
'downloads' => self::first_array([
$canonical_meta['downloads'] ?? [],
$legacy_downloads,
]),
'attributes' => self::first_array([
$canonical_meta['attributes'] ?? [],
]),
'gallery' => self::first_array([
$canonical_meta['gallery'] ?? [],
]),
'require_login' => self::first_boolean([
$canonical_meta['require_login'] ?? null,
self::meta_value($all_meta, '_aksrt_res_require_login'),
]),
'require_captcha' => self::first_boolean([
$canonical_meta['require_captcha'] ?? null,
self::meta_value($all_meta, '_aksrt_res_require_captcha'),
]),
]);
if ($pay_meta['pay_type'] === AKSRT_Domain_Model::PAY_TYPE_NO && $has_resource_marker) {
$pay_meta['pay_type'] = AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD;
}
$pay_meta = AKSRT_Domain_Model::sanitize_pay_meta($pay_meta);
update_post_meta($post_id, AKSRT_Domain_Model::PAY_META_KEY, $pay_meta);
update_post_meta($post_id, AKSRT_Domain_Model::PAY_TYPE_KEY, (string) $pay_meta['pay_type']);
update_post_meta($post_id, AKSRT_Domain_Model::PAY_PRICE_KEY, (float) $pay_meta['price']);
update_post_meta(
$post_id,
AKSRT_Domain_Model::RESOURCE_MARKER_KEY,
AKSRT_Domain_Model::is_resource_pay_type((string) $pay_meta['pay_type']) ? 1 : 0
);
clean_post_cache($post_id);
}
/**
* @param array<string, array<int, mixed>> $all_meta
* @return mixed
*/
private static function meta_value(array $all_meta, string $key) {
if (!isset($all_meta[$key][0])) {
return null;
}
return maybe_unserialize($all_meta[$key][0]);
}
/**
* @param array<int, mixed> $candidates
*/
private static function first_non_empty_string(array $candidates): string {
foreach ($candidates as $candidate) {
if (is_array($candidate)) {
continue;
}
$value = trim((string) $candidate);
if ($value !== '') {
return $value;
}
}
return '';
}
/**
* @param array<int, mixed> $candidates
*/
private static function first_numeric(array $candidates): float {
foreach ($candidates as $candidate) {
if ($candidate === null || $candidate === '') {
continue;
}
if (is_numeric($candidate)) {
return (float) $candidate;
}
}
return 0.0;
}
/**
* @param array<int, mixed> $candidates
*/
private static function first_boolean(array $candidates): bool {
foreach ($candidates as $candidate) {
if ($candidate === null || $candidate === '') {
continue;
}
return !empty($candidate);
}
return false;
}
/**
* @param array<int, mixed> $candidates
* @return array<int|string, mixed>
*/
private static function first_array(array $candidates): array {
foreach ($candidates as $candidate) {
if (is_array($candidate) && !empty($candidate)) {
return $candidate;
}
}
return [];
}
}