The old 售后 flow was an order-refund appendage: tickets were hard-bound to an order number, users could only file them from 「我的订单」against a paid order, and the admin action bottomed out at 「标记订单已退款」. It could not carry account questions or feature requests that have nothing to do with an order. Replace it with a general ticket system modelled on 魔方财务云's idcsmart_ticket: title + department + description are required, linking an order is optional (user's own paid orders only). Add departments, custom statuses, preset replies and internal admin notes as four configuration surfaces. Conversation history moves off the ticket row's JSON log into dedicated ticket_reply rows, which unlocks quoted replies. Front end gets a 工单中心 panel under 我的服务; back end moves from 支付中心 → 售后服务 to users.php → 工单管理. Per CLAUDE.md's rewrite-over-compatibility rule: drop the old table, build new ones, delete the old code. No dual paths, no data migration. The upstream plugin's upstream/downstream delivery and internal-ticket conversion are deliberately left out — both are multi-provider platform concerns with no meaning on a single WordPress site. Data: order_aftersale becomes six aksrt_ticket* tables (ticket, ticket_reply, ticket_status, ticket_type, ticket_prereply, ticket_note). SCHEMA_VERSION 2.6.0 -> 3.0.0. Status and department seeds are idempotent; the default status 「待接单」is flagged as non-editable. Domain: new src/Ticket/ (13 files, all under 460 lines). Attachments reuse UserController::rest_upload_comment_image with a ticket-specific permission callback, mirroring the existing /report/upload-image route rather than growing a second upload path. Notifications go through AKSRT\Messages\Message::send() and deep-link to the right surface; email/SMS stay out of scope since the theme's mail and SMS stacks are separate implementations. Refund gate opens up: marking an order refunded no longer requires an in-flight ticket, only that the order is paid. Also generalise a hardcoded special case in tools/audit-admin-contracts.php — schema domain inference now reads the parent directory instead of matching "PostList" by name, so new leaf schema files need no per-name exception. Verification: php tools/audit-admin-contracts.php exit 0, errors=0 node --test (widget-system + admin) 664 pass / 0 fail php -l over all new/changed files clean node --check over all JS clean npm run build + build:public clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
437 lines
16 KiB
PHP
437 lines
16 KiB
PHP
<?php
|
||
/**
|
||
* AKSRT Theme - Main Functions File (v3.2 Restructured)
|
||
*
|
||
* 精简版入口文件,仅负责:
|
||
* 1. 瀹氫箟甯搁噺 & 杈呭姪鍑芥暟(aksrt_get_setting)
|
||
* 2. 2. 加载 bootstrap(自动加载所有模块)
|
||
* 3. 3. 初始化单例实例
|
||
* 4. 4. 主题激活时刷新重写规则
|
||
*
|
||
* 业务逻辑已拆分至 inc/core-hooks.php 和各独立类文件。
|
||
*
|
||
* @package AKSRT
|
||
* @version 3.2.0
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
define('AKSRT_VERSION', '3.4.1-dev');
|
||
define('AKSRT_DIR', get_template_directory());
|
||
define('AKSRT_URI', get_template_directory_uri());
|
||
|
||
// WordPress 6.7+ 会对 after_setup_theme 之前的翻译调用产生 _load_textdomain_just_in_time 通知,
|
||
// 此处抑制该通知以避免前台输出 debug 信息。
|
||
add_filter('doing_it_wrong_trigger_error', function ($trigger, $function_name) {
|
||
if ($function_name === '_load_textdomain_just_in_time') {
|
||
return false;
|
||
}
|
||
return $trigger;
|
||
}, 10, 2);
|
||
|
||
// ============================================================
|
||
// 全局辅助函数(必须在类文件加载之前定义)
|
||
// ============================================================
|
||
|
||
if (!function_exists('aksrt_get_setting')) {
|
||
/**
|
||
* 获取合并后的主题设置。
|
||
* 兼容旧版 aksrt_settings,以及新版 basic / seo 拆分存储。
|
||
*
|
||
* @return array
|
||
*/
|
||
function aksrt_get_all_settings() {
|
||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||
return \AKSRT\Settings\Settings::get_all();
|
||
}
|
||
|
||
$settings = get_option('aksrt_settings', array());
|
||
return is_array($settings) ? $settings : array();
|
||
}
|
||
|
||
/**
|
||
* 获取主题设置项
|
||
* @param string $key 璁剧疆閿悕
|
||
* @param string $default 榛樿鍊?
|
||
* @return mixed
|
||
*/
|
||
function aksrt_get_setting($key = '', $default = '') {
|
||
$all = aksrt_get_all_settings();
|
||
if (empty($key)) return $all;
|
||
return isset($all[$key]) ? $all[$key] : $default;
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 加载 Composer 依赖(第三方库 + 主题 PSR-4:AKSRT\ → src/)
|
||
// ============================================================
|
||
$composer_autoload = AKSRT_DIR . '/vendor/autoload.php';
|
||
if (!file_exists($composer_autoload)) {
|
||
wp_die(
|
||
esc_html__('AKSRT requires Composer dependencies. Run "composer install" in the theme directory.', 'aksrt'),
|
||
esc_html__('AKSRT theme', 'aksrt'),
|
||
['response' => 500]
|
||
);
|
||
}
|
||
require_once $composer_autoload;
|
||
|
||
// Theme classes live under src/ and only load via Composer PSR-4.
|
||
// Stale vendor/ (e.g. install before composer.json gained "AKSRT\\") leaves
|
||
// autoload_psr4 without AKSRT\ and fatals on the first DomainModel reference.
|
||
if (!class_exists(\AKSRT\Core\DomainModel::class, true)) {
|
||
wp_die(
|
||
esc_html__(
|
||
'AKSRT class autoload is incomplete: AKSRT\\Core\\DomainModel was not found. In the theme directory run "composer install" (or "composer dump-autoload -o"), then clear any opcode cache (opcache_reset / php-fpm reload).',
|
||
'aksrt'
|
||
),
|
||
esc_html__('AKSRT theme', 'aksrt'),
|
||
['response' => 500]
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 加载 Bootstrap(辅助函数 → 类文件 → 核心钩子)
|
||
// ============================================================
|
||
require_once AKSRT_DIR . '/inc/bootstrap.php';
|
||
|
||
// ============================================================
|
||
// 主题激活时刷新重写规则
|
||
// ============================================================
|
||
add_action('after_switch_theme', function () {
|
||
flush_rewrite_rules();
|
||
});
|
||
|
||
// §3.4 custom_filter: register custom query vars for archive filter
|
||
add_filter('query_vars', function ($vars) {
|
||
$vars[] = 'aksrt_filter_cat';
|
||
$vars[] = 'aksrt_filter_tag';
|
||
// Dynamic aksrt_cf_* vars are handled via $_GET fallback in templates
|
||
return $vars;
|
||
});
|
||
|
||
// 主题切换离开时:清理本主题注册的 cron 任务
|
||
add_action('switch_theme', function () {
|
||
$hooks = ['aksrt_dl_token_cleanup', 'aksrt_notification_cleanup', 'aksrt_creator_income_settlement', 'aksrt_vip_membership_expiry'];
|
||
foreach ($hooks as $hook) {
|
||
$next = wp_next_scheduled($hook);
|
||
while ($next) {
|
||
wp_unschedule_event($next, $hook);
|
||
$next = wp_next_scheduled($hook);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 兜底:检测版本变更时自动刷新重写规则
|
||
if (get_option('aksrt_flushed_version') !== AKSRT_VERSION) {
|
||
delete_option('rewrite_rules');
|
||
add_action('init', function () {
|
||
flush_rewrite_rules();
|
||
update_option('aksrt_flushed_version', AKSRT_VERSION);
|
||
}, 99);
|
||
}
|
||
|
||
// ============================================================
|
||
// 初始化所有单例实例
|
||
// ============================================================
|
||
\AKSRT\Core\Setup::get_instance()->init();
|
||
\AKSRT\WidgetSystem\Runtime::init();
|
||
\AKSRT\Core\Scripts::get_instance()->init();
|
||
\AKSRT\Admin\Admin::get_instance()->init();
|
||
if (class_exists('\AKSRT\Admin\NavMenuOptions')) {
|
||
\AKSRT\Admin\NavMenuOptions::init();
|
||
}
|
||
if (class_exists('\AKSRT\Admin\PageConfigMetaBox')) {
|
||
\AKSRT\Admin\PageConfigMetaBox::init();
|
||
}
|
||
\AKSRT\Auth\OAuth::get_instance()->init();
|
||
if (class_exists('\AKSRT\Auth\WeixinGzh')) {
|
||
\AKSRT\Auth\WeixinGzh::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Frontend\WeChatShare')) {
|
||
\AKSRT\Frontend\WeChatShare::get_instance()->init();
|
||
}
|
||
|
||
// 初始化代理登录(根据配置模式选择性加载)
|
||
$aksrt_proxy_mode = \AKSRT\Auth\OAuthProviderCatalog::proxy_mode();
|
||
if ($aksrt_proxy_mode === 'client' && class_exists('\AKSRT\Auth\OAuthProxyClient')) {
|
||
\AKSRT\Auth\OAuthProxyClient::get_instance()->init();
|
||
}
|
||
if ($aksrt_proxy_mode === 'server' && class_exists('\AKSRT\Auth\OAuthProxyServer')) {
|
||
\AKSRT\Auth\OAuthProxyServer::get_instance()->init();
|
||
}
|
||
|
||
\AKSRT\Settings\Settings::get_instance()->init();
|
||
if (class_exists('\AKSRT\Settings\MailNotifications')) {
|
||
\AKSRT\Settings\MailNotifications::init();
|
||
}
|
||
\AKSRT\Auth\Router::get_instance()->init();
|
||
\AKSRT\Core\Templates::get_instance()->init();
|
||
\AKSRT\Auth\Moderation::get_instance()->init();
|
||
if (class_exists('\AKSRT\Extensions\SecurityHardening')) {
|
||
\AKSRT\Extensions\SecurityHardening::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Extensions\SystemTools')) {
|
||
\AKSRT\Extensions\SystemTools::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Frontend\PageAdminSet')) {
|
||
\AKSRT\Frontend\PageAdminSet::get_instance()->init();
|
||
}
|
||
|
||
// 短代码统一注册入口(registry)
|
||
if (class_exists('\AKSRT\Core\Shortcodes')) {
|
||
add_action('init', function () {
|
||
\AKSRT\Core\Shortcodes::get_instance()->register();
|
||
});
|
||
}
|
||
|
||
// 用户限制/封禁系统
|
||
if (class_exists('\AKSRT\User\Restriction')) {
|
||
\AKSRT\User\Restriction::get_instance()->init();
|
||
}
|
||
|
||
// 用户中心 & 公开主页协调类
|
||
if (class_exists('\AKSRT\User\Center')) {
|
||
\AKSRT\User\Center::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\NotificationPreferences')) {
|
||
\AKSRT\User\NotificationPreferences::init();
|
||
}
|
||
if (class_exists('\AKSRT\User\BadgeAutoAward')) {
|
||
\AKSRT\User\BadgeAutoAward::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\Home')) {
|
||
\AKSRT\User\Home::get_instance()->init();
|
||
}
|
||
|
||
// 数据表迁移器:注册 hook(after_switch_theme + admin_init 双触发)
|
||
if (class_exists('\AKSRT\Core\DBMigrator')) {
|
||
\AKSRT\Core\DBMigrator::init();
|
||
}
|
||
|
||
// 积分获取入口(注册赠送 / 评论奖励 / 签到)
|
||
if (class_exists('\AKSRT\Credits\Earn')) {
|
||
\AKSRT\Credits\Earn::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Credits\Signin')) {
|
||
\AKSRT\Credits\Signin::get_instance()->init();
|
||
}
|
||
if (class_exists('AKSRT\\Payment\\CreatorRevenue')) {
|
||
\AKSRT\Payment\CreatorRevenue::get_instance()->init();
|
||
}
|
||
if (class_exists('AKSRT\\Payment\\ReferralAttribution')) {
|
||
\AKSRT\Payment\ReferralAttribution::get_instance()->init();
|
||
}
|
||
if (class_exists('AKSRT\\Payment\\CommissionLedger')) {
|
||
\AKSRT\Payment\CommissionLedger::get_instance()->init();
|
||
}
|
||
if (class_exists('AKSRT\\Payment\\InviteCode')) {
|
||
\AKSRT\Payment\InviteCode::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Content\Pay')) {
|
||
\AKSRT\Content\Pay::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Content\VipService')) {
|
||
\AKSRT\Content\VipService::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Payment\ChargeCard')) {
|
||
\AKSRT\Payment\ChargeCard::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Payment\OrderCenterRest')) {
|
||
\AKSRT\Payment\OrderCenterRest::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Ticket\TicketService')) {
|
||
\AKSRT\Ticket\TicketService::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Credits\BalanceService')) {
|
||
\AKSRT\Credits\BalanceService::get_instance()->init();
|
||
}
|
||
// Download State & Docs Presenter are pure service singletons (no hooks)
|
||
if (class_exists('\AKSRT\Content\DownloadState')) {
|
||
\AKSRT\Content\DownloadState::get_instance();
|
||
}
|
||
if (class_exists('\AKSRT\Core\DocsPresenter')) {
|
||
\AKSRT\Core\DocsPresenter::get_instance();
|
||
}
|
||
if (class_exists('\AKSRT\Content\Visibility')) {
|
||
\AKSRT\Content\Visibility::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Ai\AI')) {
|
||
\AKSRT\Ai\AI::get_instance(); // config-gated; available when admin enables
|
||
}
|
||
if (class_exists('\AKSRT\Payment\WeChatNotify')) {
|
||
\AKSRT\Payment\WeChatNotify::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\CommentActions')) {
|
||
\AKSRT\Core\CommentActions::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\GoRedirect')) {
|
||
\AKSRT\Core\GoRedirect::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\PostsNavs')) {
|
||
\AKSRT\Core\PostsNavs::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Identity\Identity')) {
|
||
\AKSRT\Identity\Identity::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\AuthApply')) {
|
||
\AKSRT\User\AuthApply::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\BanAppeal')) {
|
||
\AKSRT\User\BanAppeal::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Messages\SiteNotice')) {
|
||
\AKSRT\Messages\SiteNotice::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Extensions\APIAudit')) {
|
||
\AKSRT\Extensions\APIAudit::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Extensions\MeiliSearch')) {
|
||
\AKSRT\Extensions\MeiliSearch::get_instance()->init();
|
||
// Async single post index (debounced)
|
||
add_action('aksrt_meilisearch_index_single', function (int $post_id) {
|
||
\AKSRT\Extensions\MeiliSearch::get_instance()->index_post($post_id);
|
||
});
|
||
}
|
||
if (class_exists('\AKSRT\Extensions\SearchPush')) {
|
||
\AKSRT\Extensions\SearchPush::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Extensions\SearchPushMetaBox')) {
|
||
\AKSRT\Extensions\SearchPushMetaBox::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\SEOHead')) {
|
||
\AKSRT\Core\SEOHead::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\UpdateChecker')) {
|
||
\AKSRT\Core\UpdateChecker::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\CategoryRewrite')) {
|
||
\AKSRT\Core\CategoryRewrite::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Core\FrontSubmit')) {
|
||
\AKSRT\Core\FrontSubmit::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Attachment\UploadLimits')) {
|
||
\AKSRT\Attachment\UploadLimits::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\Payment\Order')) {
|
||
\AKSRT\Payment\Order::get_instance()->init();
|
||
}
|
||
|
||
// Register built-in payment gateways
|
||
if (class_exists('\AKSRT\Payment\Gateway')) {
|
||
$gw = \AKSRT\Payment\Gateway::get_instance();
|
||
// 注册支付回调路由(/payment/{gateway}/notify|return|cancel)。
|
||
$gw->init();
|
||
if (class_exists('\AKSRT\Payment\BalanceGateway')) {
|
||
$gw->register('balance', new \AKSRT\Payment\BalanceGateway());
|
||
}
|
||
if (class_exists('\AKSRT\Payment\Gateways\AlipayGateway')) {
|
||
$gw->register('alipay', new \AKSRT\Payment\Gateways\AlipayGateway());
|
||
}
|
||
if (class_exists('\AKSRT\Payment\Gateways\WechatPayGateway')) {
|
||
$gw->register('wechatpay', new \AKSRT\Payment\Gateways\WechatPayGateway());
|
||
}
|
||
if (class_exists('\AKSRT\Payment\Gateways\PayPalGateway')) {
|
||
$gw->register('paypal', new \AKSRT\Payment\Gateways\PayPalGateway());
|
||
}
|
||
if (class_exists('\AKSRT\Payment\Gateways\StripeGateway')) {
|
||
$gw->register('stripe', new \AKSRT\Payment\Gateways\StripeGateway());
|
||
}
|
||
if (class_exists('\AKSRT\Payment\Gateways\EpayGateway')) {
|
||
$gw->register('epay_alipay', new \AKSRT\Payment\Gateways\EpayGateway('alipay'));
|
||
$gw->register('epay_wxpay', new \AKSRT\Payment\Gateways\EpayGateway('wxpay'));
|
||
}
|
||
do_action('aksrt_register_payment_gateways', $gw);
|
||
}
|
||
|
||
if (class_exists('\AKSRT\Core\ResourceMetaBox')) {
|
||
\AKSRT\Core\ResourceMetaBox::get_instance()->init();
|
||
}
|
||
|
||
// 分类/标签局部配置(term meta 字段)
|
||
if (class_exists('\AKSRT\Core\TermMeta')) {
|
||
\AKSRT\Core\TermMeta::get_instance()->init();
|
||
}
|
||
|
||
// 文章局部配置 meta box(仅后台)
|
||
if (is_admin() && class_exists('\AKSRT\Core\PostMetaBox')) {
|
||
\AKSRT\Core\PostMetaBox::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Core\SEOMetaBox')) {
|
||
\AKSRT\Core\SEOMetaBox::get_instance()->init();
|
||
}
|
||
|
||
// 绔欑偣璁块棶閲忕粺璁?
|
||
if (class_exists('\AKSRT\Core\StatsTracker')) {
|
||
\AKSRT\Core\StatsTracker::init();
|
||
}
|
||
|
||
// WordPress 原生"用户"菜单:积分管理
|
||
if (class_exists('\AKSRT\User\CreditsAdmin')) {
|
||
\AKSRT\User\CreditsAdmin::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\BalanceAdmin')) {
|
||
\AKSRT\User\BalanceAdmin::get_instance()->init();
|
||
}
|
||
|
||
if (class_exists('\AKSRT\User\Badges')) {
|
||
\AKSRT\User\Badges::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\BadgesAdmin')) {
|
||
\AKSRT\User\BadgesAdmin::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\Level')) {
|
||
\AKSRT\User\Level::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\LevelAdmin')) {
|
||
\AKSRT\User\LevelAdmin::get_instance()->init();
|
||
}
|
||
if (class_exists('\AKSRT\User\ProfileAdmin')) {
|
||
\AKSRT\User\ProfileAdmin::init();
|
||
}
|
||
if (class_exists('\AKSRT\User\Metrics')) {
|
||
\AKSRT\User\Metrics::get_instance()->init();
|
||
}
|
||
|
||
// WordPress 原生侧栏:支付/订单运营中心(legacy aksrtpay 对齐)
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayMenus')) {
|
||
\AKSRT\Admin\Native\PayMenus::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayCoupons')) {
|
||
\AKSRT\Admin\Native\PayCoupons::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayChargeCards')) {
|
||
\AKSRT\Admin\Native\PayChargeCards::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) {
|
||
\AKSRT\Admin\Native\PayOrders::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayProducts')) {
|
||
\AKSRT\Admin\Native\PayProducts::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayWithdrawals')) {
|
||
\AKSRT\Admin\Native\PayWithdrawals::get_instance()->init();
|
||
}
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayIncome')) {
|
||
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
|
||
}
|
||
|
||
// 后台文章列表增强:互动/销售数据列 + 快捷编辑配置项
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\PostListEnhance')) {
|
||
\AKSRT\Admin\Native\PostListEnhance::get_instance()->init();
|
||
}
|
||
|
||
// WordPress 原生用户菜单:消息/推广码/认证/禁封(legacy users.php 子菜单对齐)
|
||
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
|
||
\AKSRT\Admin\Native\UsersMenus::get_instance()->init();
|
||
}
|
||
|
||
// ============================================================
|
||
// 主题删除时的数据库清理(仅在用户启用清理选项时生效)
|
||
// uninstall.php 会检查 aksrt_cleanup_on_uninstall 选项
|
||
// ============================================================
|
||
register_deactivation_hook(__FILE__, function () {
|
||
// 主题停用时不做任何清理,仅在删除时由 uninstall.php 处理。
|
||
// 自定义表(aksrt_*)在主题切换后保留数据,确保重新启用时不丢失。
|
||
});
|